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 iOS delegates and target/selectors and how I prefer to use closures. Let’s see how we can use closures with 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 also experimented with using extensions, but since extensions can’t store data, I have protocols that ensure that I have closure storage properties, then extend the protocols with closure-based gesture functions. It still requires me to implement the protocol for each view, though.

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

Then today, I found this blog post that describes how you can use associated objects to let extensions 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.