I made a calculator app (swift), and I set up some UIKeyCommands so that if the user has a bluetooth keyboard, they can type numbers/symbols into the calculator.
They work like so:
UIKeyCommand(input: "4", modifierFlags: [], action: "TypeFourInCalculator:")
func TypeFourInCalculator(sender: UIKeyCommand) {
btn4Press(UIButton)
}
That all worked well, adding a four into the calculator when the user pressed the four key (even though the calculator itself has no text field). However: I also have a couple of standard text fields, and I want the UIKeyCommands to stop when the user goes into one of those text fields (so they can type regularly with the BT keyboard again). Without disabling the UIKeyCommands, typing results in calculator functions and no input into the text field.
So I tried this in an attempt to disable UIKeyCommands when the text field becomes the first responder:
let globalKeyCommands = [UIKeyCommand(input: "4", modifierFlags: [], action: "TypeFourInCalculator:"), UIKeyCommand(input: "5", modifierFlags: [], action: "TypeFiveInCalculator:")]
override var keyCommands: [UIKeyCommand]? {
if powertextfield.isFirstResponder() == false { // if the user isn't typing into that text field
return globalKeyCommands // use key commands
} else { // if the user is typing
return nil // disable those UIKeyCommands
}
This works occasionally but yet often doesn't work. If the user has not typed anything with the BT keyboard yet (i.e. not activating the key commands, I guess) then they can type with the BT keyboard as normal into a text field. But if they have already been typing numbers into the calculator via UIKeyCommand, they can not type into the text field (well, sometimes it works with normal behavior, sometimes it fails like it did before I added that preventative code). Typed text just doesn't appear in that text field and, instead, it just calls the calculator command.
So what can I do to disable these UIKeyCommands when the user starts typing in a normal text field?
Instead of making keyCommands
a computed property, you can use addKeyCommand(_:)
and removeKeyCommand(_:)
methods for UIViewController
s. Subclass your text field like this:
class PowerTextField: UITextField {
var enableKeyCommands: (Bool->())?
override func becomeFirstResponder() -> Bool {
super.becomeFirstResponder()
enableKeyCommands?(false)
return true
}
override func resignFirstResponder() -> Bool {
super.resignFirstResponder()
enableKeyCommands?(true)
return true
}
}
Then in your UIViewController
:
override func viewDidLoad() {
super.viewDidLoad()
// globalKeyCommands is a view controller property
// Add key commands to be on by default
for command in globalKeyCommands {
self.addKeyCommand(command)
}
// Configure text field to callback when
// it should enable key commands
powerTextField.enableKeyCommands = { [weak self] enabled in
guard let commands = self?.globalKeyCommands else {
return
}
if enabled {
for command in globalKeyCommands {
self?.addKeyCommand(command)
}
} else {
for command in globalKeyCommands {
self?.removeKeyCommand(command)
}
}
}
}
Instead of an optional stored procedure that gets configured by the UIViewController
, you could setup a delegate protocol for the UITextField
that the UIViewController
will adopt.
Also, if you need to stop these key commands when a UIAlertController
pops up, you can make a subclass of UIAlertController
that implements the viewWillAppear(_:)
and viewWillDisappear(_:)
event methods similar to how you implemented becomeFirstResponder()
and resignFirstResponder()
, by calling an enableKeyCommands(_:)
optional stored procedure that's configured by your main view controller during its viewDidLoad()
.
As for the explanation of why this is happening, perhaps the most likely explanation is that it's a bug. I'm not sure why this is irregular in your testing though. I think you could try to isolate under what conditions it works or doesn't. It's not obvious why this would only happen for bluetooth keyboards, but there are plenty of edge cases that can creep in when you start introducing wireless technologies.
So I was facing the same problem and found a really good and simple solution.
I made a way to figure out, if the current firstResponder is a text field, or similar.
extension UIResponder {
private weak static var _currentFirstResponder: UIResponder? = nil
public static var isFirstResponderTextField: Bool {
var isTextField = false
if let firstResponder = UIResponder.currentFirstResponder {
isTextField = firstResponder.isKind(of: UITextField.self) || firstResponder.isKind(of: UITextView.self) || firstResponder.isKind(of: UISearchBar.self)
}
return isTextField
}
public static var currentFirstResponder: UIResponder? {
UIResponder._currentFirstResponder = nil
UIApplication.shared.sendAction(#selector(findFirstResponder(sender:)), to: nil, from: nil, for: nil)
return UIResponder._currentFirstResponder
}
@objc internal func findFirstResponder(sender: AnyObject) {
UIResponder._currentFirstResponder = self
}
}
And then simply used this boolean value to determine which UIKeyCommands to return when overriding the keyCommands var:
override var keyCommands: [UIKeyCommand]? {
var commands = [UIKeyCommand]()
if !UIResponder.isFirstResponderTextField {
commands.append(UIKeyCommand(title: "Some title", image: nil, action: #selector(someAction), input: "1", modifierFlags: [], propertyList: nil, alternates: [], discoverabilityTitle: "Some title", attributes: [], state: .on))
}
commands.append(UIKeyCommand(title: "Some other title", image: nil, action: #selector(someOtherAction), input: "2", modifierFlags: [.command], propertyList: nil, alternates: [], discoverabilityTitle: "Some title", attributes: [], state: .on))
return commands
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With