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

2

7

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?

Bookmaker answered 20/5, 2016 at 22:34 Comment(2)
I'd use some logging to see when keyCommands is being queried to make sure it's actually being updated when you think it is being updated.Frontpage
The answer below was really helpful, so thanks Chris. If anyone stopping by here needs any help, just comment below and I can assist you.Bookmaker
O
3

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.

Owner answered 1/6, 2016 at 5:52 Comment(8)
Where do I put that second block of code? If I just put it alone anywhere in my UIViewController it says "Error: Expected Declaration".Bookmaker
@JohnRamos there are multiple places you can put it that could make sense depending on your app, but I'm guessing you probably should put it in viewDidLoad()Owner
Ok, so I put the first block of code right at the bottom of my calculator view controller file, inside my UIViewController class but not inside anything else, and the second block of code inside viewdidload, and all my other UIKeyCommand code inside viewdidload, and my project build failed with the error "Segmentation Fault 11."Bookmaker
@JohnRamos the first block of code is part of the text field's class declaration. If you already had a subclass for it, add the code there. If not, make a separate file for declaring this subclass. Make sure you use this subclass either where you instantiate your text field in code or in Interface Builder. The second block of code goes in viewDidLoad(). Think of this as configuring your custom text field with a callback. In viewDidLoad() you'll also want to add all of your key commands to the view controller so that they're on by default.Owner
When I put that first block of code in a new file, the second block of code can't read it. There's an error on the first line of the second block: Value of type 'UITextView' has no member 'enableKeyCommands'Bookmaker
@JohnRamos that's because you need to use the custom subclass (PowerTextField or whatever you want to name it), not UITextField or UITextView. Your variable name was powertextfield so I assumed it was a UITextField. You'll need to change the class declaration to inherit from UITextView, if that's the case. Regardless, you need to use the custom subclass. If you're using Interface Builder, click on your text view and assign its custom class using the Identity Inspector.Owner
I don't really know what you mean by "custom subclass" (I'm fairly new to swift) but I did change the class to a UITextView (it is a UITextView), so now the class declaration is this: class powerTextField: UITextView {... I also connected the text view in the Interface Builder to the class. Yet, as I retype "powertextfield." there's no option for enableKeyCommands. This is regardless of whether it auto-completes powertextfield as the text view or as the class, the only options after the . are text viewy stuff like editingEnabled and text but now enableKeyCommands.Bookmaker
Let us continue this discussion in chat.Owner
C
2

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
    }
Clapp answered 11/10, 2019 at 13:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.