Welcome back! In part 7 of my “Making Apple TV Apps” series, its time to explore all things focus-related with the Apple TV. If you need to know how the focus engine works and how to control it, this is the article for you.
The Great Communicator
Without direct touch interaction, the user needs visual feedback that shows them which on-screen item will be the recipient of their remote or controller actions. Likewise, something has to provide the logic for managing focus when the user’s action should cause the focused item to change.
These roles are the responsibility of the focus engine. However, it can’t handle this responsibility alone. It depends on user interface elements implementing the UIFocusEnvironment protocol. This protocol defines required methods and properties that the focus engine and user interface elements use to cooperatively implement the focus behaviors we see on the Apple TV.
Fortunately, Apple has baked focus engine handling into the standard UI views and controllers for us. If you’re using standard UI components and arranging them in a grid layout, the focus engine will just work™.
In addition to handling focus changes between elements that are directly above, below, or to the left or right of one another, the default implementation also selects the first element to receive focus when your app is launched. It does this by searching the view hierarchy for the view that is closest to the top-left corner of the screen. We’ll discuss ways to change this behavior later in this article.
Here’s the tvOS 9.2 definition of the UIFocusEnvironment protocol:
public protocol UIFocusEnvironment : NSObjectProtocol { public func setNeedsFocusUpdate() public func updateFocusIfNeeded() public func shouldUpdateFocusInContext(context: UIFocusUpdateContext) -> Bool public func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) weak public var preferredFocusedView: UIView? { get } }
Lets take a closer look. First, we can request focus updates by using two different methods. I think of them as asking nicely, and do it now! setNeedsFocusUpdate submits a request for a focus update in the environment. In this case, the actual update will occur when convenient for the OS based on current system demands.
updateFocusIfNeeded, on the other hand, tells the focus engine to force an update immediately. While there are totally valid situations for needing to force an immediate update, you should use this option sparingly, just as you would any other call that forces a deviation from the standard flow of operations.
Whenever the focus engine has received a request for the focus to be changed, its first step is verifying that the focus movement should happen. It does this by calling the shouldUpdateFocusInContext method. This method returns a Boolean value indicating whether or not the focus engine should allow an update to occur.
If shouldUpdateFocusInContext returns true, the focus engine will perform the focus update, then call didUpdateFocusInContext: withAnimationCoordinator. Even if you don’t need to customize the implementation of these two methods, they can be useful for debugging focus issues. We’ll see how in the next part of our series
Last but not least, preferredFocusView is a read-only property that specifies the view that should be focused if the view’s environment is focused.
Tailored Focus
If the default focus engine behavior doesn’t work for you, you can override the UIFocusEnvironment protocol members to customize it. For example, lets say you’re displaying a collection view that contains user-deletable items. You want users to select an item for deletion just as they do with apps on the home screen—that is, after a long select action on the remote, a “shake” animation is added to the focused item. The user can then press the play/pause button to confirm the deletion or the menu button to cancel it.
You add code to keep up with your new “delete mode” state and to shake the focused item. You test your changes and discover the user can change focus while your app is in delete mode. The previously focused element is still shaking, and another element now has focus to boot. Don’t let your UX coworker see this if you know what is good for you!
To fix this, override the collection view controller’s shouldUpdateFocusInContext method. Add a line or two of code to return false if your app is in delete mode, or the base class implementation’s result if its not. You’re done!
Some Assistance Please?
Helping the focus engine out doesn’t always mean having to override a UIFocusEnvironment protocol member. In many cases, the UIFocusGuide class is our friend. UIFocusGuide instances are assigned screen position and dimension values, but they don’t appear on the screen. UIFocusGuide also doesn’t implement the UIFocusEnvironment protocol, but it has a preferredFocusView property and is recognized by the focus engine. When the focus engine finds a UIFocusGuide instance in the path of a focus change, the engine will move focus to the visual element specified by the guide’s preferredFocusView property.
Consider the accompanying image. We want the user to be able to move focus back and forth between UI elements A and B. While no claims regarding the brilliance of this UI design are made, it does a fine job of showing a layout that the standard implementation can’t handle by itself. However, we can handle this issue pretty easily with the strategic placement of a couple of UIFocusGuide instances and updating their preferredFocusView properties when elements A or B receive focus.
Here’s how it works. We add one UIFocusGuide instance to the right of element A and above element B, and another instance below element A and to the left of element B, as seen in the updated illustration below.
Next, we override the elements’ parent view controller’s didUpdateFocusInContext: withAnimationCoordinator: method so we will be notified when a focus update has occurred. If either element A or B has received focus, we update both UIFocusGuide instances’ preferredFocusView properties to refer to the corresponding element. In other words, if element A receives focus, we set both guides’ preferred focus views to be element B. If element B receives focus, both guides’ preferred focus views are set to element A.
Once the guides and code are in place, focus changes for UI elements A and B will work as expected. When element A has focus and the user indicates they want focus moved to the right, UI Focus Guide 1 will be found by the focus engine. If they indicate they want the focus moved down, UI Focus Guide 2 will be found. In either case, the focus engine will inspect the found guide’s preferredFocusView property, see that it indicates UI element B, and set the focus to that element.
Here’s another example of how focus guides can provide hints to the focus engine so that it can get the job done. If the focus guides didn’t exist in this illustration, moving focus down from element A would cause focus to jump directly to element C. Likewise, moving focus up from element C would cause element A to be focused. Element B would be unreachable from either of the other two elements.
All that is required to put element B in the mix is adding the three focus guides as shown, then assigning each guide’s associated UI element as their preferred focus view, i.e., guide 1’s preferred view being element A, guide 2’s being element B, and guide 3’s being element C. The focus movements will then work as expected.
Wrapping Up
This is the longest article in the series so far. We’ve clearly waded into the deeper end of the “Making Apple TV Apps” pool at this point. I hope the examples I provided helped you become comfortable with how the focus engine works.
Although this is the end of this article, it isn’t the end of the focus engine story. Sooner or later, the focus engine isn’t going to behave as expected and you’ll need help fixing it. My next article, “Debugging Focus Issues”, will show you how to use the tools Apple has provided to get back on track.
As always, I appreciate you stopping by, and invite you to subscribe for new articles to be delivered to your inbox, hot off the digital press. Code long and prosper!
Leave a Reply