Implementing mouse pointer interactions on iPad

Mar 24 2020 6:00 PM

The new iPad Pro is here! It features a brand new LiDAR sensor and there’s also a cool new Magic Keyboard with a built-in trackpad coming in May. But the best thing about this latest Apple launch for developers is the new and improved mouse and trackpad support on iPadOS 13.4, which works on every iPad model that can run the version.

The way mouse interaction works on iPad is not the same as it does on a Mac, since the cursor can morph into UI elements and work similarly to the focus engine on tvOS, so if you want to make sure your app works great with pointing devices on iPad, there’s some work you have to do. Since there are new APIs for developers to adopt mouse interactions, I decided to write up this quick, simple guide to help you get started with that.

First of all, make sure your test device is running the iPadOS 13.4 GM (called “beta 6” in the developer portal). Also make sure you have downloaded the Xcode 11.4 GM, which includes the new APIs in the iOS SDK. If you’re reading this after March 24, 2020, then the final public releases of iPadOS 13.4 and Xcode 11.4 should already be out.

UIHoverGestureRecognizer

This subclass of UIGestureRecognizer has been available since iOS 13.0, and was designed to be used on iPad apps running on the Mac via Catalyst. It triggers its .began state when the mouse cursor enters the view, and its .ended state when it exits the view.

Using UIHoverGestureRecognizer, you can create simple hover effects, such as scaling up an UI element when the mouse is over it. The first step is to add the gesture recognizer to some view, like you’d do with any other gesture recognizer:

let gesture = UIHoverGestureRecognizer(target: self, action: #selector(viewHoverChanged))
targetView.addGestureRecognizer(gesture)

Then you need to implement the method that’s called when the gesture’s state changes, and animate your view to the desired appearance:

@objc private func viewHoverChanged(_ gesture: UIHoverGestureRecognizer) {
    UIView.animate(withDuration: 0.3, delay: 0, options: [.allowUserInteraction], animations: {
        switch gesture.state {
        case .began, .changed:
            self.targetView.layer.transform = CATransform3DMakeScale(1.1, 1.1, 1)
        case .ended:
            self.targetView.layer.transform = CATransform3DIdentity
        default: break
        }
    }, completion: nil)
}

Notice how the example above uses the longer version of the animate method, which includes the option .allowUserInteraction. It's very important that you include that option when animating during a hover gesture, otherwise your animations can end up cancelling click events.

Here’s what that looks like:

Simple hover gesture

That’s it! With a simple gesture recognizer you can make your UI elements come to life when the cursor is over them on iPadOS. If you’ve already ported your iPad app to the Mac with Catalyst, make sure you’re including your UIHoverGestureRecognizer code when compiling for iOS as well, so that your users on iPad can benefit from the same hover effects you have on the Mac.

That’s a very simple way to improve mouse support on your iPad app. But as I mentioned before, the system has special treatment for some UI elements. Try it: move the pointer over icons in SpringBoard and notice how the cursor snaps to each icon and has a cool parallax effect when you move it. The same happens for bar button items.

The good news is that you can implement the same effect in your apps, using UIPointerInteraction.

UIPointerInteraction

If you want to go even further with pointer support in your app, you have to use UIPointerInteraction. It implements the UIInteraction protocol, one which I’m a huge fan of, since it can encapsulate complex interactions in a very simple API (an article specifically about UIInteraction is in the works, stay tuned).

Basic pointer interaction

We can start simple and just add a UIPointerInteraction to some view, like so:

let interaction = UIPointerInteraction(delegate: nil)
targetView.addInteraction(interaction)

Just by doing that, you get the effect of the mouse cursor transforming into the shape of the view (in this case, a rounded rectangle):

Custom pointer interaction

It's important to note that if your view's area is too large, the parallax effect won't be applied.

Advanced pointer interaction

If you’d like to customize the pointer interaction further, you have to implement the UIPointerInteractionDelegate protocol. It lets you customize the region of the view that triggers the point interaction, what your view looks like while the interaction is happening and even the shape of the cursor.

Let’s say you have a view that’s shaped like a star and it has a property, starPath which is the UIBezierPath representing the star being drawn. In your implementation of UIPointerInteractionDelegate, you can customize the shape of the cursor by implementing the following method:

func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? {
	let params = UIPreviewParameters()
	params.visiblePath = starView.starPath
	
	let preview = UITargetedPreview(view: starView, parameters: params)

	return UIPointerStyle(effect: .automatic(preview), shape: .path(starView.starPath))
}

By doing that, the cursor will transition into the star shape of the view. The same can be done for any type of shape, so that the cursor disappears neatly into your view.

Two other delegate methods that are worth mentioning are pointerInteraction:willEnterRegion:animator: and pointerInteraction:willExitRegion:animator: . They can be used to perform custom animations alongside the cursor transition animation.

If you’d like to change your view’s background color while the interaction is happening, you could implement these delegate methods like so:

func pointerInteraction(_ interaction: UIPointerInteraction, willEnter region: UIPointerRegion, animator: UIPointerInteractionAnimator) {
	animator.addAnimations {
		self.starView.backgroundColor = .systemPink
	}
}

func pointerInteraction(_ interaction: UIPointerInteraction, willExit region: UIPointerRegion, animator: UIPointerInteractionAnimator) {
	animator.addAnimations {
		self.starView.backgroundColor = .systemYellow
	}
}

By adding property changes to the animator, the system ensures the changes animate alongside the cursor transition.

This was a very quick overview on how to implement custom pointer support on iPad for your apps. To learn more, check out Apple’s official documentation.