Is there any way to disable dictation support of a UITextField? - xcode

So I'm working on a tvos app in swift and I was wondering if it's possible to disable dictation support for a custom UITextField. It doesn't really work well for it and I don't want the user to be able to do so

Did you try to use the textfield's keyboardType property? Maybe you can change the text input type, so the dictation function is automatically not shown.
Documentation: https://developer.apple.com/library/tvos/documentation/UIKit/Reference/UITextInputTraits_Protocol/index.html#//apple_ref/occ/intfp/UITextInputTraits/keyboardType

This is a Swift 4 solution based on #BadPirate's hack. It will trigger the initial bell sound stating that dictation started, but the dictation layout will never appear on the keyboard.
This will not hide the dictation button from your keyboard: for that the only option seems to be to use an email layout with UIKeyboardType.emailAddress.
In viewDidLoad of the view controller owning the UITextField for which you want to disable dictation:
// Track if the keyboard mode changed to discard dictation
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardModeChanged),
name: UITextInputMode.currentInputModeDidChangeNotification,
object: nil)
Then the custom callback:
#objc func keyboardModeChanged(notification: Notification) {
// Could use `Selector("identifier")` instead for idSelector but
// it would trigger a warning advising to use #selector instead
let idSelector = #selector(getter: UILayoutGuide.identifier)
// Check if the text input mode is dictation
guard
let textField = yourTextField as? UITextField
let mode = textField.textInputMode,
mode.responds(to: idSelector),
let id = mode.perform(idSelector)?.takeUnretainedValue() as? String,
id.contains("dictation") else {
return
}
// If the keyboard is in dictation mode, hide
// then show the keyboard without animations
// to display the initial generic keyboard
UIView.setAnimationsEnabled(false)
textField.resignFirstResponder()
textField.becomeFirstResponder()
UIView.setAnimationsEnabled(true)
// Do additional update here to inform your
// user that dictation is disabled
}

Related

UITextView with custom NSUndoManager breaks auto-enabling of software keyboard undo button

I have a custom UITextView where entering text causes the undo button in the software keyboard to be enabled. When I override the undo manager like this:
class TextView: UITextView {
let _undoManager = UndoManager()
override var undoManager: UndoManager? {
return _undoManager
}
}
the auto-enabling of the undo button doesn't work anymore. What is the correct way of providing my own undo manager?

How to align a toolbar (or its items) with the leading edge of a split view controller's child?

In iOS, a toolbar can be added to any view. In macOS however, it seems only possible to add a toolbar to a window.
I'm working on an app with a split view controller with a toolbar but the toolbar's items only have a meaning with respect to the right view controller's context.
E.g. let's say I have a text editor of some sort, where the left pane shows all documents (like in the Notes app) and the right pane shows the actual text which can be edited. The formatting buttons only affect the text in the right pane. Thus, it seems very intuitive to place the toolbar within that right pane instead of stretching it over the full width of the window.
Is there some way to achieve this?
(Or is there a good UX reason why this would be a bad practice?)
I've noticed how Apple solved this problem in terms of UX in their Notes app: They still use a full-width toolbar but align the button items that are only related to the right pane with the leading edge of that pane.
So in case, there is no way to place a toolbar in a view controller, how can I align the toolbar items with the leading edge of the right view controller as seen in the screenshot above?
Edit:
According to TimTwoToes' answer and the posts linked by Willeke in the comments, it seems to be possible to use Auto Layout for constraining a toolbar item with the split view's child view. This solution would work if there was a fixed toolbar layout. However, Apple encourages (for a good reason) to let users customize your app's toolbar.
Thus, I cannot add constraints to a fixed item in the toolbar. Instead, a viable solution seems to be to use a leading flexible space and adjust its size accordingly.
Initial Notes
It turns out this is tricky because there are many things that need to be considered:
Auto Layout doesn't seem to work properly with toolbar items. (I've read a few posts mentioning that Apple has classified this as a bug.)
Normally, the user can customize your app's toolbar (add and remove items). We should not deprive the user of that option.
Thus, simply constraining a particular toolbar item with the split view or a layout guide is not an option (because the item might be at a different position than expected or not there at all).
After hours of "hacking", I've finally found a reliable way to achieve the desired behavior that doesn't use any internal / undocumented methods. Here's how it looks:
How To
Instead of a standard NSToolbarFlexibleSpaceItem create an NSToolbarItem with a custom view. This will serve as your flexible, resizing space. You can do that in code or in Interface Builder:
Create outlets/properties for your toolbar and your flexible space (inside the respective NSWindowController):
#IBOutlet weak var toolbar: NSToolbar!
#IBOutlet weak var tabSpace: NSToolbarItem!
Create a method inside the same window controller that adjusts the space width:
private func adjustTabSpaceWidth() {
for item in toolbar.items {
if item == tabSpace {
guard
let origin = item.view?.frame.origin,
let originInWindowCoordinates = item.view?.convert(origin, to: nil),
let leftPane = splitViewController?.splitViewItems.first?.viewController.view
else {
return
}
let leftPaneWidth = leftPane.frame.size.width
let tabWidth = max(leftPaneWidth - originInWindowCoordinates.x, MainWindowController.minTabSpaceWidth)
item.set(width: tabWidth)
}
}
}
Define the set(width:) method in an extension on NSToolbarItem as follows:
private extension NSToolbarItem {
func set(width: CGFloat) {
minSize = .init(width: width, height: minSize.height)
maxSize = .init(width: width, height: maxSize.height)
}
}
Make your window controller conform to NSSplitViewDelegate and assign it to your split view's delegate property.1 Implement the following NSSplitViewDelegate protocol method in your window controller:
override func splitViewDidResizeSubviews(_ notification: Notification) {
adjustTabSpaceWidth()
}
This will yield the desired resizing behavior. (The user will still be able to remove the space completely or reposition it, but he can always add it back to the front.)
1 Note:
If you're using an NSSplitViewController, the system automatically assigns that controller to its split view's delegate property and you cannot change that. As a consequence, you need to subclass NSSplitViewController, override its splitViewDidResizeSubviews() method and notify the window controller from there. Your can achieve that with the following code:
protocol SplitViewControllerDelegate: class {
func splitViewControllerDidResize(_ splitViewController: SplitViewController)
}
class SplitViewController: NSSplitViewController {
weak var delegate: SplitViewControllerDelegate?
override func splitViewDidResizeSubviews(_ notification: Notification) {
delegate?.splitViewControllerDidResize(self)
}
}
Don't forget to assign your window controller as the split view controller's delegate:
override func windowDidLoad() {
super.windowDidLoad()
splitViewController?.delegate = self
}
and to implement the respective delegate method:
extension MainWindowController: SplitViewControllerDelegate {
func splitViewControllerDidResize(_ splitViewController: SplitViewController) {
adjustTabSpaceWidth()
}
}
There is no native way to achieve a "local" toolbar. You would have to create the control yourself, but I believe it would be simpel to make.
Aligning the toolbar items using autolayout is described here. Align with custom toolbar item described by Mischa.
The macOS way is to use the Toolbar solution and make them context sensitive. In this instance the text attribute buttons would enable when the right pane has the focus and disable when it looses the focus.

