Stop UIKeyCommand repeated actions - ios

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

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"];

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.

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)
}

Related

Is it possible to operate with the last of matching elements in XCTest?

I would like to tap the last [Play] button in my app, and I'm looking for something like
app.buttons["play"].lastMatch.tap()
Is there any way to do so?
I managed this problem by writing a little extension
extension XCUIElementQuery {
var lastMatch: XCUIElement { return self.element(boundBy: self.count - 1) }
}
after that, I can simply write code like this
app.buttons.matching(identifier: "play").lastMatch.tap()

Objective-C Class Swapping at runtime

At the moment we have a class structure like...
GenericFruitViewController
- AppleViewController
- PearViewController
- StrawberryViewController
Where the specific view controllers are subclasses and only change a small amount of implementation.
What I'd like to do is be able to swap out the GenericViewController at runtime. I want to change the way the generic view controller works and change some of the methods (this won't affect the subclassed overridden methods).
But I'd like to be able to switch this on/off (A/B testing).
At the moment we have a factory method that does something like...
- (GenericFruitViewController *)fruitControllerWithType:(FruitType)type
{
if (type == Apple) {
return [AppleViewController new];
}
return [GenericFruitViewController new];
}
What I'd like to do (ideally) is something like...
- (GenericFruitViewController *)fruitControllerWithType:(FruitType)type
{
// this is the new bit!
if (switchOnTheTesting) {
// swap GenericFruitViewController for my NewFruitViewController
}
// new bit ends
// existing code not changed
if (type == Apple) {
// this now returns a subclass of NewFruitVC if switched
return [AppleViewController new];
}
// this now returns NewFruitVC if switched
return [GenericFruitViewController new];
}
And by doing this it will then use my new VC whenever it refers to the GenericFruitVC.
Is that even possible?

Overriding CALayer's initWithLayer in Swift

I'm learning how to use CALayers and perform animations on their properties. To a beginner Apple's documentation is simply cryptic. I managed to find an example (called: CustomAnimatableProperty) in iOS's documentation which somewhat 'explains' how to do what I want:
// For CALayer subclasses, always support initWithLayer: by copying over custom properties.
-(id)initWithLayer:(id)layer {
if( ( self = [super initWithLayer:layer] ) ) {
if ([layer isKindOfClass:[BulbLayer class]]) {
self.brightness = ((BulbLayer*)layer).brightness;
}
}
return self;
}
Translating the method override to Swift however gives me a few errors:
The errors stem from my lacking understanding of what's going on here. I'm not sure what are we checking for in those nested if statements. Also I am a bit baffled by the usage of "=" in the main if(){} block. Shouldn't we be checking ("==") for equality?
But yeah any general help would mean the world. I've tried reviewing a few blog-posts / tutorials online, however non of them deals with this speciffic issue.
The self = [super init...] idiom is for Objective-C, not Swift. In Swift, init blocks aren't normal functions and don't return anything.
While we're at it, let's use the Swift idiom for downcasting. We also need to guarantee that size is initialized before we call super.init.
override init(layer: AnyObject!) {
if let layer = layer as? SegmentActiveLayer {
size = layer.size
} else {
size = 0
}
super.init(layer: layer)
}

Detect delete key using UIKeyCommand

Anyone know how to detect the "delete" key using UIKeyCommand on iOS 7?
As people were having problems with Swift, I figured a small, complete example in both Objective C and Swift might be a good answer.
Note that Swift doesn't have a \b escape character for backspace, so you need to use a simple Unicode scalar value escape sequence of \u{8}. This maps to the same old-school ASCII control character number 8 ("control-H", or ^H in caret notation) for backspace as \b does in Objective C.
Here's an Objective C view controller implementation that catches backspaces:
#import "ViewController.h"
#implementation ViewController
// The View Controller must be able to become a first responder to register
// key presses.
- (BOOL)canBecomeFirstResponder {
return YES;
}
- (NSArray *)keyCommands {
return #[
[UIKeyCommand keyCommandWithInput:#"\b" modifierFlags:0 action:#selector(backspacePressed)]
];
}
- (void)backspacePressed {
NSLog(#"Backspace key was pressed");
}
#end
And here's the equivalent view controller in Swift:
import UIKit
class ViewController: UIViewController {
override var canBecomeFirstResponder: Bool {
return true;
}
override var keyCommands: [UIKeyCommand]? {
return [
UIKeyCommand(input: "\u{8}", modifierFlags: [], action: #selector(backspacePressed))
]
}
#objc func backspacePressed() {
NSLog("Backspace key was pressed")
}
}
Simple really - need to look for the backspace character "\b"
You can always try UIKeyInput. https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UIKeyInput_Protocol/index.html#//apple_ref/occ/intfm/UIKeyInput/deleteBackward
The function should be
- (void)deleteBackward

Multiple UIActivityViewController placeholder items?

UIActivityItemSources, it seems, can only return one kind of placeholder item? This seems strange, because I have a UIActivityItemSource that could return a string, an NSData object, or an image depending upon the activity it's given.
Is there really no way to return more than one kind of placeholder? (NSArrays don't seem to work.)
(I could imagine a solution where I instantiate a bunch of UIActivityItemProvider instances, each supporting the different datatypes mentioned above. But that seems like a lot more work than should be necessary...?)
If you add a trace inside your itemForActivityType function you will see that this function will be called multiple times. One for each activity available for share.
For example - if I want to provide different text for Twitter and mail/sms sharing I would have something like this:
- (id) activityViewController: (UIActivityViewController*) activityViewController itemForActivityType: (NSString*) activityType {
if (activityType == UIActivityTypePostToTwitter) {
return #"Sharing by Twitter";
}
else
return #"Other kind of sharing";
}
UPDATE:
If you want to provide different types of data to share (say text and images) - you need to wrote your placeholder function in a way so it returns two different kind of object when called multiple times.
- (id) activityViewControllerPlaceholderItem: (UIActivityViewController*) activityViewController {
static int step = 0;
if (step == 0) {
step = 1;
return #"text";
}
else if (step == 1) {
step = 2;
return [UIImage imageNamed: #"image"];
}
}

Resources