I recently had the privilege of attending an excellent Apple TV Tech Talk in London (read my Tech Talk Titbits for all the details from the day).
One of the most helpful sessions at the event explored the Focus Engine and in this post we’ll cover all you need to know.
The Focus Engine sits right at the heart of interactions on tvOS. It’s responsible for moving the focus between elements. Unlike iOS, there is no touch input or mouse cursor. Instead, Apple have devised this Focus Engine which essentially moves the focus between elements automagically.
In short; the Focus Engine is responsible for enabling the user to move around your app’s UI.
By default, the Focus Engine will allow the user to jump from one focusable element to another if that other element is vertically or horizontally a neighbour.
Everything happens pretty much automatically for us, but there are a few cases where you might need a bit of extra control. This section will cover a few areas that were talked about at the Apple TV Tech Talk in London.
To convey focused elements Apple have this cool parallax effect, they also apply a drop shadow and enlarge the element slightly. This all happens for free if you're using UIButton
with images.
If you’re building a custom control you’ll need to use a UIImageView
to get this effect. You’ll need to opt-in for this effect by setting the image view’s adjustsImageWhenAnsestorIsFoused
property to True. Nice!
This approach is needed if using a UICollectionView
and you want these effects.
It’s worth noting that this effect only works on image views so if you’ve got a custom control made up of 2 or more sub views you’ll need to flatten the view hierarchy into an image to get this effect.
If you don’t use the system parallax effects you can use UIMotionEffects
class to perform your own custom animations, though Apple recommend that you don’t mimic the system effect and come up with something distinctly different.
If you need the parallax effect, then you’re better on using the system effect.
When the user changes focus, the Focus Engine performs a subtle animation. In the Apple bundled apps there are examples where when the focus changes, then a label appears below the focused element.
The label fades in and out as the focus comes and goes. To achieve this effect you’ll need to implement the methods of the UIFocusEnvironment
protocol. This protocol is implemented on UIView
, UIViewController
, UIWindow
and UIPresentationController
.
If you’ve ever done any animations along-side view controller transitions in iOS, you’ll feel right at home as the process is practically identical.
The UIFocusEnvironment protocol exposes the delegate method didUpdateFocusInContext(_:withAnimationCoordinator:)
which provides a UIFocusUpdateContext
and UIFocusAnimationCoordinator
objects.
The latter allows you to animate along-side the main focus animation, this is very similar to UIViewController’s UIViewControllerTransitionCoordinator
API.
You can perform your animations in a special animation block simply by calling the addCoordinatedAnimations
method and passing in a block of animation code.
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) { coordinator.addCoordinatedAnimations({ // add your animations }) }
According to Apple’s documentation you can even supply your own UIView
animation block for fine grained control of the coordinated animations.
The reason for using the UIFocusAnimationCoordinator
over just adding animation code within the main body of the didUpdateFocusInContext(_:withAnimationCoordinator:)
method is because Apple may shorten or speed up the animations depending on how fast the user is moving between focusable elements.
For example, the user may swipe quickly from the left most item to the right-most item, the focus animation would move quickly through all the elements in-between but the animations would be faster than just moving at normal speed between direct neighbouring elements.
Focus Guides allow you to supply hints to the focus engine on where the next focus should be when it’s not obvious. It’s worth noting that the focus engine determines where the focus should move to next by looking horizontally or vertically in direction that the user is moving toward when swiping on the Siri remote. 
In the example above the user is swiping down on the Siri remote but there are no views within the focus area vertically below the Current Item so the focus engine can’t move the focus.
In this case we want to move the focus to the item marked Next Item. Normally the focus engine can’t do this as it will only move focus to views directly aligned vertically or horizontally to the current item.
Fortunately, Apple have provided a way around this. UIFocusGuide
is a subclass of UILayoutGuide
and represents an invisible, focusable region that can redirect focus to other views.
If we place the focus guide in the area below the current item as shown in above example, we can then redirect the focus to the desired Next Item using the UIFocusFuide.preferredFocusedView
property.
You’ve heard of the responder chain right? Well let's introduce the focus chain. It works in a very similar way; when a new view controller is presented the focus engine tries to determine the initial view to focus on.
It’s worth noting one view is always focused.
According to Apple’s documentation “This [initial] view is usually the closest focusable view to the top-left corner of the screen.”
The above photo shows the search process for the initial view to focus.
If the initial view that the Focus Engine has determined is not to your liking you can give the Focus Engine some hints using the UIFocusEnvironment protocol’s preferred focus view property.
For more information, see the Apple documentation.
One of the really priceless titbits at the Apple TV Tech Talk was two Focus Engine debugging techniques that are available (though somewhat hidden).
The first is a private hidden method called _whyIsThisViewNotFocusable
. It’s worth noting this method is not defined in any public headers so if you want to use it you’ll have to use something like the Objective-C run-time to call it on the view in question.
lldb > myView.performSelector(@selector(_whyIsThisViewNotFocusable)) ISSUE: This view is not contained in a window view hierarchy. ISSUE: This view returns NO from -canBecomeFocused. ISSUE: One or more ancestors have issues that may be preventing this view from being focusable. Details: : ISSUE: This view is not contained in a window view hierarchy.
The above lldb example will cause the debugger to print out useful information on why the view can’t be focused. Make sure you don’t include calls to this method in shipping code or your app will likely be rejected.
The other tip was a Quick Look action in lldb that will render a rather awesome image of your app with overlays showing what the focus engine is seeing:
To access this feature of the debugger all you need to do is pause the debugger while within the scope of an instance of UIFocusUpdateContext
and then tap the small Quick Look button in the debugger pane in Xcode (icon of a small eye at the bottom of the panel).
The easiest way to do this is just by adding a breakpoint within the body of the didUpdateFocusInContext(_:withAnimationCoordinator:)
method and then selecting the instance of UIFocusUpdateContext.
You’ll need to implement this method if you've not done so already to access the instance of that corresponds to the focus event.<>
Very cool.
I’ve only covered a small segment of what Apple discussed at the Apple TV Tech Talks but I hope it’s been helpful, lookout for more posts in the future and don't forget my Apple TV Titbits notes.
I also expect at some point Apple will release videos of the sessions (just the resources from the event are onine at present), fingers crossed.
Further reading:
https://developer.apple.com/tech-talks/resources/
https://developer.apple.com/library/tvos/documentation/UIKit/Reference/UIFocusGuide_Class/
This article was originally written for Brightec by Cameron Cooke
Search over 400 blog posts from our team
Subscribe to our monthly digest of blogs to stay in the loop and come with us on our journey to make things better!