Custom Force Touch Gesture in iOS

Profile photo of Rihards Baumanis
Rihards Baumanis
Oct 23, 2017
5 min
Categories: Development
Lego master Yoda from star wars
Lego master Yoda from star wars

Ok, then — Force touch. Starting off, I had no idea, how and when to handle this. Initially, I thought about using the default way of handling a Force Touch, but this case turned out somewhat specific therefore I resulted to going in for a custom implementation of a UIGestureRecognizer.

https://miro.medium.com/max/808/1*jlLGnTbCFyaqWDyYqmhFkw.gif

The end goal here was to have the ability to force touch a specific UICollectionViewCell and have a simultaneous animation for background dimming and the two action icons expanding from the touch point, so to achieve this a custom approach was developed.

The protocol

So, it all started off with a protocol, that works as our back and forth between the gesture actions and the main controller itself.

protocol ForceActionPresentable: class { var forceGestureRecognizer: ForceTouchGestureRecognizer? { get set } var forceTouchView: UIView { get } func snapshot(at point: CGPoint) -> UIView? func dataObject(for point: CGPoint) -> DataObject? }
  • ForceGestureRecognizer — the recognizer object;
  • ForceTouchView — UIView representation of the active area;
  • Snapshot — UIView representation of the ForceTouchView image;
  • DataObject — The related data object;

The recognizer.

This class is a subclass of UIGestureRecognizer and will be the main gesture. It is responsible for actually managing the gesture and providing action callbacks, when necessary.

First let's make a custom initializer for this class

init(threshold: CGFloat) { self.threshold = threshold super.init(target: nil, action: nil) cancelsTouchesInView = false }

A threshold variable to have a minimum force. In our code we would measure the touch force as a value from 0 to 1, so the threshold can be used in the area around 0.2, 0.3. But this is based on preference.

And of course, cancelsTouchesInView to stop other actions from happening when we're in the process of force touch handling.

Let's add a lastTouch property to work as a cache for the point to be passed on to callback.

private var lastTouch: UITouch?

Then let's set up callbacks to notify other components of the force touch action.

var startedStateCallback: ((UITouch) -> Void)? var fixedStateCallback: (() -> Void)? var forceUpdate: ((CGFloat) -> Void)? var endedStateCallback: (() -> Void)?
  • startedStateCallback — tells the handler that the gesture has started and passes the received touch as a parameter to later allow getting the touch point;
  • fixedStateCallback — tells the handler that the touch has reached a state, where animating UI needs to be fixed in place;
  • forceUpdate — tells the handler to perform animation based on the passed force value;
  • endedStateCallback — tells the handler to stop and remove all animating UI elements

To actually read the force user presses on the screen, one has to implement the touchesDidSomething() functions.

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) { super.touchesBegan(touches, with: event) changeState(.began, touches: touches, event: event) } override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) { super.touchesMoved(touches, with: event) changeState(.changed, touches: touches, event: event) } override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) { super.touchesEnded(touches, with: event) changeState(.ended, touches: touches, event: event) } override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) { super.touchesCancelled(touches, with: event) changeState(.cancelled, touches: touches, event: event) }

And after that lets have a function, which is called from touch functions, where all the work is done based on the touch inputs.

func changeState(_ state: UIGestureRecognizerState, touches: Set<UITouch>, event: UIEvent)

So, the touches functions call the changeState function with a respective handler. Before doing anything else, lets set up our internal gesture states to better allow managing of the things happening with it.

enum RecognizerState { case started case fixed case ended }

And a private variable to go with the enum.

private(set) var gestureState: RecognizerState = .ended { didSet { guard gestureState != oldValue else { return } switch gestureState { case .fixed: fixedStateCallback?() case .started: startedStateCallback?(lastTouch) cancelsTouchesInView = true case .ended: cancelsTouchesInView = false if oldValue != .fixed { endedStateCallback?() } } } }

Since this variable is basically the direct representation of the force action state, on didSet we'll include the callbacks.

Handling force touch

Once we have all that done, we can expand the changeState function. The first thing we do, is geting the touch.

guard let touch = touches.first else { return }

And then we get the force of this touch.

let force = touch.force / touch.maximumPossibleForce

Lastly, set the lastTouch property from newly received touch

lastTouch = touch

It seemed, that force touch handling only would require handling 3 states of UIGestureRecognizerState.

So, the switching of the gesture state would look something like this

