David Cordero

Working with TapGestures in Swift

Published on 02 Mar 2017

A lot of great improvements have been released as part of the last major update of Swift. Swift is growing and growing and, in my opinion, the direction that it is taking is really promising.

On the other hand, it is also true that certain Apple APIs do not have yet the most desirable interface one could wish taking advantage of all those new capabilites of Swift.

If you work in a tvOS project, there is one of them that you will have to use quite often, it is UITapGesturesRecognizer, which is a pretty common way to create custom gestures for Siri Remote.

More specifically, I am speaking about the way the allowed touch and press types are set to them.

let gesture = UITapGestureRecognizer(target: self, action: #selector(trigger))
gesture.allowedPressTypes = [NSNumber(value: UIPressType.menu.rawValue)]
gesture.allowedTouchTypes = [NSNumber(value: UITouchType.indirect.rawValue)]
view.addGestureRecognizer(gesture)

The previous code is written in Swift 3, but you can find below the evolution of it during the last years:

// Objective-C:
gesture.allowedPressTypes = @[@UIPressTypeMenu];
gesture.allowedTouchTypes = @[@UITouchTypeIndirect];

// Swift 2:
gesture.allowedPressTypes = [UIPressType.Menu.rawValue]
gesture.allowedTouchTypes = [UITouchType.Indirect.rawValue]

// Swift 3:
gesture.allowedPressTypes = [NSNumber(value: UIPressType.menu.rawValue)]
gesture.allowedTouchTypes = [NSNumber(value: UITouchType.indirect.rawValue)]

As you can see, each iteration is making this code less and less readable adding redundant verbosity inherited from the evolution of NSNumber.

Some days ago I was speaking about this issue with my colleague Mr Chris Goldsby as result of the work that we are doing to migrate Zattoo to Swift 3.

And… he gave me a great idea. In fact, it is not that hard to get an interface as clean and readable as the one desired by Mr Chris.

After all, these two properties are just arrays of NSNumbers. It should not be hard to create them, right? all we need is an extension to create a predefined set of NSNumbers for the different press and touch types.

extension NSNumber {
    static var menu: NSNumber {
        return NSNumber(pressType: .menu)
    }
    static var playPause: NSNumber {
        return NSNumber(pressType: .playPause)
    }
    static var select: NSNumber {
        return NSNumber(pressType: .select)
    }
    static var upArrow: NSNumber {
        return NSNumber(pressType: .upArrow)
    }

    static var downArrow: NSNumber {
        return NSNumber(pressType: .downArrow)
    }
    static var leftArrow: NSNumber {
        return NSNumber(pressType: .leftArrow)
    }
    static var rightArrow: NSNumber {
        return NSNumber(pressType: .rightArrow)
    }
    // MARK: - Private

    private convenience init(pressType: UIPressType) {
        self.init(integerLiteral: pressType.rawValue)
    }
}

extension NSNumber {
    static var direct: NSNumber {
        return NSNumber(touchType: .direct)
    }
    static var indirect: NSNumber {
        return NSNumber(touchType: .indirect)
    }
    // MARK: - Private

    private convenience init(touchType: UITouchType) {
        self.init(integerLiteral: touchType.rawValue)
    }
}

And that’s all… we can already define our gestures recognizers in a more readable way:

let gesture = UITapGestureRecognizer(target: self, action: .upDownGestureWasTriggered)
gesture.allowedPressTypes = [.upArrow, .downArrow]
gesture.allowedTouchTypes = [.indirect]
view.addGestureRecognizer(gesture)

{ Closure, all the things !!! }

Well… that is not actually all…

I am sorry, but I could not finish this post at this point, speaking about making UITapGestureRecognizers more readable, and at the same time presenting at the end an interface working with Selectors.

Selectors are a great interface provided by a lot of Apple APIs, and they are really powerful.

But the fact is that Selectors are coming from Objective-C, working with dynamic dispatch taking advantage of objc runtime. I am pretty sure that they will be replaced by closures in the near future.

The good part is that with just a little bit of creativeness and… wrappiness :D … we can get a more Swifty interface out Selectors.

final class TapGestureRecognizer: UITapGestureRecognizer {
    private let callback: () -> Void

    public init(allowedPressTypes: [NSNumber] = [], allowedTouchTypes: [NSNumber] = [.indirect], callback: @escaping () -> Void) {
        self.callback = callback
        super.init(target: nil, action: nil)
        self.allowedPressTypes = allowedPressTypes
        self.allowedTouchTypes = allowedTouchTypes
        addTarget(self, action: #selector(invokeCallback))
    }
    // MARK: — Private

    @objc private func invokeCallback() {
        callback()
    }
}

And that’s all (once again). We can already define our gestures recognizers in a definitely more readable and Swifty way:

let gesture = TapGestureRecognizer(allowedPressTypes: [.menu]) {
    // Menu key was pressed
}
view.addGestureRecognizer(gesture)

Conclusion

Probably (and hopefully), Apple will give soon all the love and tender to those APIs that still need it, but in the meanwhile, they are a good excuse to be creative, getting as result more readable code… Don’t be shy !!!

Feel free to follow me on github, twitter or dcordero.me if you have any further question.