UI Save/Restoration mechanism in Cocoa via Swift

I'd like to save the state of Check Box, quit application, then launch macOS app again to see restored state of my Check Box. But there's no restored state in UI of my app.
What am I doing wrong?
import Cocoa
class ViewController: NSViewController {
#IBOutlet weak var tick: NSButton!
override func viewDidLoad() {
super.viewDidLoad()
}
override func encodeRestorableState(with coder: NSCoder) {
super.encodeRestorableState(with: coder)
coder.encode(tick.state, forKey: "")
}
override func restoreState(with coder: NSCoder) {
super.restoreState(with: coder)
if let state = coder.decodeObject(forKey: "") as? NSControl.StateValue {
tick.state = state
}
}
}
To the best of my knowledge, this is the absolute minimum you need to implement custom UI state restoration of a window and/or its contents.
In this example, I have a window with a checkbox and that checkbox's state represents some custom view state that I want to restore when the app is relaunched.
The project contains a single window with a single checkbox button. The button's value is bound to the myState property of the window's content view controller. So, technically, the fact that this is a checkbox control is irrelevant; we're actually going to preserve and restore the myState property (the UI takes care of itself).
To make this work, the window's restorable property is set to true (in the window object inspector) and the window is assigned an identifier ("PersistentWindow"). NSWindow is subclassed (PersistentWindow) and the subclass implements the restorableStateKeyPaths property. This property lists the custom properties to be preserved/restored.
Note: if you can define your UI state restoration in terms of a list of key-value compliant property paths, that is (by far) the simplest solution. If not, you must implement encodeRestorableState / restoreState and are responsible for calling invalidateRestorableState.
Here's the custom window class:
class PersistentWindow: NSWindow {
// Custom subclass of window the perserves/restores UI state
// The simple way to preserve and restore state information is to just declare the key-value paths
// of the properties you want preserved/restored; Cocoa does the rest
override class var restorableStateKeyPaths: [String] {
return [ "self.contentViewController.myState" ]
}
// Alternatively, if you have complex UI state, you can implement these methods
// override func encodeRestorableState(with coder: NSCoder) {
// // optional method to encode special/complex view state here
// }
//
// override func restoreState(with coder: NSCoder) {
// // companion method to decode special/complex view state
// }
}
And here's the (relevant portion) of the content view controller
class ViewController: NSViewController {
#objc var myState : Bool = false
blah, blah, blah
}
(I built this as a Cocoa app project, which I could upload if someone tells me where I could upload it to.)
Actually you don't have to go through restorableStateKeyPaths / KVO / KVC if you don't want to.
I was stuck in the same state as you with the encodeRestorableState() & restoreState() methods not being called but found out what was missing.
In System Preferences > General, make sure "Close windows when quitting an app" is unchecked.
Make sure that the NSWindow containing your view has "Restorable" behavior enabled in IB.
Make sure that your NSViewController has a "Restoration ID" set.
Your NSViewController won't be encoded unless you call invalidateRestorableState(). You need to call this each time there's a state in your NSViewController that changes and that you want to have saved.
When no state changes in the NSViewController after having restored it, its state would not be encoded again when closing the app. Which would cause the custom states to not be restored when relaunching the app. The simplest way I found is to also call invalidateRestorableState() in viewDidLoad(), so that state is always saved.
After doing all that, I didn't even have to additionally implement NSApplicationDelegate or NSWindowRestoration protocol methods. So the state restoration of the NSViewController is pretty self-contained. Only external property is restorable NSWindow.
After losing a couple of hours of my life to this problem I finally got it working. Some of the information in the other answers was helpful, some was missing, some was not necessary.
Here is my minimal example based on a new Xcode 13 project:
in AppDelegate add (this is missing in the other examples):
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { return true }
in ViewController add:
#objc var myState : Bool = false
override class var restorableStateKeyPaths: [String] {
return [ "myState" ]
}
set up some UI and bind it to myState to see what is going on
make sure System Preferences > General > "Close windows when quitting an app" is unchecked
Things that I did not need to do:
create a custom window subclass
set a custom restoration id
it worked fine just with Xcode start/stop

