Debugging Focus in tvOS

Allow me to begin with quick run-through of tvOS's Focus Engine and Focus Guides:

Focus Engine: The Basics

With the advent of tvOS, Apple moved from an in-the-hands touchscreen to a TV 10 feet away. Now that you could no longer touch the environment with your finger, Apple needed a new paradigm for user interaction.

Enter the Focus Engine.

We can think of Focus as a spotlight, directing the user's attention toward a particular element on screen. Views can be highlighted, or Focused, and the user can move Focus around different elements of the app using the remote control. It's like a mouse cursor but with only a discrete number of options.

But how does the Focus Engine work underneath the hood?

When a user swipes on the remote control's touch surface, the Focus Engine looks in the direction of the swipe for the next view to which to move Focus. If it "sees" one, it moves Focus there, if it doesn't see one, it doesn't move Focus. Simple as that.

As an example, see the simple 3-button app below. When the Top Left button is in Focus and the user swipes right, the Focus Engine will move Focus to the Top Right Button, as expected.

Focus Moves As Expected

At this point, a swipe down would do nothing. The Focus Engine looks below the Top Right Button and sees no views, so it does not move Focus. The only way to move to the Bottom Left Button would be to go back and down through the Top Left Button.

Focus Will Not Move

However, most users would likely expect a swipe down to move Focus to the Bottom Left Button.

This is where Focus Guides come in.

Focus Guides

As in our example, there are scenarios where a user might expect the Focus Engine to behave beyond its default behavior. We can help the Focus Engine out by making use of Focus Guides.

A Focus Guide is an invisble layout guide that helps the Focus Engine know where to move Focus.

In our example, we can place a Focus Guide in the bottom right position:

Now, when the Focus Engine looks down, it will find the Focus Guide...

...and the Focus Guide will direct Focus to the Bottom Left Button

The Code

First, we add the Focus Guide to the view.

    // Create the Focus Guide and add it to the view
    var focusGuide = UIFocusGuide()
    view.addLayoutGuide(focusGuide)

    // Anchor the Focus Guide     
    focusGuide.widthAnchor.constraintEqualToAnchor(topRightButton.widthAnchor).active = true
    focusGuide.heightAnchor.constraintEqualToAnchor(bottomLeftButton.heightAnchor).active = true
    focusGuide.topAnchor.constraintEqualToAnchor(bottomLeftButton.topAnchor).active = true
    focusGuide.leftAnchor.constraintEqualToAnchor(topRightButton.leftAnchor).active = true

Then, to make the Focus Guide direct Focus to the Bottom Left Button, we set the Focus Guide's preferredFocusView property; this is the view to which the Focus Guide will direct Focus.

    // Set preferred focus
    focusGuide.preferredFocusedView = bottomLeftButton

So now, with the help of our strategcally-placed Focus Guide, a swipe down will move Focus down to the Bottom Left Button, as the user would expect.

That's a basic rundown on how the Focus Engine and Focus Guides work.

Bugs

But what if they're not working? How do you debug a Focus Guide issue?

Apple provides 2 solutions:
1. Quick Look
2. _whyIsThisViewNotFocusable

Quick Look

Quick Look is a visual tool to debug Focus Engine and Focus Guide problems.

To activate it, you have to catch a break in the method didUpdateFocusInContext(context, withAnimationCoordinator coordinator); this gets called every time Focus is moved. Then, in the Varibles View in the bottom-left section of Xcode, highlight the context variable, and click the eye icon at the bottom (or press the spacebar).

To Open Quick Look

This brings up Quick Look, which looks like:
Quick Look

Quick Look shows our 3 buttons, with the button currently in Focus higlighted in red. The direction in which the Focus Engine is "looking" is shown in light red, and a light red border is drawn around the view to which Focus is about to move (which just so happens to be the Focus Guide). Focus Guides are highlighted in blue.

You can open the image in Preview and save it, email it to other developers, etc.

Quick Look definitely provides a useful debugging tool, but it has its flaws.

The biggest flaw is that in order to activate Quick Look, you have to catch a break in didUpdateFocusInContext(), but of course if the very issue you're trying to debug makes it so Focus never moves, didUpdateFocusInContext() never gets called and the breakpoint will never be hit.

That, Ladies and Gents, is what we call a Catch-22.

whyIsThisViewNotFocusable

Apple also offers the (very descriptively named) LLDB command called _whyIsThisViewNotFocusable as another Focus debug tool.

To activate this command, you need to catch a break anywhere in your code (or even just pause it), and run the command _whyIsThisViewNotFocusable in the LLDB command line debugger, like so:

    [yourView _whyIsThisViewNotFocusable]

As you may have noticed, the command is in Objective-C, so it won't work if your project is written in Swift. The Swift command is:

    po yourView.performSelector(Selector("_whyIsThisViewNotFocusable"))

This will print out a description of some potential reasons for why Focus won't move to your view. See example below:

In this particular example, the simple solution would be to just go the storyboard and enable user interaction on the button.

But _whyIsThisViewNotFocusable has its share of flaws as well, some of which include:

  • Not visual
  • Only works for views not focusable for very specific reasons such as:

    • View is hidden
    • userInteraction is disabled
    • View is obscured by another View

    For example, if none of the above reasons hold true, _whyIsThisViewNotFocusable returns a message that's not particularly helpful:

Visual Focus Guides

To help fill some of the gaps left by Apple's Focus Guide debugging solutions, I created a 3rd tool1 called Visual Focus Guides. They're visual (as the name suggests), and you can activate them with a method within your code, or you can activate them anywhere via the LLDB debugger.

They make your Focus Guides visible (almost) in real-time, and they're color-coded to match each Focus Guide to its preferredFocusedView. I've found that debugging a problem with Focus Guides is much easier when you can see their location with your own two eyes.

The tool is packaged as an external library, and once installed, can be activated via the following methods:

    // Show focus guides
    yourView.showAllVisualFocusGuides()

    // Hide focus guides
    yourView.hideAllVisualFocusGuides()

When activated, a Visual Focus Guide (shown below in purple) shows the location of a Focus Guide with a solid color2. It also displays a thin border in the same color around the Focus Guide's corresponding preferredFocusedView.

If there are multiple Focus Guides that share a single preferredFocusedView, they will be shown in the same color2, as shown here in teal:

LLDB

In order to activate Visual Focus Guides from within LLDB, simply use the following commands:

    (lldb) po yourView.showAllVisualFocusGuides()
    (lldb) e CATransaction.flush()                 //Re-draws view

To deactivate them:

    (lldb) po yourView.hideAllVisualFocusGuides()
    (lldb) e CATransaction.flush()                 //Re-draws view     

Installation

The Visual Focus Guide library is available through CocoaPods.

Simply add the following lines to your Podfile and run a pod install command from Terminal:

    source 'https://github.com/CocoaPods/Specs.git'
    platform :tvos, '8.0'
    use_frameworks! 

    pod 'VisualFocusGuides', '~> 0.1'

Hopefully this helps you make some cool apps.
Happy coding!


  1. Visual Focus Guides are not intended to replace Quick Look or the _whyIsThisViewNotFocusable command, but rather to work complementary to them.

  2. Note: The colors are randomly generated, so they will be different every time the Visual Focus Guides are activated.

David Engelhardt

iOS Developer. Turns coffee into apps. Does not actually have a mustache.

New York City