├── 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 |
--------------------------------------------------------------------------------