Dismissing keyboard in UITextField with RAC(5)?

Newbie to ReactiveCocoa and ReactiveSwfit here... Sorry if the answer is obvious.
I am trying to adapt the Start Developing iOS Apps with Swift sample to ReactiveSwift / ReactiveCocoa, and I am running into an issue with "translating" the UITextField's Delegate method -- which gets rid of the keyboard and essentially ends the editing (so I can capture the text field in the mealNameLabel) :
func textFieldShouldReturn(_ textField: UITextField) -> Bool
I am using
nameTextField.reactive.textValues.observeValues { value in
viewModel.mealName.swap(value ?? "")
}
// Setup bindings to update the view's meal label
// based on data from the View Model
mealNameLabel.reactive.text <~ viewModel.mealLabel
to get the value from the text field into the view model and percolate the view model's label back to the UILabel (convoluted...)
That works fine, as long as I maintain the viewController as the UITextField's delegate and I still implement the method depicted in the tutorial and mentioned above. Essentially :
override func viewDidLoad() {
super.viewDidLoad()
nameTextField.delegate = self
// view controller logic
...
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
// Hide the keyboard.
textField.resignFirstResponder()
return true
}
I tried using
nameTextField.reactive.controlEvents
but that failed miserably due to my lack of understanding of controlEvents (docs anywhere ?).
So what do I need to do to make the keyboard disappear when the user is done editing, the "reactive way" ?
Thanks !!!
(Of course right after I post my question...)
It looks like this might actually do the trick :
nameTextField.reactive.controlEvents(UIControlEvents.primaryActionTriggered)
.observeValues { textField in
textField.resignFirstResponder()
}
After fiddling with the different event types, it looks like .primaryActionTriggered is what gets triggered when the "Done" button is pressed.
Any better way to do this ?

NSBorderlessWindowMask Subview NSTextfield not keybard editable Swift

I'm using a NSBorderlessWindowMask for my main window on a Swift project (without storyboards), when I load a Subview, the NSTextfield outlet is not keybard editable. I already put this code on the initialisation:
self.window?.makeKeyWindow()
self.window?.becomeKeyWindow()
this allows the outlet to be "blue" like on focus, but the keyboard editing is disabled, i can copy/paste on the textfield
You need to use a custom subclass of NSWindow and override canBecomeKeyWindow() to return true. By default, it returns false for windows without title bars (as documented).
You probably want to do the same for canBecomeMainWindow().
Also, never call becomeKeyWindow() (except to call super in an override). That is called by Cocoa to inform the window that it has become the key window. It does not instruct the window to become the key window.
I found an awesome workaround for this problem:
basically setup at beginning the NSWindow mask as NSTitledWindowMask, when application is loaded, remove set up the new mask NSBorderlessWindowMask
func applicationWillFinishLaunching(notification: NSNotification) {
self.window?.titleVisibility = NSWindowTitleVisibility.Hidden
self.window?.styleMask = NSTitledWindowMask // adds title bar
}
func applicationDidFinishLaunching(aNotification: NSNotification) {
self.window?.makeKeyWindow()
self.window?.becomeKeyWindow()
self.window.setIsVisible(true)
self.window?.styleMask = NSBorderlessWindowMask // removes title bar
}

Resources