Closure-based gesture recognizers

Jan 30, 2018 · Follow on Twitter and Mastodon swiftgestures

In my previous post, I wrote about how I don’t like delegates and target/selectors and how I prefer closures. Let’s see how to use closures in gesture recognizers to make things nicer.

In my apps, I work around using delegates and target/selectors by adding action properties to my views. However, this requires me to add these properties to every view or create sub classes, which isn’t nice.

I have experimented with extensions, but since extensions can’t store data, I use protocols that ensure that I have closure storage properties, then extend the protocols with closure-based gesture functions. This requires me to implement the protocol for each view, though.

While neither approach is perfect, I still think that they are better than using delegates and selectors. I’d very much prefer Apple to add closure-based gestures.

Today, I found this article that describes how to use associated objects to let an extension store properties. With this, we can implement UIView extensions that add closure-based gesture recognizers to our views:

public extension UIView {
    
    public func addLongPressGestureRecognizer(action: (() -> Void)?) {
        longPressAction = action
        isUserInteractionEnabled = true
        let selector = #selector(handleLongPress)
        let recognizer = UILongPressGestureRecognizer(target: self, action: selector)
        addGestureRecognizer(recognizer)
    }
}

fileprivate extension UIView {
    
    typealias Action = (() -> Void)
    
    struct Key { static var id = "longPressAction" }
    
    var longPressAction: Action? {
        get {
            return objc_getAssociatedObject(self, &Key.id) as? Action
        }
        set {
            guard let value = newValue else { return }
            let policy = objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN
            objc_setAssociatedObject(self, &Key.id, value, policy)
        }
    }
    
    @objc func handleLongPress(sender: UILongPressGestureRecognizer) {
        guard sender.state == .began else { return }
        longPressAction?()
    }
}
public extension UIView {
    
    public func addTapGestureRecognizer(action: (() -> Void)?) {
        tapAction = action
        isUserInteractionEnabled = true
        let selector = #selector(handleTap)
        let recognizer = UITapGestureRecognizer(target: self, action: selector)
        addGestureRecognizer(recognizer)
    }
}

fileprivate extension UIView {
    
    typealias Action = (() -> Void)
    
    struct Key { static var id = "tapAction" }
    
    var tapAction: Action? {
        get {
            return objc_getAssociatedObject(self, &Key.id) as? Action
        }
        set {
            guard let value = newValue else { return }
            let policy = objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN
            objc_setAssociatedObject(self, &Key.id, value, policy)
        }
    }

    @objc func handleTap(sender: UITapGestureRecognizer) {
        tapAction?()
    }
}

If this approach doesn’t turn out to use private api:s not approved by Apple, I believe that I’ve now found a perfect approach to not having to use delegates or selectors ever again.

Discussions & More

Please share any ideas, feedback or comments you may have in the Disqus section below, or by replying on Twitter or Mastodon.

If you found this text interesting, make sure to follow me on Twitter and Mastodon for more content like this, and to be notified when new content is published.

If you like & want to support my work, please consider sponsoring me on GitHub Sponsors.