switch state { case .changed: if gestureState == .fixed { // do nothing if we are in fixed state break } // do nothing if we are in ended state below threshold if gestureState == .ended && force < threshold { break } // promote to started state if above threshold if gestureState == .ended && force >= threshold { gestureState = .started } forceUpdate?(force) // lastly, if we reached force == 1, change to fixed state if gestureState == .started && force == 1 { gestureState = .fixed } case .ended, .cancelled: gestureState = .ended default: break } }

For the basic setup, that should do the trick. Additionally, haptic feedback could be added to give that touch a little more 'force' behind it.

Adding the recognizer

To add the recognizer one can do a long and dramatic initialization process in the ViewController itself. But you can also extend our protocol and create the initialization function within:

extension ForceActionPresentable where Self: UIViewController

So that all UIViewController subclasses can just comply with the protocol & call the initialization function. The subscription functionality might look something like this.

let forceTouchView = self.forceTouchView let forceRecognizer = ForceTouchGestureRecognizer(threshold: 0.3) self.forceGestureRecognizer = forceRecognizer forceTouchView.addGestureRecognizer(forceRecognizer) if let delegate = self as? UIGestureRecognizerDelegate { forceRecognizer.delegate = delegate } forceRecognizer.began = { [weak self] touch in // perform actions on recogniser began callback. }

No force?

Ok, that mostly covers force touch handling without any additional stuff, which would include doing something with UI elements based on force.

But another thing that remains is that, what if the device can't handle force touch? Well, in that case, long press handling is in order. Or at least some kind of variation from it.

There probably is a way how to just use a UILongPressGestureRecogniser and go from there, but in this solution, I have set up a UI control, where the animations are dependant on the force used, therefore I resulted to having a some sort of simulated force handler.

Starting off we created a new enum and changed the GestureRecognizer initializer to adapt to this type.

let type: RecognizerType init(threshold: CGFloat, type: RecognizerType) { self.threshold = threshold self.type = type super.init(target: nil, action: nil) cancelsTouchesInView = false } enum RecognizerType { case forceTouch case longPress }

After this, initialisation of the recognizer is changed to something like:

let canHandleForceTouch = forceTouchView.traitCollection.forceTouchCapability == .available let threshold: CGFloat = canHandleForceTouch ? CGFloat.forceTouchThreshold : CGFloat.longPressThreshold let type: RecognizerType = canHandleForceTouch ? .forceTouch : .longPress let forceRecognizer = ForceTouchGestureRecognizer(threshold: threshold, type: type)

Ok, now since we have no way of getting the force out of UITouch (because the touch event never changes based on force), we have to somehow adapt the recogniser subclass. For this we'll create two timers as properties.

  • The first one to act as a force touch threshold;
  • The second one to act as a simulated force increase/decrease handler

And a couple of new properties to compliment our new handling

var startTimer: Timer? var animateTimer: Timer? let type: RecognizerType var forceSimulator: CGFloat = 0.1

And then initialize the first timer on a newly handled case in:

switch state { case .began: if type == .longPress { startTimer = Timer.scheduledTimer(timeInterval: CGFloat, target: self, selector: #selector(setStart), userInfo: nil, repeats: false) } case .changed if type == .longPress || gestureState == .fixed { // do nothing if we are in fixed state break } ... }

This means, that if we receive a touch, we'll start the startTimer to work as the force touch threshold. Which in return after the set time will call:

func setStart() { startTimer?.invalidate() gestureState = .started animateTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(animateView), userInfo: nil, repeats: true) }

So now we've managed to get to simulating actual force press based on time spent holding down. The animateTimer every 0.01 seconds will call

func animateView() { forceUpdate?(forceSimulator) forceSimulator += 0.05 if gestureState == .started && forceSimulator >= 1 { forceUpdate?(1) gestureState = .fixed animateTimer?.invalidate() } }

So here we keep increasing our forceSimulator property by a small value while having the user pressing down on the screen and calling the forceUpdate callback until forceThreshold reaches a value of 1.

Helpers

And more or less that's it. Just remember to adapt the code to also include proper reset for all used properties, if the user ends or cancels his touch. That all should be handled when setting the gestureState property.

Sample code and demo is available on Github. Hopefully this helps someone. Thanks for reading.

https://miro.medium.com/max/300/1*hB1OofoTpqI6wm43oKjH8A.png

Receive a new Mobile development related stories first. — Hit that follow button

Twitter: @ChiliLabs

www.chililabs.io

Share with friends

Let’s build products together!

Contact us