Pointer Interaction Tutorial for iOS: Supporting the Mouse and Trackpad
This tutorial will show you how to use the iOS pointer API for simple cases, and some more complex situations, with both UIKit and SwiftUI. By Warren Burton.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Pointer Interaction Tutorial for iOS: Supporting the Mouse and Trackpad
25 mins
- Getting Started
- Enabling UIKit Interactions
- Switching on Built-in Behaviors
- Customizing UIButton Interaction
- Applying Pointer Interactions to Other Views
- Supplying a Custom Pointer Shape
- Coordinating Animations
- Responding to Hover Events
- Enabling SwiftUI Interactions
- Adding a Simple Hover Effect
- Adding an OnHover Handler
- Where to Go From Here?
Supplying a Custom Pointer Shape
The next thing you’ll do is create a custom shape for the cursor when it’s inside the button. Remember UIPointerStyle
, which you created to give lift and highlight effects to your buttons? You can also create a style which is all about changing the shape of the cursor. To do this, you use UIPointerShape
.
UIPointerShape
is an enum. It has three predefined shapes, or it can use any UIBezierPath
:
public enum UIPointerShape { case path(UIBezierPath) case roundedRect(CGRect, radius: CGFloat = UIPointerShape.defaultCornerRadius) case verticalBeam(length: CGFloat) case horizontalBeam(length: CGFloat) public static let defaultCornerRadius: CGFloat }
You’ll use the path initializer to create a shaped cursor. Still in GameButton.swift, locate pointerInteraction(_:styleFor)
inUIPointerInteractionDelegate
.
Find the statement:
return nil
Replace it with this code:
let hand = UIBezierPath(svgPath: AppShapeStrings.hand, offset: 24)
return UIPointerStyle(shape: UIPointerShape.path(hand))
Here you create a path using an open-source utility that converts Scalable Vector Graphics (SVG) data to a UIBezierPath
. You can export SVG from many vector art applications, such as Sketch. The offset
parameter moves the path a bit to center it on the cursor.
Build and run. Go to the UIKit section of the app and capture the pointer. Now, when you enter a game button, the cursor will morph into the new shape. That’s handy! :]
The ability to create a shaped cursor is fun and cool, but it also has practical application. A custom shape can provide extra contextual information about an operation. Your customer will appreciate that!
Coordinating Animations
You’ve seen how we can animate the pointer when it enters and exits a region. In this section, you’ll find out how to perform additional coordinated animations alongside those of the pointer itself.
In GameButton.swift, go to the UIPointerInteractionDelegate
delegate extension you added earlier. Take a look at pointerInteraction(_:willExit:animator:)
and pointerInteraction(_:willEnter:animator:)
.
The last parameter, animator
, is an opaque object that conforms to UIPointerInteractionAnimating
.
This protocol has two methods: addAnimations(_:)
and addCompletion(_:)
Now, you’ll supply an animation related to the circle you drew earlier. Add this extension to GameButton.swift:
extension GameButton {
func animateOut(_ origin: CGPoint)
-> (animatedView: UIView, animation: () -> Void) {
//1
let blob = UIView(frame: CGRect(center: origin, size: blobSize))
blob.backgroundColor = UIColor.white
blob.layer.cornerRadius = blobSize / 2
self.addSubview(blob)
//2
return (blob, {
blob.frame = CGRect(center: self.bounds.center, size: self.blobSize / 10)
blob.layer.cornerRadius = self.blobSize / 20
blob.backgroundColor = self.color
})
}
}
In this method, you:
- Create a view that looks the same as the circle you draw in
draw(_:)
, then add that view to the button. - Supply a closure that states the end values of frame and color.
Now locate pointerInteraction(_:willExit:animator:)
. Find the line:
pointerInside = false
Following this line, add the code:
let animation = animateOut(pointerLocation)
animator.addAnimations(animation.animation)
animator.addCompletion { _ in
animation.animatedView.removeFromSuperview()
}
Here you add a closure to the animator. Then you strip the animated view when the animation has finished.
Build and run. Go to the UIKit section of the app and capture the pointer. Now, each time the pointer leaves a game button, the circle will animate back into the center of the button. animator
has done the work of animating the change for you!
Responding to Hover Events
Apple introduced Catalyst with iOS 13.0 and macOS 10.15. Catalyst gave you the ability to build a macOS app with the UIKit API. To provide compatibility with a screen pointer on macOS, Apple added a new gesture, UIHoverGestureRecognizer
. It wasn’t until iOS 13.4 and the rest of the pointer interaction changes arrived that this had any effect in iOS.
In the Project navigator, look in the UIKit View Controllers folder. Open Main.storyboard. Pan to the right side of the storyboard, and you’ll see a view controller with a sad cat emoji. This is the view you see when you lose the game.
Once again, look in the UIKit View Controllers folder. Now open LoseViewController.swift.
There’s not much to see in this class. There is already a UITapGestureRecognizer
to allow the view to be dismissed. Now you’ll add a hover gesture. This will display a speech bubble when the pointer is inside the central area.
Add this method to LoseViewController
:
@objc func hoverOnCentralView(_ gesture: UIHoverGestureRecognizer) {
let animationSpeed = 0.25
switch gesture.state {
//1
case .began:
centralLabel.text = happyCat
UIView.animate(withDuration: animationSpeed ) {
self.speechBubble.alpha = 1.0
self.speechBubble.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
}
//2
case .ended:
centralLabel.text = sadCat
UIView.animate(withDuration: animationSpeed ) {
self.speechBubble.alpha = 0
self.speechBubble.transform = .identity
}
default:
print("message - unhandled state for hover")
}
}
In this code, you display and dismiss the speech bubble:
- When the pointer enters the tracked view, the speech bubble becomes opaque and expands.
- When the pointer exits the tracked view, the speech bubble becomes transparent and returns to the identity transform.
Next, add these lines to configureGestures()
:
let hover = UIHoverGestureRecognizer(
target: self,
action: #selector(hoverOnCentralView(_:))
)
centralView.addGestureRecognizer(hover)
Here you add the hover gesture to UIStackView
, in the middle of the main view.
Finally, in viewDidLoad()
, find this line:
speechBubble.alpha = 1
Change it to this:
speechBubble.alpha = UIDevice.current.userInterfaceIdiom == .pad ? 0 : 1
In general, people use a pointer device only with an iPad. On an iPhone, you always want the speech bubble to remain visible.
Build and run and capture the pointer. Go to the UIKit section. Play a game! Now, when you lose, the speech bubble will react to the position of the pointer. Sad cat becomes happy cat!
UIHoverGestureRecognizer
is the only pointer interaction API recognized. macOS ignores all the other pointer interactions you add for iOS. So, for example, if you wanted to add tool tip style labels for both iOS and macOS, this is the API you would need to use.
You’ve now completed the UIKit section of the tutorial. Maybe it’s time for a break while you try to beat your high score!
Enabling SwiftUI Interactions
In this section, you’ll add similar interactions to the SwiftUI version of the user interface. Unfortunately, the hover API in SwiftUI is not yet as rich as that in UIKit. But it still provides you with a great opportunity to enhance your app.