Search

10 Tips for Mastering the Focus Engine on tvOS

David House

13 min read

Mar 27, 2017

iOS

10 Tips for Mastering the Focus Engine on tvOS

Applications that run on the Apple TV present an entirely new method of interaction compared to mobile and desktop applications. Instead of directly interacting with the device, a user uses the remote to indirectly control it. In order to accomplish this, there is generally always one, but never more than one, item that is in a focused state on the screen. This focused item should have a visual indication that it is focused, and in some cases respond to small movements on the remote. A good example is the main screen that contains all the applications that you have installed. As you navigate with the remote, a single application icon becomes larger and responds to small movements on the remote by tilting the icon to create a 3D effect. The icon also responds to a click with a slight animation that mimics the icon being pressed down. In this post we will look at 10 different tips for mastering focus that you can use while building your tvOS applications.

Focus Engine

In the documentation that Apple provides, they refer to the system that manages focus on tvOS as the Focus Engine. Most of the Focus Engine is implemented inside UIKit, but there are critical properties and callbacks that you can use to interact and direct focus for the needs of your application. A focus item is something on screen that can receive focus. By default, UIKit allows UIButton, UITextField, UITableView, UICollectionView, UITextView, UISegmentedControl and UISearchBar as focusable items. A focus environment is simply an object that contains focusable items and/or other focus environments. This focus environment hierarchy follows along with the view hierarchy, so the objects that are focus environments by default are UIView, UIViewController, UIWindow and UIPresentationController.

The most important tip for working with the focus engine is to understand that it has two very separate roles that work differently. The first role the focus engine must fulfill is to determine what item should be focused when a screen transition has happened, or when a screen focus update is requested. In this case there is nothing currently focused and it has to determine what item out of all possible ones should get focus. The second role is to determine where focus should change to based on user input. This happens when the user swipes on the remote in a certain direction.

For the first case of determining focus when there is nothing currently focused, the determination is made by traversing the focus environments hierarchy looking for something to receive focus. Once it finds some focusable items, it looks at their priority to determine which item should receive focus. Tips 1 and 2 explain how to affect this selection.

When determining what item should receive focus after user input, the focus engine uses the direction and velocity of the user input to attempt to build a list of possible focusable items. This calculation is geometry based, while the case above is handled through the focus environment hierarchy. Tips 4 and 9 go into further details on this process and how to influence it.

As you start to master the focus engine, you will see the variety of mechanisms the focus engine has exposed to allow you to participate in its workings. One thing you will notice quickly is that there are several ways to accomplish many of the below tips. Use the tips below as a starting point, but use your imagination and experiment with different mechanisms to find ones that work best for your application.

Tip 1 – Set the initially focused item

As a general rule, the focus engine is pretty good about picking something appropriate from your views to receive initial focus. So for most cases you don’t need to do anything, but certain designs require you to pick something else. The focus engine provides us with a way to express our desire of what path it should take as it is navigating our hierarchy. To accomplish this, we need to set the preferredFocusEnvironments property to an array of items in priority order. When the focus engine needs to determine the initial focused item, it starts at the active UIWindow and follows the hierarchy until it finds something that can be focused. Because of this, you may need to provide preferredFocusEnvironments at each level in the view hierarchy to direct focus into a specific path. preferredFocusEnvironments doesn’t have to represent only direct children in the hierarchy, it can be any level of child view in its hierarchy. The order of the items in the preferredFocusEnvironments array is the priority that the focus engine will use when picking this initial focused item.

As an example, given the following screen:

Notice that the Top Left button has focus. The order that preferredFocusEnvironments were examined is as follows:

Examined Preferred Focus Environments
ViewController [LeftSubView, RightSubView]
LeftSubView [Top Left, Bottom Left]

Because LeftSubView contained an item that can receive focus, the focus engine stopped traversing the hierarchy and focused the Top Left button.

Tip 2 – Control which item is focused when a view has returned to the screen after a transition

A good user experience for returning to a screen after a transition is to return focus to the item that had focus previously. Thankfully by default, UIViewController has this exact behavior. This behavior is controlled through the property restoresFocusAfterTransition which has a default value of true. If the value of this property is false, the focus engine uses the same algorithm that is used when the screen is first displayed to determine where focus should go.

