Swift conciseness through operators, extensions, and unnamed arguments (Part 1)
In a toy project, I had the need to intercept all external keyboard keypresses.
Something I am always trying to keep a critical eye on when coding in swift is 'Have I made this as concise as possible without sacrificing readability in the name of cleverness/magic/etc? I want to be sure to jettison any overly verbose habits from coding in Objective-C for the last 5 years. Elegance and readability are critical in creating easily maintainable code.
In order to intercept external keyboard keypresses in iOS, one has to implement a function
func keyCommands() -> [UIKeyCommand]!
in a ViewController
From that function you return an array of valid UIKeyCommand's. When iOS receives a keypress, it checks, in order, your array of valid UIKeyCommand's, and if it finds a match, invokes a method on the class to alert you to the keypress.
If you wanted to invoke the 'keyPressed:' method on your class when Command-A was pressed, you would create a UIKeyCommand instance:
UIKeyCommand("A", UIKeyModifierFlags.Command, Selector("keyPressed:"))
And to build an entire keyboard worth of possible keypresses, you could make an Array of characters, and put them all together like this:
var keys = [UIKeyCommand]()
for digit in Array("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
{
keys.append(UIKeyCommand(input: String(digit), modifierFlags: UIKeyModifierFlags.Command, action: Selector("keyPressed:"))
keys.append(UIKeyCommand(input: String(digit), modifierFlags: UIKeyModifierFlags.Control, action: Selector("keyPressed:"))
keys.append(UIKeyCommand(input: String(digit), modifierFlags: nil, action: Selector("keyPressed:"))
}
How can we make this code more concise?
It turns out swift offers a number of things out of the box like Strings are already enumerable for their elements (drop Array())
var keys = [UIKeyCommand]()
for digit in "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
{
keys.append(UIKeyCommand(input: String(digit), modifierFlags: .Command, action: Selector("keyPressed:"))
keys.append(UIKeyCommand(input: String(digit), modifierFlags: .Control, action: Selector("keyPressed:"))
keys.append(UIKeyCommand(input: String(digit), modifierFlags: nil, action: Selector("keyPressed:"))
}
and dropping Enum prefixes (UIKeyModifierFlags in this case):
var keys = [UIKeyCommand]()
for digit in "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
{
keys.append(UIKeyCommand(input: String(digit), modifierFlags: .Command, action: Selector("keyPressed:"))
keys.append(UIKeyCommand(input: String(digit), modifierFlags: .Control, action: Selector("keyPressed:"))
keys.append(UIKeyCommand(input: String(digit), modifierFlags: nil, action: Selector("keyPressed:"))
}
and automatic String to Selector conversion (drop Selector())
var keys = [UIKeyCommand]()
for digit in "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
{
keys.append(UIKeyCommand(input: String(digit), modifierFlags: .Command, action: "keyPressed:"))
keys.append(UIKeyCommand(input: String(digit), modifierFlags: .Control, action: "keyPressed:"))
keys.append(UIKeyCommand(input: String(digit), modifierFlags: nil, action: "keyPressed:"))
}
and of course we could only convert the letter Character to a String type once:
var keys = [UIKeyCommand]()
for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
{
let sletter = String(letter)
keys.append(UIKeyCommand(input: sletter, modifierFlags: .Command, action: "keyPressed:")
keys.append(UIKeyCommand(input: sletter, modifierFlags: .Control, action: "keyPressed:")
keys.append(UIKeyCommand(input: sletter, modifierFlags: nil, action: "keyPressed:")
}
Ok . . . this is looking a little bit more concise than Objective-C, and fairly easy to read although you could argue, and I would, that passing a String to reference a function callback seems brittle. Alas it's a carry over from Objective-C interoperability.
How can we make this code tighter?
UIKeyCommand is a fairly unambiguous class. It's purpose is to hold a key w/modifiers and the action it should trigger. In this case, those parameter names ('input', 'modifierFlags', 'action:') don't seem to be adding much value to the readability of our code.
So it seems appropriate here to cut things down even further by creating a new convenience constructor for UIKeyCommand which allows using that same original constructor, but without the parameter names.
extension UIKeyCommand {
convenience init( _ input: String!, _ modifierFlags: UIKeyModifierFlags, _ action: Selector)
{
self.init(input: input, modifierFlags: modifierFlags, action: action)
}
}
The underscores (_
) are what makes this work. Swift allows you to specifiy a outwardly visible parameter name seperately from parameter name that is inwardly referencable in the function. Underscore (_
) basically means 'Allow nothing' here.
With this new nameless parameter convenience constructor, our code can be further refined to drop input: and modifierFlags: and action::
var keys = [UIKeyCommand]()
for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
{
let sletter = String(sletter)
keys.append(UIKeyCommand(sletter, .Command, "keyPressed:"))
keys.append(UIKeyCommand(sletter, .Control, "keyPressed:"))
keys.append(UIKeyCommand(sletter, nil, "keyPressed:"))
}
Because the parameters .Command, .Control, 'keyPressed:', and sletter are so recognizable for their intent, I would argue we have only gained readability and conciseness here.
A little bit more tidyness can be attained by using the built in swift operator overload += (part of Array)
var keys = [UIKeyCommand]()
for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
{
let sletter = String(sletter)
keys += [UIKeyCommand(sletter, .Command, "keyPressed:"),
UIKeyCommand(sletter, .Control, "keyPressed:"),
UIKeyCommand(sletter, nil, "keyPressed:")]
}
Another area that could be tightened up is converting of the String to an Array, looping through the Sequence of Characters, and then converting each Character to a String. It is necessary but does not seem core to the problem we're solving here. We can extract those gruntwork details into a String extension to further simplify our code.
Adding a String extension:
extension String {
func each(closure: (String) -> Void ) {
for digit in self
{
closure(String(digit))
}
}
}
allows us to further refine our code to:
var keys = [UIKeyCommand]()
"ABCDEFGHIJKLMNOPQRSTUVWXYZ".each({
keys += [UIKeyCommand($0, .Command, "keyPressed:"),
UIKeyCommand($0, .Control, "keyPressed:"),
UIKeyCommand($0, nil, "keyPressed:")]
})
In swift, $0 is a special keyword that means 'the first parameter passed to the closure' which in this case would be each letter contained in the A-Z String.
Happy Swifting!
UPDATE: I wrote a follow-up to this blog entry here which covers some awesome feedback I received and new ideas.