├── DeepPressGestureRecognizer.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata ├── xcuserdata │ └── simon_non_admin.xcuserdatad │ │ └── xcschemes │ │ ├── xcschememanagement.plist │ │ └── DeepPressGestureRecognizer.xcscheme └── project.pbxproj ├── DeepPressGestureRecognizer ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Info.plist ├── Base.lproj │ ├── Main.storyboard │ └── LaunchScreen.storyboard ├── AppDelegate.swift ├── ViewController.swift └── deepPressGestureRecognizer │ └── DeepPressGestureRecognizer.swift └── README.md /DeepPressGestureRecognizer.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DeepPressGestureRecognizer.xcodeproj/xcuserdata/simon_non_admin.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | DeepPressGestureRecognizer.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 05D2772D1BBFABA9000AA18F 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /DeepPressGestureRecognizer/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /DeepPressGestureRecognizer/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /DeepPressGestureRecognizer/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /DeepPressGestureRecognizer/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /DeepPressGestureRecognizer/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // DeepPressGestureRecognizer 4 | // 5 | // Created by SIMON_NON_ADMIN on 03/10/2015. 6 | // Copyright © 2015 Simon Gladman. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(application: UIApplication) { 33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /DeepPressGestureRecognizer/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // DeepPressGestureRecognizer 4 | // 5 | // Created by SIMON_NON_ADMIN on 03/10/2015. 6 | // Copyright © 2015 Simon Gladman. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | let stackView = UIStackView() 14 | 15 | let button = UIButton(type: UIButtonType.System) 16 | let deepPressableButton = DeepPressableButton(type: UIButtonType.System) 17 | let slider = DeepPressableSlider() 18 | let stepper = UIStepper() 19 | 20 | override func viewDidLoad() 21 | { 22 | super.viewDidLoad() 23 | 24 | view.addSubview(stackView) 25 | 26 | // ---- 27 | 28 | button.setTitle("Button with Gesture Recognizer", forState: UIControlState.Normal) 29 | 30 | stackView.addArrangedSubview(button) 31 | 32 | let deepPressGestureRecognizer = DeepPressGestureRecognizer(target: self, action: "deepPressHandler:", threshold: 0.75) 33 | 34 | button.addGestureRecognizer(deepPressGestureRecognizer) 35 | 36 | // ---- 37 | 38 | deepPressableButton.setTitle("DeepPressableButton", forState: UIControlState.Normal) 39 | 40 | stackView.addArrangedSubview(deepPressableButton) 41 | 42 | deepPressableButton.setDeepPressAction(self, action: "deepPressHandler:") 43 | 44 | stackView.addArrangedSubview(button) 45 | 46 | // ---- 47 | 48 | slider.setDeepPressAction(self, action: "deepPressHandler:") 49 | 50 | slider.addTarget(self, action: "sliderChange", forControlEvents: UIControlEvents.ValueChanged) 51 | 52 | stackView.addArrangedSubview(slider) 53 | 54 | // ---- 55 | 56 | let deepPressGestureRecognizer_2 = DeepPressGestureRecognizer(target: self, action: "deepPressHandler:", threshold: 0.75) 57 | 58 | stepper.addGestureRecognizer(deepPressGestureRecognizer_2) 59 | stepper.addTarget(self, action: "stepperChange", forControlEvents: UIControlEvents.ValueChanged) 60 | 61 | stackView.addArrangedSubview(stepper) 62 | 63 | } 64 | 65 | func deepPressHandler(value: DeepPressGestureRecognizer) 66 | { 67 | if value.state == UIGestureRecognizerState.Began 68 | { 69 | print("deep press begin: ", value.view?.description) 70 | } 71 | else if value.state == UIGestureRecognizerState.Ended 72 | { 73 | print("deep press ends.") 74 | } 75 | } 76 | 77 | func stepperChange() 78 | { 79 | print("stepper change", stepper.value) 80 | } 81 | 82 | func sliderChange() 83 | { 84 | print("slider change", slider.value) 85 | } 86 | 87 | override func viewDidLayoutSubviews() 88 | { 89 | stackView.axis = UILayoutConstraintAxis.Vertical 90 | stackView.distribution = UIStackViewDistribution.EqualSpacing 91 | stackView.alignment = UIStackViewAlignment.Center 92 | 93 | stackView.frame = CGRect(x: 0, 94 | y: topLayoutGuide.length, 95 | width: view.frame.width, 96 | height: view.frame.height - topLayoutGuide.length).insetBy(dx: 50, dy: 100) 97 | } 98 | } 99 | 100 | class DeepPressableButton: UIButton, DeepPressable 101 | { 102 | 103 | } 104 | 105 | class DeepPressableSlider: UISlider, DeepPressable 106 | { 107 | override func intrinsicContentSize() -> CGSize 108 | { 109 | return CGSize(width: 200, height: super.intrinsicContentSize().height) 110 | } 111 | } 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /DeepPressGestureRecognizer.xcodeproj/xcuserdata/simon_non_admin.xcuserdatad/xcschemes/DeepPressGestureRecognizer.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /DeepPressGestureRecognizer/deepPressGestureRecognizer/DeepPressGestureRecognizer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeepPressGestureRecognizer.swift 3 | // DeepPressGestureRecognizer 4 | // 5 | // Created by SIMON_NON_ADMIN on 03/10/2015. 6 | // Copyright © 2015 Simon Gladman. All rights reserved. 7 | // 8 | // Thanks to Alaric Cole - bridging header replaced by proper import :) 9 | 10 | import AudioToolbox 11 | import UIKit.UIGestureRecognizerSubclass 12 | 13 | // MARK: GestureRecognizer 14 | 15 | class DeepPressGestureRecognizer: UIGestureRecognizer 16 | { 17 | var vibrateOnDeepPress = false 18 | let threshold: CGFloat 19 | 20 | private let pulse = PulseLayer() 21 | private var deepPressed: Bool = false 22 | 23 | required init(target: AnyObject?, action: Selector, threshold: CGFloat) 24 | { 25 | self.threshold = threshold 26 | 27 | super.init(target: target, action: action) 28 | } 29 | 30 | override func touchesBegan(touches: Set, withEvent event: UIEvent) 31 | { 32 | if let touch = touches.first 33 | { 34 | handleTouch(touch) 35 | } 36 | } 37 | 38 | override func touchesMoved(touches: Set, withEvent event: UIEvent) 39 | { 40 | if let touch = touches.first 41 | { 42 | handleTouch(touch) 43 | } 44 | } 45 | 46 | override func touchesEnded(touches: Set, withEvent event: UIEvent) 47 | { 48 | super.touchesEnded(touches, withEvent: event) 49 | 50 | state = deepPressed ? UIGestureRecognizerState.Ended : UIGestureRecognizerState.Failed 51 | 52 | deepPressed = false 53 | } 54 | 55 | private func handleTouch(touch: UITouch) 56 | { 57 | guard let view = view where touch.force != 0 && touch.maximumPossibleForce != 0 else 58 | { 59 | return 60 | } 61 | 62 | if !deepPressed && (touch.force / touch.maximumPossibleForce) >= threshold 63 | { 64 | view.layer.addSublayer(pulse) 65 | pulse.pulse(CGRect(origin: CGPointZero, size: view.frame.size)) 66 | 67 | state = UIGestureRecognizerState.Began 68 | 69 | if vibrateOnDeepPress 70 | { 71 | AudioServicesPlayAlertSound(kSystemSoundID_Vibrate) 72 | } 73 | 74 | deepPressed = true 75 | } 76 | else if deepPressed && (touch.force / touch.maximumPossibleForce) < threshold 77 | { 78 | state = UIGestureRecognizerState.Ended 79 | 80 | deepPressed = false 81 | } 82 | } 83 | } 84 | 85 | // MARK: DeepPressable protocol extension 86 | 87 | protocol DeepPressable 88 | { 89 | var gestureRecognizers: [UIGestureRecognizer]? {get set} 90 | 91 | func addGestureRecognizer(gestureRecognizer: UIGestureRecognizer) 92 | func removeGestureRecognizer(gestureRecognizer: UIGestureRecognizer) 93 | 94 | func setDeepPressAction(target: AnyObject, action: Selector) 95 | func removeDeepPressAction() 96 | } 97 | 98 | extension DeepPressable 99 | { 100 | func setDeepPressAction(target: AnyObject, action: Selector) 101 | { 102 | let deepPressGestureRecognizer = DeepPressGestureRecognizer(target: target, action: action, threshold: 0.75) 103 | 104 | self.addGestureRecognizer(deepPressGestureRecognizer) 105 | } 106 | 107 | func removeDeepPressAction() 108 | { 109 | guard let gestureRecognizers = gestureRecognizers else 110 | { 111 | return 112 | } 113 | 114 | for recogniser in gestureRecognizers where recogniser is DeepPressGestureRecognizer 115 | { 116 | removeGestureRecognizer(recogniser) 117 | } 118 | } 119 | } 120 | 121 | // MARK: PulseLayer 122 | 123 | // Thanks to http://jamesonquave.com/blog/fun-with-cashapelayer/ 124 | 125 | class PulseLayer: CAShapeLayer 126 | { 127 | var pulseColor: CGColorRef = UIColor.redColor().CGColor 128 | 129 | func pulse(frame: CGRect) 130 | { 131 | strokeColor = pulseColor 132 | fillColor = nil 133 | 134 | let startPath = UIBezierPath(roundedRect: frame, cornerRadius: 5).CGPath 135 | let endPath = UIBezierPath(roundedRect: frame.insetBy(dx: -50, dy: -50), cornerRadius: 5).CGPath 136 | 137 | path = startPath 138 | lineWidth = 1 139 | 140 | let pathAnimation = CABasicAnimation(keyPath: "path") 141 | pathAnimation.toValue = endPath 142 | 143 | let opacityAnimation = CABasicAnimation(keyPath: "opacity") 144 | opacityAnimation.toValue = 0 145 | 146 | let lineWidthAnimation = CABasicAnimation(keyPath: "lineWidth") 147 | lineWidthAnimation.toValue = 10 148 | 149 | CATransaction.begin() 150 | 151 | CATransaction.setCompletionBlock 152 | { 153 | self.removeFromSuperlayer() 154 | } 155 | 156 | for animation in [pathAnimation, opacityAnimation, lineWidthAnimation] 157 | { 158 | animation.duration = 0.25 159 | animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) 160 | animation.removedOnCompletion = false 161 | animation.fillMode = kCAFillModeForwards 162 | 163 | addAnimation(animation, forKey: animation.keyPath) 164 | } 165 | 166 | CATransaction.commit() 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DeepPressGestureRecognizer 2 | UIGestureRecognizer for recognising deep press 3D Touch on iPhone 6s 3 | 4 | _Companion project to this blog post: http://flexmonkey.blogspot.com/2015/10/deeppressgesturerecognizer-3d-touch.html_ 5 | 6 | Back in March, I looked at creating custom gesture recognisers for a single touch rotation in Creating Custom Gesture Recognisers in Swift. With the introduction of 3D Touch in the new iPhone 6s, I thought it would be an interesting exercise to do the same for deep presses. 7 | 8 | My DeepPressGestureRecognizer is an extended UIGestureRecognizer that invokes an action when the press passes a given threshold. Its syntax is the same as any other gesture recogniser, such as long press, and is implemented like so: 9 | 10 | let button = UIButton(type: UIButtonType.System) 11 | 12 | button.setTitle("Button with Gesture Recognizer", forState: UIControlState.Normal) 13 | 14 | stackView.addArrangedSubview(button) 15 | 16 | let deepPressGestureRecognizer = DeepPressGestureRecognizer(target: self, 17 | action: "deepPressHandler:", 18 | threshold: 0.75) 19 | 20 | 21 | button.addGestureRecognizer(deepPressGestureRecognizer) 22 | 23 | The action has the same states as other recognisers too, so when the state is Began, the user's touch's force has passed the threshold: 24 | 25 | func deepPressHandler(value: DeepPressGestureRecognizer) 26 | { 27 | if value.state == UIGestureRecognizerState.Began 28 | { 29 | print("deep press begin") 30 | } 31 | else if value.state == UIGestureRecognizerState.Ended 32 | { 33 | print("deep press ends.") 34 | } 35 | 36 | } 37 | 38 | If that's too much code, I've also created a protocol extension which means you get the deep press recogniser simply by having your class implement DeepPressable: 39 | 40 | class DeepPressableButton: UIButton, DeepPressable 41 | { 42 | 43 | } 44 | 45 | ...and then setting the appropriate action in setDeepPressAction(): 46 | 47 | let deepPressableButton = DeepPressableButton(type: UIButtonType.System) 48 | deepPressableButton.setDeepPressAction(self, action: "deepPressHandler:") 49 | 50 | Sadly, there's no public API to Apple's Taptic Engine (however, there are workarounds as Dal Rupnik discusses here). Rather than using private APIs, my code optionally vibrates the device when a deep press has been recognised. 51 | Deep Press Gesture Recogniser Mechanics 52 | 53 | To extend UIGestureRecognizer, you'll need to add a bridging header to import UIKit/UIGestureRecognizerSubclass.h. Once you have that you're free to override touchesBegan, touchesMoved and touchesEnded. In DeepPressGestureRecognizer, the first of these two methods call handleTouch() which checks either: 54 | 55 | If a deep press hasn't been recognised but the current force is above a normalised threshold, then treat that touch event as the beginning of the deep touch gesture. 56 | If a deep press has been recognised and the touch force has dropped below the threshold, treat that touch event as the end of the gesture. 57 | 58 | The code for handleTouch() is: 59 | 60 | private func handleTouch(touch: UITouch) 61 | { 62 | guard let view = view where touch.force != 0 && touch.maximumPossibleForce != 0 else 63 | { 64 | return 65 | } 66 | 67 | if !deepPressed && (touch.force / touch.maximumPossibleForce) >= threshold 68 | { 69 | view.layer.addSublayer(pulse) 70 | pulse.pulse(CGRect(origin: CGPointZero, size: view.frame.size)) 71 | 72 | state = UIGestureRecognizerState.Began 73 | 74 | if vibrateOnDeepPress 75 | { 76 | AudioServicesPlayAlertSound(kSystemSoundID_Vibrate) 77 | } 78 | 79 | deepPressed = true 80 | } 81 | else if deepPressed && (touch.force / touch.maximumPossibleForce) < threshold 82 | { 83 | state = UIGestureRecognizerState.Ended 84 | 85 | deepPressed = false 86 | } 87 | 88 | } 89 | 90 | In touchesEnded if a deep touch hasn't been recognised (e.g. the user has lightly tapped a button or changed a slider), I set the gesture's state to Failed: 91 | 92 | override func touchesEnded(touches: Set, withEvent event: UIEvent) 93 | { 94 | super.touchesEnded(touches, withEvent: event) 95 | 96 | state = deepPressed ? 97 | UIGestureRecognizerState.Ended : 98 | UIGestureRecognizerState.Failed 99 | 100 | deepPressed = false 101 | 102 | } 103 | Visual Feedback 104 | 105 | In the absence of access to the iPhone's Taptic Engine, I decided to add a radiating pulse effect to the source component when the gesture is recognised. This is done by adding a CAShapeLayer to the component's CALayer and transitioning from a rectangle path the size of the component to a much larger one (thanks to Jameson Quave for this article that describes that beautifully). 106 | 107 | To do this, first I two CGPath instances for the beginning and end states: 108 | 109 | let startPath = UIBezierPath(roundedRect: frame, 110 | cornerRadius: 5).CGPath 111 | let endPath = UIBezierPath(roundedRect: frame.insetBy(dx: -50, dy: -50), 112 | 113 | cornerRadius: 5).CGPath 114 | 115 | Then create three basic animations to grow the path, fade it out by reducing the opacity to zero and fattening the stroke: 116 | 117 | let pathAnimation = CABasicAnimation(keyPath: "path") 118 | pathAnimation.toValue = endPath 119 | 120 | let opacityAnimation = CABasicAnimation(keyPath: "opacity") 121 | opacityAnimation.toValue = 0 122 | 123 | let lineWidthAnimation = CABasicAnimation(keyPath: "lineWidth") 124 | 125 | lineWidthAnimation.toValue = 10 126 | 127 | Inside a single CATransaction I give all three animations the same properties for duration, timing function, etc. and set them going. Once the animation is finished, I remove the pulse layer from the source component's layer: 128 | 129 | CATransaction.begin() 130 | 131 | CATransaction.setCompletionBlock 132 | { 133 | self.removeFromSuperlayer() 134 | } 135 | 136 | for animation in [pathAnimation, opacityAnimation, lineWidthAnimation] 137 | { 138 | animation.duration = 0.25 139 | animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) 140 | animation.removedOnCompletion = false 141 | animation.fillMode = kCAFillModeForwards 142 | 143 | addAnimation(animation, forKey: animation.keyPath) 144 | } 145 | 146 | 147 | CATransaction.commit() 148 | DeepPressable Protocol Extension 149 | 150 | I couldn't resist adding a protocol extension to make any class that can add gesture recognisers deep-pressable. The protocol itself has two of my own methods for setting and removing deep press actions: 151 | 152 | func setDeepPressAction(target: AnyObject, action: Selector) 153 | func removeDeepPressAction() 154 | 155 | These are given default behaviour in the extension: 156 | 157 | func setDeepPressAction(target: AnyObject, action: Selector) 158 | { 159 | let deepPressGestureRecognizer = DeepPressGestureRecognizer(target: target, action: action, threshold: 0.75) 160 | 161 | self.addGestureRecognizer(deepPressGestureRecognizer) 162 | } 163 | 164 | func removeDeepPressAction() 165 | { 166 | guard let gestureRecognizers = gestureRecognizers else 167 | { 168 | return 169 | } 170 | 171 | for recogniser in gestureRecognizers where recogniser is DeepPressGestureRecognizer 172 | { 173 | removeGestureRecognizer(recogniser) 174 | } 175 | 176 | } 177 | In Conclusion 178 | 179 | Without the access to the Taptic Engine, this may not be an ideal interaction experience, however the visual feedback may help mitigate that. However, hopefully this post illustrates how easy it is to integrate 3D Touch information into a custom gesture recogniser. You may want to use this example to create a continuous force gesture recogniser, for example in a drawing application. 180 | 181 | As always, the source code for this project is available in my GitHub repository here. Enjoy! 182 | -------------------------------------------------------------------------------- /DeepPressGestureRecognizer.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 05D277321BBFABA9000AA18F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05D277311BBFABA9000AA18F /* AppDelegate.swift */; }; 11 | 05D277341BBFABA9000AA18F /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05D277331BBFABA9000AA18F /* ViewController.swift */; }; 12 | 05D277371BBFABA9000AA18F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 05D277351BBFABA9000AA18F /* Main.storyboard */; }; 13 | 05D277391BBFABA9000AA18F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 05D277381BBFABA9000AA18F /* Assets.xcassets */; }; 14 | 05D2773C1BBFABA9000AA18F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 05D2773A1BBFABA9000AA18F /* LaunchScreen.storyboard */; }; 15 | 05D277461BBFBDCB000AA18F /* DeepPressGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05D277451BBFBDCB000AA18F /* DeepPressGestureRecognizer.swift */; settings = {ASSET_TAGS = (); }; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | 05D2772E1BBFABA9000AA18F /* DeepPressGestureRecognizer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DeepPressGestureRecognizer.app; sourceTree = BUILT_PRODUCTS_DIR; }; 20 | 05D277311BBFABA9000AA18F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 21 | 05D277331BBFABA9000AA18F /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 22 | 05D277361BBFABA9000AA18F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 23 | 05D277381BBFABA9000AA18F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 24 | 05D2773B1BBFABA9000AA18F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 25 | 05D2773D1BBFABAA000AA18F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 26 | 05D277451BBFBDCB000AA18F /* DeepPressGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DeepPressGestureRecognizer.swift; path = deepPressGestureRecognizer/DeepPressGestureRecognizer.swift; sourceTree = ""; }; 27 | /* End PBXFileReference section */ 28 | 29 | /* Begin PBXFrameworksBuildPhase section */ 30 | 05D2772B1BBFABA9000AA18F /* Frameworks */ = { 31 | isa = PBXFrameworksBuildPhase; 32 | buildActionMask = 2147483647; 33 | files = ( 34 | ); 35 | runOnlyForDeploymentPostprocessing = 0; 36 | }; 37 | /* End PBXFrameworksBuildPhase section */ 38 | 39 | /* Begin PBXGroup section */ 40 | 05D277251BBFABA9000AA18F = { 41 | isa = PBXGroup; 42 | children = ( 43 | 05D277301BBFABA9000AA18F /* DeepPressGestureRecognizer */, 44 | 05D2772F1BBFABA9000AA18F /* Products */, 45 | ); 46 | sourceTree = ""; 47 | }; 48 | 05D2772F1BBFABA9000AA18F /* Products */ = { 49 | isa = PBXGroup; 50 | children = ( 51 | 05D2772E1BBFABA9000AA18F /* DeepPressGestureRecognizer.app */, 52 | ); 53 | name = Products; 54 | sourceTree = ""; 55 | }; 56 | 05D277301BBFABA9000AA18F /* DeepPressGestureRecognizer */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | 05D277431BBFAE3A000AA18F /* deepPressGestureRecognizer */, 60 | 05D277311BBFABA9000AA18F /* AppDelegate.swift */, 61 | 05D277331BBFABA9000AA18F /* ViewController.swift */, 62 | 05D277351BBFABA9000AA18F /* Main.storyboard */, 63 | 05D277381BBFABA9000AA18F /* Assets.xcassets */, 64 | 05D2773A1BBFABA9000AA18F /* LaunchScreen.storyboard */, 65 | 05D2773D1BBFABAA000AA18F /* Info.plist */, 66 | ); 67 | path = DeepPressGestureRecognizer; 68 | sourceTree = ""; 69 | }; 70 | 05D277431BBFAE3A000AA18F /* deepPressGestureRecognizer */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | 05D277451BBFBDCB000AA18F /* DeepPressGestureRecognizer.swift */, 74 | ); 75 | name = deepPressGestureRecognizer; 76 | sourceTree = ""; 77 | }; 78 | /* End PBXGroup section */ 79 | 80 | /* Begin PBXNativeTarget section */ 81 | 05D2772D1BBFABA9000AA18F /* DeepPressGestureRecognizer */ = { 82 | isa = PBXNativeTarget; 83 | buildConfigurationList = 05D277401BBFABAA000AA18F /* Build configuration list for PBXNativeTarget "DeepPressGestureRecognizer" */; 84 | buildPhases = ( 85 | 05D2772A1BBFABA9000AA18F /* Sources */, 86 | 05D2772B1BBFABA9000AA18F /* Frameworks */, 87 | 05D2772C1BBFABA9000AA18F /* Resources */, 88 | ); 89 | buildRules = ( 90 | ); 91 | dependencies = ( 92 | ); 93 | name = DeepPressGestureRecognizer; 94 | productName = DeepPressGestureRecognizer; 95 | productReference = 05D2772E1BBFABA9000AA18F /* DeepPressGestureRecognizer.app */; 96 | productType = "com.apple.product-type.application"; 97 | }; 98 | /* End PBXNativeTarget section */ 99 | 100 | /* Begin PBXProject section */ 101 | 05D277261BBFABA9000AA18F /* Project object */ = { 102 | isa = PBXProject; 103 | attributes = { 104 | LastUpgradeCheck = 0700; 105 | ORGANIZATIONNAME = "Simon Gladman"; 106 | TargetAttributes = { 107 | 05D2772D1BBFABA9000AA18F = { 108 | CreatedOnToolsVersion = 7.0; 109 | }; 110 | }; 111 | }; 112 | buildConfigurationList = 05D277291BBFABA9000AA18F /* Build configuration list for PBXProject "DeepPressGestureRecognizer" */; 113 | compatibilityVersion = "Xcode 3.2"; 114 | developmentRegion = English; 115 | hasScannedForEncodings = 0; 116 | knownRegions = ( 117 | en, 118 | Base, 119 | ); 120 | mainGroup = 05D277251BBFABA9000AA18F; 121 | productRefGroup = 05D2772F1BBFABA9000AA18F /* Products */; 122 | projectDirPath = ""; 123 | projectRoot = ""; 124 | targets = ( 125 | 05D2772D1BBFABA9000AA18F /* DeepPressGestureRecognizer */, 126 | ); 127 | }; 128 | /* End PBXProject section */ 129 | 130 | /* Begin PBXResourcesBuildPhase section */ 131 | 05D2772C1BBFABA9000AA18F /* Resources */ = { 132 | isa = PBXResourcesBuildPhase; 133 | buildActionMask = 2147483647; 134 | files = ( 135 | 05D2773C1BBFABA9000AA18F /* LaunchScreen.storyboard in Resources */, 136 | 05D277391BBFABA9000AA18F /* Assets.xcassets in Resources */, 137 | 05D277371BBFABA9000AA18F /* Main.storyboard in Resources */, 138 | ); 139 | runOnlyForDeploymentPostprocessing = 0; 140 | }; 141 | /* End PBXResourcesBuildPhase section */ 142 | 143 | /* Begin PBXSourcesBuildPhase section */ 144 | 05D2772A1BBFABA9000AA18F /* Sources */ = { 145 | isa = PBXSourcesBuildPhase; 146 | buildActionMask = 2147483647; 147 | files = ( 148 | 05D277461BBFBDCB000AA18F /* DeepPressGestureRecognizer.swift in Sources */, 149 | 05D277341BBFABA9000AA18F /* ViewController.swift in Sources */, 150 | 05D277321BBFABA9000AA18F /* AppDelegate.swift in Sources */, 151 | ); 152 | runOnlyForDeploymentPostprocessing = 0; 153 | }; 154 | /* End PBXSourcesBuildPhase section */ 155 | 156 | /* Begin PBXVariantGroup section */ 157 | 05D277351BBFABA9000AA18F /* Main.storyboard */ = { 158 | isa = PBXVariantGroup; 159 | children = ( 160 | 05D277361BBFABA9000AA18F /* Base */, 161 | ); 162 | name = Main.storyboard; 163 | sourceTree = ""; 164 | }; 165 | 05D2773A1BBFABA9000AA18F /* LaunchScreen.storyboard */ = { 166 | isa = PBXVariantGroup; 167 | children = ( 168 | 05D2773B1BBFABA9000AA18F /* Base */, 169 | ); 170 | name = LaunchScreen.storyboard; 171 | sourceTree = ""; 172 | }; 173 | /* End PBXVariantGroup section */ 174 | 175 | /* Begin XCBuildConfiguration section */ 176 | 05D2773E1BBFABAA000AA18F /* Debug */ = { 177 | isa = XCBuildConfiguration; 178 | buildSettings = { 179 | ALWAYS_SEARCH_USER_PATHS = NO; 180 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 181 | CLANG_CXX_LIBRARY = "libc++"; 182 | CLANG_ENABLE_MODULES = YES; 183 | CLANG_ENABLE_OBJC_ARC = YES; 184 | CLANG_WARN_BOOL_CONVERSION = YES; 185 | CLANG_WARN_CONSTANT_CONVERSION = YES; 186 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 187 | CLANG_WARN_EMPTY_BODY = YES; 188 | CLANG_WARN_ENUM_CONVERSION = YES; 189 | CLANG_WARN_INT_CONVERSION = YES; 190 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 191 | CLANG_WARN_UNREACHABLE_CODE = YES; 192 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 193 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 194 | COPY_PHASE_STRIP = NO; 195 | DEBUG_INFORMATION_FORMAT = dwarf; 196 | ENABLE_STRICT_OBJC_MSGSEND = YES; 197 | ENABLE_TESTABILITY = YES; 198 | GCC_C_LANGUAGE_STANDARD = gnu99; 199 | GCC_DYNAMIC_NO_PIC = NO; 200 | GCC_NO_COMMON_BLOCKS = YES; 201 | GCC_OPTIMIZATION_LEVEL = 0; 202 | GCC_PREPROCESSOR_DEFINITIONS = ( 203 | "DEBUG=1", 204 | "$(inherited)", 205 | ); 206 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 207 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 208 | GCC_WARN_UNDECLARED_SELECTOR = YES; 209 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 210 | GCC_WARN_UNUSED_FUNCTION = YES; 211 | GCC_WARN_UNUSED_VARIABLE = YES; 212 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 213 | MTL_ENABLE_DEBUG_INFO = YES; 214 | ONLY_ACTIVE_ARCH = YES; 215 | SDKROOT = iphoneos; 216 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 217 | TARGETED_DEVICE_FAMILY = "1,2"; 218 | }; 219 | name = Debug; 220 | }; 221 | 05D2773F1BBFABAA000AA18F /* Release */ = { 222 | isa = XCBuildConfiguration; 223 | buildSettings = { 224 | ALWAYS_SEARCH_USER_PATHS = NO; 225 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 226 | CLANG_CXX_LIBRARY = "libc++"; 227 | CLANG_ENABLE_MODULES = YES; 228 | CLANG_ENABLE_OBJC_ARC = YES; 229 | CLANG_WARN_BOOL_CONVERSION = YES; 230 | CLANG_WARN_CONSTANT_CONVERSION = YES; 231 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 232 | CLANG_WARN_EMPTY_BODY = YES; 233 | CLANG_WARN_ENUM_CONVERSION = YES; 234 | CLANG_WARN_INT_CONVERSION = YES; 235 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 236 | CLANG_WARN_UNREACHABLE_CODE = YES; 237 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 238 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 239 | COPY_PHASE_STRIP = NO; 240 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 241 | ENABLE_NS_ASSERTIONS = NO; 242 | ENABLE_STRICT_OBJC_MSGSEND = YES; 243 | GCC_C_LANGUAGE_STANDARD = gnu99; 244 | GCC_NO_COMMON_BLOCKS = YES; 245 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 246 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 247 | GCC_WARN_UNDECLARED_SELECTOR = YES; 248 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 249 | GCC_WARN_UNUSED_FUNCTION = YES; 250 | GCC_WARN_UNUSED_VARIABLE = YES; 251 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 252 | MTL_ENABLE_DEBUG_INFO = NO; 253 | SDKROOT = iphoneos; 254 | TARGETED_DEVICE_FAMILY = "1,2"; 255 | VALIDATE_PRODUCT = YES; 256 | }; 257 | name = Release; 258 | }; 259 | 05D277411BBFABAA000AA18F /* Debug */ = { 260 | isa = XCBuildConfiguration; 261 | buildSettings = { 262 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 263 | INFOPLIST_FILE = DeepPressGestureRecognizer/Info.plist; 264 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 265 | PRODUCT_BUNDLE_IDENTIFIER = uk.co.flexmonkey.DeepPressGestureRecognizer; 266 | PRODUCT_NAME = "$(TARGET_NAME)"; 267 | SWIFT_OBJC_BRIDGING_HEADER = ""; 268 | }; 269 | name = Debug; 270 | }; 271 | 05D277421BBFABAA000AA18F /* Release */ = { 272 | isa = XCBuildConfiguration; 273 | buildSettings = { 274 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 275 | INFOPLIST_FILE = DeepPressGestureRecognizer/Info.plist; 276 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 277 | PRODUCT_BUNDLE_IDENTIFIER = uk.co.flexmonkey.DeepPressGestureRecognizer; 278 | PRODUCT_NAME = "$(TARGET_NAME)"; 279 | SWIFT_OBJC_BRIDGING_HEADER = ""; 280 | }; 281 | name = Release; 282 | }; 283 | /* End XCBuildConfiguration section */ 284 | 285 | /* Begin XCConfigurationList section */ 286 | 05D277291BBFABA9000AA18F /* Build configuration list for PBXProject "DeepPressGestureRecognizer" */ = { 287 | isa = XCConfigurationList; 288 | buildConfigurations = ( 289 | 05D2773E1BBFABAA000AA18F /* Debug */, 290 | 05D2773F1BBFABAA000AA18F /* Release */, 291 | ); 292 | defaultConfigurationIsVisible = 0; 293 | defaultConfigurationName = Release; 294 | }; 295 | 05D277401BBFABAA000AA18F /* Build configuration list for PBXNativeTarget "DeepPressGestureRecognizer" */ = { 296 | isa = XCConfigurationList; 297 | buildConfigurations = ( 298 | 05D277411BBFABAA000AA18F /* Debug */, 299 | 05D277421BBFABAA000AA18F /* Release */, 300 | ); 301 | defaultConfigurationIsVisible = 0; 302 | defaultConfigurationName = Release; 303 | }; 304 | /* End XCConfigurationList section */ 305 | }; 306 | rootObject = 05D277261BBFABA9000AA18F /* Project object */; 307 | } 308 | --------------------------------------------------------------------------------