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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

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! :]

handpointer with a game button

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:

  1. Create a view that looks the same as the circle you draw in draw(_:), then add that view to the button.
  2. 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.

the lost state game state view
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:

  1. When the pointer enters the tracked view, the speech bubble becomes opaque and expands.
  2. 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!

the win state screen

Note: When this app runs in macOS, 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.