David Cordero

Capture Circular Gestures from Siri Remote 2nd Generation

Published on 01 Sep 2021

Recently, together with the release of the new Apple TV 4K, Apple released a new generation of their beloved and at the same time hated Siri Remote controller.

One of the features that Apple announced for this remote control was the possibility of scrubbing content in AVPlayerViewController, using a circular gesture similar to the classic click wheel of the first iPods.

Sadly for developers, Apple did not present any new API to make it easier for us developers to adopt this new gesture in our Apps. In addition to that, when I explicitly asked about how to implement it in the Apple developer forums, I was told to use AVPlayerViewController as the only way to get this gesture.

This post is the result of my attempts to capture this new gesture.

Getting the info from the digitizer

As we already saw in “Directional clicks on tvOS”, there is no way to get the precise location of the finger in the digitizer of Siri Remote from an instance of UITouch. In fact, in order to avoid people creating pointer-based applications, the coordinates of any gesture in Siri Remote always start from the center of the touchpad wherever you actually start the gesture from.

Nevertheless, thanks to the GameController SDK we can have a lower level of abstraction with the controllers engine. And, lucky for us… it does allow getting the absolute directional pad values from the controllers (in our case, from Siri Remote)

In the following code snippet, you can find an example of how we can capture and log the exact location of the finger on the digitizer.

import UIKit
import GameController

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        setUpControllerObserver()
    }
    
    // MARK: - Private

    private func setUpControllerObserver() {
        NotificationCenter.default.addObserver(self, selector: #selector(controllerConnected(note:)), name: .GCControllerDidConnect, object: nil)
    }
    
    @objc
    private func controllerConnected(note: NSNotification) {
        
        // Only considering a single controller for simplicity
        guard let controller = GCController.controllers().first else { return }
        
        guard let micro = controller.microGamepad else { return }
        micro.reportsAbsoluteDpadValues = true
        micro.dpad.valueChangedHandler = {
            [weak self] (pad, x, y) in
            print("[\(x), \(y)])
        }
    }
}

From the logs of the previous code we can find out that our space of coordinates looks like the following image:

Discarding gestures

Now that we know our working environment better, the next step is detecting if the user is touching the outer ring or the internal part of the digitizer.

We can do that calculating the radius from the center of the digitizer to the user’s finger, and then discarding the gestures that are too close to the center.

Using the following image as reference, we want to filter the gestures with a radius in the red area.

This is something we can do with the following code. After testing with multiple values, I found out that a threshold of 0.5 works pretty well.

// Get the distance from the center of the digitizer to the gesture location
let radius = sqrt(x*x + y*y)

// Discard gestures out of the ring area of Siri Remote
guard radius > 0.5 else {
    return
}

Making the gesture more visual

The next step is to give some visual feedback to the users, for them to understand that the gesture is working. For this playground project, I opted for adding a hint view with a transparent background in top of an image of Siri Remote.

If you want to use this solution in your project, you will need to adjust this part to your visual requirements.

With some simple trigonometry, we can easily calculate the angle that we should rotate the hint view.

The following code snippet is the code doing the actual maths and the rotation.

// Rotate hintView to the appropriate radians
let cos = x / radius
let sin = y / radius
let radians = atan2(sin, cos)

hintView.transform = CGAffineTransform(rotationAngle: CGFloat(-radians))

To make it easier to understand what is happening behind the scene, here you can see another version using a colored background.

Getting the Gesture Direction (⬅️ vs ➡️)

So far, we already have a pretty good-looking result. But in practice we will also need the direction of the gesture, that is at the end the information that will allow us to change the value in a slider.

The following code shows the maths to get and to log the direction of the circular gesture.

let normalizedRadians = (radians + (2 * .pi)).truncatingRemainder(dividingBy: 2 * .pi)       
let radiansOffset = normalizedRadians - self.currentRadians
let normalizedRadiansOffset = (radiansOffset + (2 * .pi)).truncatingRemainder(dividingBy: 2 * .pi)

print(normalizedRadiansOffset > .pi ? "➡️" : "⬅️")

Show me the code

For better understanding, please find here a project showing a working implementation of the method described in this post.