Using the Focus Guide to Improve your tvOS Apps

After experimenting a lot with tvOS and playing around with the Apple TV remote, I noticed how easy it was to navigate following simple paths (up, down, left, or right), but also how impossible it was to move along a diagonal path. This was the central problem: how are you supposed to focus zones of your view that are located diagonally from one another? That’s where `UIFocusGuide` comes in.

The `UIFocusGuide` class is designed to fix this problem, allowing developers to expose non-view areas as focusable; by adding a focus guide to your UI, you will be able to focus an element that couldn’t be handled automatically by the focus engine.

Let’s walk through a simple example:

focus-guide1

As you can see here, the “MORE INFO” button below the image is not directly aligned with the “BUY” button which means it is impossible for a right swipe on the remote to focus on that item and make it active. Even overriding `didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator)` will not help as you’ll notice that this method is not called when swiping right because the focus engine is not able to find a focusable view.

To solve this problem, we need to create a focus guide between the two buttons to fill the gap and use the `preferredFocusedView` property of the focus guide to tell the focus engine who should be focused next.

Focus guide set up

The first step in my view controller is to make a property for the focus guide:

private var focusGuide = UIFocusGuide()

Then in `viewDidLoad`, I add the focus layout to my view and set up its constraints:

// We create a focus guide to fill the space between the more info button 
// and the buy button since it's not obvious for the focus engine which element should be focused.
self.view.addLayoutGuide(self.focusGuide)

self.focusGuide.leftAnchor.constraintEqualToAnchor(self.buyButton.leftAnchor).active = true
self.focusGuide.topAnchor.constraintEqualToAnchor(self.moreInfoButton.topAnchor).active = true
self.focusGuide.widthAnchor.constraintEqualToAnchor(self.buyButton.widthAnchor).active = true
self.focusGuide.heightAnchor.constraintEqualToAnchor(self.moreInfoButton.heightAnchor).active = true

An interesting thing to note here is the usage of `constraintEqualToAnchor`. With tvOS and iOS 9.1, Apple finally provides a new, easy way to create auto-layout constraints that are much more convenient than the older APIs. You can define a relationship between two attributes that you can subsequently activate to create a constraint. Here is how the above code would look using the old system:

// Creating a constraint using NSLayoutConstraint
NSLayoutConstraint(item: subview,
   attribute: .Leading,
   relatedBy: .Equal,
   toItem: view,
   attribute: .LeadingMargin,
   multiplier: 1.0,
   constant: 0.0).active = true

// Creating the same constraint using constraintEqualToAnchor:
let margins = view.layoutMarginsGuide
subview.leadingAnchor.constraintEqualToAnchor(margins.leadingAnchor).active = true

Much better, right?

Anyway, now that our focus guide has its layout constraints ready, the invisible view should be placed correctly. Below, I’ve highlighted in red the focus guide, but remember that a focus guide is always an invisible frame that is just there to help the focus engine.

focus2

With our guide ready, it’s time to add the logic. In my view controller, I override the focus element method `didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator)` and implement the following:

override func didUpdateFocusInContext(context: UIFocusUpdateContext, 
withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {
        super.didUpdateFocusInContext(context, withAnimationCoordinator: coordinator)

        guard let nextFocusedView = context.nextFocusedView else { return }

        // When the focus engine focuses on the focus guide, we can programmatically tell 
        // it which element should be focused next.
        switch nextFocusedView {
        case self.moreInfoButton:
            self.focusGuide.preferredFocusedView = self.buyButton

        case self.buyButton:
            self.focusGuide.preferredFocusedView = self.moreInfoButton

        default:
            self.focusGuide.preferredFocusedView = nil
        }
    }

Here we are getting the next focused view from the focus context, and based on this, we can redirect the focus engine to the new focused view.

If you put a breakpoint in this method, you can use Xcode Quick Look on the context parameter which will show you where the focus engine is trying to get the next focusable view.

focus3

focus4

You can see that the focus engine found our focus guide! Without it, nothing would be focusable, and we couldn’t manually update the focus to the BUY button.

I created a sample project to illustrate this example that will show you the full implementation. It demonstrates that with a few lines of code you can drastically improve the navigation into your views using the new UIFocusGuide class, and make sure the users are not frustrated when trying to navigate to a specific zone of your app. It also introduces you the new NSLayoutAnchor API Apple released for tvOS and iOS 9.1.