UITableView and UICollectionView will also try to re-focus a cell by default. According to the documentation, you can control this behavior using the property remembersLastFocusedIndexPath and the delegate callback indexPathForPreferredFocusView(in:). In practice, however, there is a problem with using them. While they are used for the original determination of focus, they are not used when the view is returned to via a transition. I think this is a bug and I have filed a radar. Luckily we can use preferredFocusEnvironments instead and it works very well. Just return the cell that should receive focus and it will be used by the system. For example to always return focus to the first cell:

override var preferredFocusEnvironments: [UIFocusEnvironment] {

    if let cell = singleTable.cellForRow(at: IndexPath(row: 0, section: 0)) {
        return [cell]
    } else {
        return []
    }
}

One final consideration when using UITableView and UICollectionView is that if you reload their contents when the view reappears, you will get an indeterminate result. It might focus the same cell location that was previously focused, or it might pick some other cell. So if you are reloading data on return then you should always use the preferredFocusEnvironments to dictate where focus should go. To return focus to the same index path, you can save the path when an item is selected, or in the prepare(for:sender:), then use it when returning the cell in preferredFocusEnvironments.

Tip 3 – Force a focus update for the entire screen

If you are familiar with updating layout after changing AutoLayout constraints, you will find the focus engine update to be very similar. A focus environment can simply call setNeedsFocusUpdate to inform the system that the focus should be updated in the next cycle of updates. To ask the system to perform the focus update immediately, call updateFocusIfNeeded after setNeedsFocusUpdate. Any focus environment can make this request, but if the currently focused item is not in the hierarchy of that environment then this request will have no effect. To ensure the best performance, make the above calls at the lowest place you can in the hierarchy that contains the currently focused item.

Tip 4 – Intercept focus changes in children

Another way that focus can change is when user interaction happens (a swipe in a specific direction for example). When this happens, the focus engine builds a list of possible focusable items in the direction of control. It checks the canBecomeFocused property of these items to filter the list and then finally picks the one that is nearest to the original focused item. Once an item is chosen, the focus engine then tries to determine if the focus change is allowed. This is done by calling shouldUpdateFocus(in:) to see if any of the affected items returns false. The order of these calls is interesting. First, the item that is losing focus has its shouldUpdateFocus(in:) called. Next the system traverses up the hierarchy, calling shouldUpdateFocus(in:) on each parent until it reaches the root. Finally it calls shouldUpdateFocus(in:) on the item that is receiving focus. The system will then traverse up the hierarchy for the receiving focus item, but will only call shouldUpdateFocus(in:) on the hierarchy items that it didn’t call while traversing for the item losing focus.

To visualize the order of these calls, given the following view and the user has swiped in the down direction:

The order of shouldUpdateFocus(in:) will be:

  • Top Left
  • LeftSubView
  • ViewController
  • Bottom Left

But if the user swipes in the right direction:

The order will be:

  • Top Left
  • LeftSubView
  • ViewController
  • Top Right
  • RightSubView

Tip 5 – Allow any UIView to receive focus

Although shouldUpdateFocus(in:) could be used to prevent an item from receiving focus, the simple way is to return false as the canBecomeFocused properties value. We can also return true for the value of canBecomeFocused to allow normally non focusable items to become focused. For example, to make a UILabel focusable, you could create a subclass of UILabel as follows:

class CustomLabel: UILabel {
  override var canBecomeFocused: Bool {
    return true
  }
}

Tip 6 – Allow a UIView to receive focus with canBecomeFocused and update its visual appearance

Returning true for canBecomeFocused only allows the item to receive focus, but it doesn’t provide any visual indication that the item has focus. Providing visual feedback for when an item has focus is an important part of the user experience, so in order to accomplish this we can implement our visual changes inside the didUpdateFocus(in:with:) method. A focusable item should change its appearance both when it receives focus as well as when it loses focus. One way to achieve this is to use the context parameter and its nextFocusedView property. If this property is the same as the item we are implementing didUpdateFocus(in:with) in, then we are receiving focus. If this property is something else then we are losing focus. For example:

override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
  super.didUpdateFocus(in: context, with: coordinator)

  if context.nextFocusedView == self {
    backgroundColor = .red
  } else if context.previouslyFocusedView == self {
    backgroundColor = .clear
  }
}

