Skip to content

152percent/MarqueeGestureRecognizer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 

Repository files navigation

Advanced Gesture Recognisers

The playground demonstrates using a UIPanGestureRecognizer to create a marquee selection tool that you can use in any UIView.

To checkout the code, either download this repo or grab the Playground from here.

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.

Pan

Pan

Zoom

Zoom

Tap

Tap

Pinch

Pinch

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.

selection.png

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
		}
	} 
}

For the full code, you can download the Playground from this repo.

Summary

Subclassing a UIGestureRecognizer and adding behaviour to it has a lot of advantages.

  1. You can easily composite this into any view
  2. You can add advanced behaviour to your gesture; e.g.
    • Hold down a second finger to constrain aspect ratio
  3. The marqueeView's lifecycle is bound to the state of the gesture; i.e.
    • no need to manage it from your view controller, etc...
  4. Gesture's are state-driven by default, which makes them great to work with.

About

A UIPanGestureRecognizer to create a marquee selection tool that you can use in any UIView.

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages