Advanced Gesture Recognisers
Through this article I’m going to show you how to use a UIPanGestureRecognizer to create a marquee selection tool that you can use in any of your UIView’s.
Gestures
UIGestureRecogniser’s are a really powerful way of working with touch events in iOS. They allow us to handle taps, pans, pinches and more. Through a simple state-driven API we can easily use them to detect lots of types of interactions in our apps.
Marquee Selection
Recently I came across a feature I needed to build. Basically I needed to provide a marquee selection tool like you might see in Finder for selecting files and folders.
UIPanGestueRecognizer
seemed like a perfect fit for the job since it provides location data while the you move your finger across the view. So I added one to my view and proceeded to write some code for handling the marquee itself.
Typical Solution
So I started off by checking the recognizer state and recording the initial location when state == .began
I then used location(in view:)
while the gesture’s state == .changed
— finally creating a rectangle between the two points.
At this point I had a working marquee tool but I was drawing the rectangle using draw(rect:)
of the gesture.view
, which wasn’t ideal, especially because it meant I couldn’t draw on ‘top’ of the subviews.
So I decided to add a view instead based on the gesture’s state
.
.began
– add the marqueeView
to the source view.changed
– set the frame of the selection view.ended
– remove the selection view from the source view
At this point however I realised that this solution wasn’t ideal since I was now tied to this specific implementation of UIView
. Furthermore, if I wanted to provide selection to something like a UICollectionView
then I would have to copy/paste a lof of code.
Alternative Solution
The solution I came up with was to move the marqueeView
into the gesture itself. In hindsight this seemed obvious. The gesture has everything I need to provide a selection rectangle. Its state-driven, provides the location of the touch events during a pan, and even provides me with the source view.
All I had to do was listen for the various state changes and insert/remove my marqueeView
appropriately. Lets checkout an example.
import UIKit.UIGestureRecognizerSubclass
public final class MarqueeGestureRecognizer: UIPanGestureRecognizer {
public private(set) var initialLocation: CGPoint = .zero
public private(set) var selectionRect: CGRect = .zero
public var tintColor: UIColor = .yellow
public var zPosition: Int = 0
public override init(target: Any?, action: Selector?) {
super.init(target: target, action: action)
addTarget(self, action: #selector(handleGesture(gesture:)))
}
private var marqueeView: UIView? {
didSet {
marqueeView?.backgroundColor = tintColor.withAlphaComponent(0.1)
marqueeView?.layer.borderColor = tintColor.cgColor
marqueeView?.layer.borderWidth = 1
marqueeView?.layer.zPosition = CGFloat(zPosition)
}
}
@objc private func handleGesture(gesture: MarqueeGestureRecognizer) {
guard let view = gesture.view else { return }
let currentLocation = gesture.location(in: view)
selectionRect = CGRectReversible.rect(from: initialLocation, to: currentLocation)
switch gesture.state {
case .began:
let marqueeView = UIView()
view.insertSubview(marqueeView, at: zPosition)
self.marqueeView = marqueeView
initialLocation = currentLocation
case .changed:
marqueeView?.frame = selectionRect
default:
initialLocation = .zero
selectionRect = .zero
marqueeView?.removeFromSuperview()
marqueeView = nil
}
}
}
Summary
Subclassing a UIGestureRecognizer and adding behaviour to it has a lot of advantages:
- You can easily composite this into any view
- You can add advanced behaviour to your gesture; e.g.
- Hold down a second finger to constrain aspect ratio
- The marqueeView’s lifecycle is bound to the state of the gesture; i.e.
- no need to manage it from your view controller, etc…
- Gesture’s are state-driven by default, which makes them great to work with.
Have a cool idea for another gesture recognizer? Maybe you’ve done something similar before? I’d love to hear about it.
For the full code, you can download the Playground on GitHub.