didUpdateFocus(in:with:) is called on the item that has lost focus first, then up through its focus environment hierarchy, next to the item that has received focus, and finally up through its focus environment hierarchy. One interesting behavior here is that didUpdateFocus(in:with:) is only called once all the items in its focus environment hierarchy that are both losing and receiving focus have been called. This is a different behavior than shouldUpdateFocus(in:).

To visualize this difference, consider the same interactions we described in tip 4.

The order ofdidUpdateFocus(in:with:) will be:

  • Top Left
  • Bottom Left
  • LeftSubView
  • ViewController

And if the user swipes in the right direction:

The order will be:

  • Top Left
  • LeftSubView
  • Top Right
  • RightSubView
  • ViewController

Tip 7 – Make sure focus animations match the system timing

When you change the visual appearance of a view based on focus, you should use animations in conjunction with UIFocusAnimationCoordinator to ensure a great looking transition. UIKit provides us with a UIFocusAnimationCoordinator in the didUpdateFocus(in:with:) method. This object has only a single method that we can use to coordinate our animations: addCoordinatedAnimations(_:completion:).

The following example will change the background color on a UIView based on focus change (assuming you are returning true from canBecomeFocused as noted above):

override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
  super.didUpdateFocus(in: context, with: coordinator)

  if context.nextFocusedView == self {
      coordinator.addCoordinatedAnimations({
        UIView.animate(withDuration: UIView.inheritedAnimationDuration) {
          self.backgroundColor = .red
        }
      }, completion: nil)
  } else if context.previouslyFocusedView == self {
      coordinator.addCoordinatedAnimations({
        UIView.animate(withDuration: UIView.inheritedAnimationDuration) {
          self.backgroundColor = .clear
        }
      }, completion: nil)
  }
}

Using UIView.inheritedAnimationDuration will ensure that the inner animation is running with the same duration as the coordinated focus animation. Also note that the duration for the item receiving focus is much smaller than the item losing focus. This creates a trailing effect that helps the user visualize the focus change. If you want to accentuate this trailing focus change, you can use a multiplier of UIView.inheritedAnimationDuration. For example:

override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
  super.didUpdateFocus(in: context, with: coordinator)

  if context.nextFocusedView == self {
      coordinator.addCoordinatedAnimations({
        UIView.animate(withDuration: UIView.inheritedAnimationDuration) {
          self.backgroundColor = .red
          self.textColor = .white
        }
      }, completion: nil)
  } else if context.previouslyFocusedView == self {
      coordinator.addCoordinatedAnimations({
        UIView.animate(withDuration: UIView.inheritedAnimationDuration * 2.0) {
          self.backgroundColor = .clear
          self.textColor = .black
        }
      }, completion: nil)
  }
}

If you simply want to set some internal state, or some other task when focus has been received on an item, you can use the completion handler on the addCoordinatedAnimations(_:completion:) method. Note that this callback happens once the primary focus animation is completed, but doesn’t guarantee that any nested animation blocks (for example the trailing animation with multiplier above) have completed first. For example:

override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
  super.didUpdateFocus(in: context, with: coordinator)

  if context.nextFocusedView == self {
    coordinator.addCoordinatedAnimations(nil) {
      // Perform some task after item has received focus
    }
  }
}

Tip 8 – Create a press animation

To get a visual effect when the user has pressed the select button on the remote while the view has focus, you can monitor for press events from your view. For example:

override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {

    super.pressesBegan(presses, with: event)
    if presses.contains(where: { (press) -> Bool in
            press.type == .select
        }) {

        transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
    }
}

override func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
    super.pressesEnded(presses, with: event)
    transform = .identity
}

This example reduces the scale on the view to get a pressed in effect. Once presses are ended the scale is returned to normal. If you want to also add a gesture recognizer, then be sure to implement pressesCancelled(:with:) to return the button to normal once the gesture recognizer is detected because by default the gesture recognizer will cancel further events once recognized.

override func pressesCancelled(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
    super.pressesCancelled(presses, with: event)
    transform = .identity
}

Tip 9 – Stop focus changing for a period of time

The focus engine uses the velocity of the gesture to skip through multiple items to provide really smooth user interaction when there are many elements organized horizontally or vertically on the screen. In some rare cases, however, you might want to prevent this behavior and only allow focus to change to the next element, regardless of velocity of the gesture.

