Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UIKeyCommand is not disabled when typing into a text field (Swift)

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?

like image 601
owlswipe Avatar asked May 20 '16 22:05

owlswipe


2 Answers

Instead of making keyCommands a computed property, you can use addKeyCommand(_:) and removeKeyCommand(_:) methods for UIViewControllers. 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.

like image 100
Christopher Whidden Avatar answered Nov 15 '22 03:11

Christopher Whidden


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
    }
like image 31
Mikkel Cortnum Avatar answered Nov 15 '22 04:11

Mikkel Cortnum