Force Touch Recognizers


One of the more exciting changes to iOS 9 by far are the changes that Apple gave us to the UITouch class. I don't know about y'all, but before iOS 9, I never gave UITouch a second look. You can't programmatically create a touch, and the properties that existed on UITouch weren't enough to do cool things with. The only unfortunate thing about iOS 9 is that Apple didn't roll out a ForceTouchGestureRecognizer. In this post, I'm going to roll one out using 2 new properties on UITouch and learn ya' a couple of things along the way.

sfj2v.jpg

Custom Gesture Recognizers

When creating a UIGestureRecognizer subclass, there are a few of things to consider:

  • In order to actually register touch events, we absolutely need to import the UIKit.UIGestureRecognizerSubclass module. Without it, overriding the UITouch methods we all know and love will cause compiler errors.
  • Like any other gesture recognizer, you have to provide a target and selector at initialization. Thankfully, you don't need to keep track of either of these within your subclass because simply setting a value to UIGestureRecognizer's state property will trigger a call to the selector you provided at initialization.
  • When providing a selector to your gesture recognizer, your selector CANNOT be private. Making that function private WILL cause a crash at runtime. Just don't do it. You will regret it. DON'T DO IT!!!

The new UITouch

Before we create our custom gesture recognizer, we're going to break down the only two new properties we care about:

  • maximumPossibleForce - This property is a CGFloat that serves as a boundary for your touches. Currently the maximum force I've seen is 6.6667. Obviously, for our custom recognizer, we're going to have logic that prevents setting a threshold higher than that by taking advantage of this property.
  • force - This property is a self-explanatory CGFloat that can be constantly monitored to provide us with updates to the current force of a user's touch. It also caps out at UITouch's maximumPossibleForce of 6.6667. Usefully, there is a comment on UITouch's generated header that states that the force of an average touch is 1.0.

Cutting to the chase

Knowing these little nuggets of wisdom, we are now free to create our gesture recognizer. Rather than blabber on about each part of the class, I'm going to copy and paste it in all it's commented glory here:

//Without this import line, you'll get compiler errors when implementing your touch methods since they aren't part of the UIGestureRecognizer superclass
//Without this import line, you'll get compiler errors when implementing your touch methods since they aren't part of the UIGestureRecognizer superclass
import UIKit.UIGestureRecognizerSubclass

//Since 3D Touch isn't available before iOS 9, we can use the availability APIs to ensure no one uses this class for earlier versions of the OS.
@available(iOS 9.0, *)
public class ForceTouchGestureRecognizer: UIGestureRecognizer {
  //Because we don't know what the maximum force will always be for a UITouch, the force property here will be normalized to a value between 0.0 and 1.0.
  public private(set) var force: CGFloat = 0.0
  public var maximumForce: CGFloat = 4.0

  convenience init() {
    self.init(target: nil, action: nil)
  }

  //We override the initializer because UIGestureRecognizer's cancelsTouchesInView property is true by default. If you were to, say, add this recognizer to a tableView's cell, it would prevent didSelectRowAtIndexPath from getting called. Thanks for finding this bug, Jordan Hipwell!
  public override init(target: Any?, action: Selector?) {
    super.init(target: target, action: action)
    cancelsTouchesInView = false
  }

  public override func touchesBegan(_ touches: Set, with event: UIEvent) {
    super.touchesBegan(touches, with: event)
    normalizeForceAndFireEvent(.began, touches: touches)
  }

  public override func touchesMoved(_ touches: Set, with event: UIEvent) {
    super.touchesMoved(touches, with: event)
    normalizeForceAndFireEvent(.changed, touches: touches)
  }


  public override func touchesEnded(_ touches: Set, with event: UIEvent) {
    super.touchesEnded(touches, with: event)
    normalizeForceAndFireEvent(.ended, touches: touches)
  }

  public override func touchesCancelled(_ touches: Set, with event: UIEvent) {
    super.touchesCancelled(touches, with: event)
    normalizeForceAndFireEvent(.cancelled, touches: touches)
  }

  private func normalizeForceAndFireEvent(_ state: UIGestureRecognizerState, touches: Set) {
    //Putting a guard statement here to make sure we don't fire off our target's selector event if a touch doesn't exist to begin with.
    guard let firstTouch = touches.first else { return }

    //Just in case the developer set a maximumForce that is higher than the touch's maximumPossibleForce, I'm setting the maximumForce to the lower of the two values.
    maximumForce = min(firstTouch.maximumPossibleForce, maximumForce)

    //Now that I have a proper maximumForce, I'm going to use that and normalize it so the developer can use a value between 0.0 and 1.0.
    force = firstTouch.force / maximumForce

    //Our properties are now ready for inspection by the developer. By setting the UIGestureRecognizer's state property, the system will automatically send the target the selector message that this recognizer was initialized with.
    self.state = state
  }

  //This function is called automatically by UIGestureRecognizer when our state is set to .Ended. We want to use this function to reset our internal state.
  public override func reset() {
    super.reset()
    force = 0.0
  }
}

And that's it y'all! Super simple to create and even easier to understand. NOW GO FORTH AND CREATE, MY PUPPETS!

Conclusion

Hopefully, this post will help you understand that power we now have with the new improvements to UITouch I would love to see what you peeps create as well! And as always,

Happy coding, fellow nerds!