One way to accomplish this is to use the shouldUpdateFocus(in:) and didUpdateFocus(in:with:) in the parent focus environment to control the focus change. Use the shouldUpdateFocus to indicate when focus change events are starting, then didUpdateFocus to indicate they have finished. In between the first shouldUpdateFocus(in:) and the didUpdateFocus(in:with:), return false to any other shouldUpdateFocus(in:) request.

var blockingFocusChange: Bool = false

override func shouldUpdateFocus(in context: UIFocusUpdateContext) -> Bool {
  return !blockingFocusChange
}

override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
  super.didUpdateFocus(in: context, with: coordinator)
  blockFocusChange = true
  DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
    self.blockingFocusChange = false
  }
}

Use this with caution because in most cases you want the default system behavior and this can make your application feel unresponsive (because it is!).

Tip 10 – Create a rich UIButton

One common pattern on tvOS is to provide an image with text or other visuals displayed on top of it. For example a video thumbnail might have a badge on top to indicate an unwatched video. Composing these views together using UIView would be trivial, but we wouldn’t have a nice focused 3D effect that is provided by UIButton and UIImageView. Recreating the 3D motion and visual effects from UIButton and UIImageView is possible, but tricky and time consuming to match these built-in components. One approach for accomplishing both is to compose our view hierarchy using UIView, then render it into a UIImage that can be placed inside a UIButton. This sounds tricky but is actually straightforward.

For this example we are going to use a XIB file (remember those?!) so we can visually design the view. To create the XIB file, select File | New | File... from the menu and pick the View filetype under the User Interface section.

Next add your UI elements to the view. For this example we have created a view that contains a background image and 2 labels on top of the image. We have also created a subclass of UIView and set it as the class for the root view in Interface Builder.

To achieve the rendering, we created a static method on the custom UIView class that loads the view from the XIB and turns it into a UIImage.

static func loadImageFromView() -> UIImage {
  let name = String(describing: self)
  guard let view = Bundle.main.loadNibNamed(name, owner: nil, options: nil) else {
    preconditionFailure("Missing nib with name (name)")
  }

  guard let buttonView = view.first as? ButtonView else {
    preconditionFailure("Cannot downcast message view as ButtonView")
  }

  let size = buttonView.bounds.size
  UIGraphicsBeginImageContext(size)
  let ctx = UIGraphicsGetCurrentContext()!
  buttonView.layer.render(in: ctx)
  let img = UIGraphicsGetImageFromCurrentImageContext()!
  UIGraphicsEndImageContext()
  return img
}

Note that this method assumes the XIB contains the correctly sized view. To use this image, we are going to create a custom UIButton and use its setImage(_:for:) method. We also want to make sure the UIButton’s imageView is set to enable the 3D motion effects using the adjustsImageWhenAncestorFocused property. The complete UIButton looks like this:

class ComposedButton: UIButton {

  override func awakeFromNib() {
    super.awakeFromNib()

    contentMode = UIViewContentMode.scaleToFill
    contentHorizontalAlignment = .fill
    contentVerticalAlignment   = .fill
    backgroundColor = .clear
    contentEdgeInsets = UIEdgeInsets.zero

    setImage(ButtonView.loadImageFromView(), for: .normal)
    imageView?.adjustsImageWhenAncestorFocused = true
    imageView?.clipsToBounds = false
  }
}

Now we can add this ComposedButton to any of the views in our application and get a nice looking button. If you want to programmatically create a button of this class, make sure to place this initialization code in the init(frame:) instead of awakeFromNib. Now our button has a great UX when using the remote, yet we have composed details on top of our image.

Well, that rounds out our 10 tips to mastering focus in your tvOS applications. We are excited about the tvOS platform and can’t wait to see what you make for it!

Josh Justice

Reviewer Big Nerd Ranch

Josh Justice has worked as a developer since 2004 across backend, frontend, and native mobile platforms. Josh values creating maintainable systems via testing, refactoring, and evolutionary design, and mentoring others to do the same. He currently serves as the Web Platform Lead at Big Nerd Ranch.

Speak with a Nerd

Schedule a call today! Our team of Nerds are ready to help

Let's Talk

Related Posts

We are ready to discuss your needs.

Not applicable? Click here to schedule a call.

Stay in Touch WITH Big Nerd Ranch News