Stop UIKeyCommand repeated actions
Asked Answered
L

4

8

If a key command is registered, it's action might be called many times if the user holds down the key too long. This can create very weird effects, like ⌘N could repeatedly open a new view many times. Is there any easy way to stop this behavior without resorting to something like a boolean "already triggered" flag?

Here's how I register two different key commands:

#pragma mark - KeyCommands

- (BOOL)canBecomeFirstResponder {
    return YES;
}

- (NSArray<UIKeyCommand *>*)keyCommands {
    return @[
             [UIKeyCommand keyCommandWithInput:@"O" modifierFlags:UIKeyModifierCommand action:@selector(keyboardShowOtherView:) discoverabilityTitle:@"Show Other View"],
             [UIKeyCommand keyCommandWithInput:@"S" modifierFlags:UIKeyModifierCommand action:@selector(keyboardPlaySound:) discoverabilityTitle:@"Play Sound"],
             ];
}

- (void)keyboardShowOtherView:(UIKeyCommand *)sender {
    NSLog(@"keyboardShowOtherView");
    [self performSegueWithIdentifier:@"showOtherView" sender:nil];
}

- (void)keyboardPlaySound:(UIKeyCommand *)sender {
    NSLog(@"keyboardPlaySound");
    [self playSound:sender];
}

#pragma mark - Actions

- (IBAction)playSound:(id)sender {
    AudioServicesPlaySystemSound(1006); // Not allowed in the AppStore
}

A sample project can be downloaded here: TestKeyCommands.zip

Liggett answered 18/1, 2017 at 23:45 Comment(0)
P
8

In general, you don't need to deal with this, since the new view would usually become the firstReponder and that would stop the repeating. For the playSound case, the user would realize what is happening and take her finger off of the key.

That said, there are real cases where specific keys should never repeat. It would be nice if Apple provided a public API for that. As far as I can tell, they do not.

Given the '//Not allowed in the AppStore' comment in your code, it seems like you're OK using a private API. In that case, you could disable repeating for a keyCommand with:

UIKeyCommand *keyCommand =  [UIKeyCommand ...];
[keyCommand setValue:@(NO) forKey:@"_repeatable"];
Philbert answered 30/10, 2017 at 1:45 Comment(1)
It's crazy that this isn't part of the public API. Thanks for the help!Liggett
M
6

I reworked @Ely's answer a bit:

extension UIKeyCommand {
    var nonRepeating: UIKeyCommand {
        let repeatableConstant = "repeatable"
        if self.responds(to: Selector(repeatableConstant)) {
            self.setValue(false, forKey: repeatableConstant)
        }
        return self
    }
}

Now you can have to write less code. If for example just override var keyCommands: [UIKeyCommand]? by returning a static list it can be used like this:

override var keyCommands: [UIKeyCommand]? {
    return [
        UIKeyCommand(...),
        UIKeyCommand(...),
        UIKeyCommand(...),

        UIKeyCommand(...).nonRepeating,
        UIKeyCommand(...).nonRepeating,
        UIKeyCommand(...).nonRepeating,
    ]
}

This makes the first three command repeating (like increasing font size) and the last three ones non repeating (like sending an email).

Works with Swift 4, iOS 11.

Maureen answered 11/8, 2019 at 16:0 Comment(1)
This is not working on Swift 5 and iOS 16 macOS Ventura as Designed for iPad target?Sheathbill
A
3

This works in iOS 12, a little bit less 'private' compared to the accepted answer:

let command = UIKeyCommand(...)   
let repeatableConstant = "repeatable"
if command.responds(to: Selector(repeatableConstant)) {
    command.setValue(false, forKey: repeatableConstant)
}
Antheridium answered 18/4, 2019 at 18:57 Comment(0)
K
0

By experimenting I found that the first repeated command is called in about 0,25 s, and the ones theafter about are called about every 0,05 s. This might vary from system to system.

To prevent a an action fired multiple times in a row, you could register the time the key command was last fired, and then only perform the action if it the key command was fired long enough ago.

In code:

    let keyCommand = UIKeyCommand(title: "Action", action: #selector(keyCommandActionWithRepeatingPreventer), input: "A", modifierFlags: [.command])

    var keyCommandActionLastCalled = Date()
    let repeatingThresholdInSeconds = 0.5 
 // The threshold should be larger than the maximum time between repeated calls when holding the key combination.
   
    @objc
    public func keyCommandActionWithRepeatingPreventer() {
        let now = Date()
        let deltaT = now.timeIntervalSince(keyCommandActionLastCalled)
        keyCommandActionLastCalled = now
        if (deltaT >= repeatingThresholdInSeconds) { 
           keyCommandAction()
        }
    }
    
    @objc
    public func keyCommandAction() {
        print("Key command action called")
    }
Kilar answered 24/5 at 12:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.