Mastering the tvOS Focus Engine
The tvOS interaction paradigm presents a unique challenge to developers and designers alike. The new Apple TV pairs a trackpad-like remote with a UI lacking a traditional cursor. As a result, “focus” is the only means by which an app can provide visual feedback to its user(s) as they navigate.
You can think of the focus engine as the bridgekeeper between users and your shiny new tvOS application. Though this bridgekeeper doesn’t expect you to know the airspeed velocity of an unladen swallow, befriending the focus engine is an essential step towards building an app that feels native to this platform.
Any seasoned iOS engineer will feel at home in UIKit on tvOS — but don’t let the platforms’ notable similarities seduce you into believing they are the same. Apple has made it easy to port your iOS app to tvOS. But if you don’t consider how your application will interact with the focus engine from the outset, you’ll find yourself fighting an uphill battle as you approach the finish line.
What Does Focus Really Mean?
Users navigate a tvOS application by moving focus between items onscreen. When an item is focused, its appearance is adjusted to stand out from the appearance of other items onscreen. Focus effects are the crux of what makes tvOS communal. Focus effects provide visual feedback not only to whomever is quarterbacking the remote, but also to any onlookers who are following along. They’re what separate this native TV experience from AirPlay-ing your iPad to the big screen.
Only views can receive focus and only one view may be in focus at a time. Consider these buttons:
Button C is currently in focus. Swiping left on the remote will focus button B. Swiping right will focus button D. Swiping left or right more aggressively will focus button A or button E, respectively. It’s worth noting that even though a more aggressive left swipe will result in button A ultimately gaining focus, button B will instantaneously gain (and then lose) focus in the process.
Whether a particular view is focusable is determined by a new instance method added UIView.
Apple has audited its public frameworks and provided sensible implementations for canBecomeFocused(). Only the following classes shipped with UIKit are focusable:
- UISearchBar (although UISearchBar itself isn’t focusable, its internal text field is)
UICollectionViewCell and UITableViewCell are exceptions. Whether a cell is focusable is determined by the UICollectionView or UITableView delegate:
Though not focusable itself, UIImageView is also a special case with the addition of the adjustsImageWhenAncestorFocused property. When enabled, an UIImageView instance will display a focused effect whenever an ancestor receives focus. As the system provides no default focus effect for UICollectionView cells, this is an easy way to breathe life into image-based collection views. You will see this technique used extensively by Apple throughout the system UI and builtin applications.
What’s Currently in Focus?
You can ask any view whether it’s currently in focus.
You can also ask the screen for the currently focused view (if it exists).
Responding to Focus Updates
In tvOS, all classes that participate in the focus system conform to the UIFocusEnvironment protocol. Each focus environment corresponds to a particular branch of the view hierarchy, meaning that focus environments can be nested.
The UIFocusEnvironment API allows for a two-way dialogue between developers and the focus engine regarding how focus should be updated within a particular branch of the view hierarchy. UIView, UIViewController, and UIPresentationController conform to UIFocusEnvironment out of the box.
Overriding shouldUpdateFocusInContext(_:) provides an opportunity to vet the proposed focus update before it’s applied. UICollectionView and UITableView delegates provide NSIndexPath-based versions of this API where the provided context contains the previously and next focused index paths rather than the views themselves.
Here’s a toy example showing how to use the UICollectionView delegate method to disable focus updates within a collection view when a cell has been selected.
Overriding didUpdateFocusInContext(_:withAnimationCoordinator:) provides an opportunity to take action in response to a focus update and participate in the associated animation. Given two adjacent buttons, here’s how one could horizontally center whichever one is currently in focus.
When a focus update cycle occurs, each method is invoked on the view receiving focus, the view losing focus, and all parent focus environments of these two views.
The focus engine will automatically initiate focus updates at appropriate times like app launch or when the currently focused view is removed from the view hierarchy. Developers can also request focus updates, but any requests must be issued through the focus engine. Since only the focus engine can update focus, it’s here that the focus engine most literally takes on the role of bridgekeeper.
UIFocusEnvironment’s setNeedsFocusUpdate() and updateFocusIfNeeded() interact with the focus engine in much the same manner as setNeedsLayout() and layoutIfNeeded() interact with the layout engine. Invoking setNeedsFocusUpdate() will make a note of your request and return immediately. The focus engine will recompute focus during the next update cycle. Invoking updateFocusIfNeeded() forces a focus update immediately.
When an update cycle begins, the focus engine queries the initiating focus environment for its preferredFocusedView. If this view is non-nil and focusable, the focus engine will attempt to give that view focus by issuing the aforementioned notification events through the focus responder chain.
Let’s look at a few examples with a particularly useless AnimalsViewController that I’ve created. In each example, dogButton initially has focus.
The focus engine will only honor requests that are issued by a focus environment that currently contains focus. As a result, even though catButton’s preferredFocusedView is itself, catButton’s request to update focus is ignored.
When animalsViewController requests a focus update, however, focus will move from dogButton to catButton since the currently focused view (dogButton) is within the branch of the view hierarchy governed by the animalsViewController focus environment.
When two focus environments request a focus update simultaneously, the focus engine will defer to the parent environment.
With this focus engine primer in hand, you will be well on your way to taking full advantage of this new platform. The focus system and API is familiar and well-engineered, so it’s easy for it to become an afterthought as you bring your application to the TV. But the more you work on this platform the more clear it will become that harnessing the full power of the focus engine will mark the difference between an iOS port and a native tvOS experience.
Check out all of our open source projects over at airbnb.io and follow us on Twitter: @AirbnbEng + @AirbnbData
Originally published at nerds.airbnb.com on September 11, 2015.