How does the tvOS focus engine work: focusable items, preferred focus, focus guides, and SwiftUI’s focusable and FocusState?

20.4.1 · L3 · Interview

Predict first

On Apple TV, an off-screen view and button, and a focus guide with a target. What do they report?

import UIKit

print(UIView().canBecomeFocused, UIButton().canBecomeFocused, UIFocusGuide().isEnabled)
let g = UIFocusGuide()
let b = UIButton()
g.preferredFocusEnvironments = [b]
print(g.preferredFocusEnvironments.count)
  • false false true, then 1
  • true true true, then 0
  • false true false, then 1

Predict first

SwiftUI focus on tvOS:

import SwiftUI

struct V: View {
    @FocusState var focused: Bool
    var body: some View { Text("Play").focusable().focused($focused) }
}
  • It compiles on tvOS
  • It does not compile on tvOS

Mental model. The focus engine is a spotlight that moves between objects on a stage when you press an arrow. It only lands on things that are lit and in the direction you pressed, so for an item in a diagonal blind spot you place an invisible ramp (a focus guide) that sends the spotlight where you want.

With no touch and no pointer, the user moves a focus (a highlighted, slightly enlarged element) with swipes on the Siri Remote’s click pad (or a game controller’s directional pad) and activates it by pressing. The focus engine decides where focus goes next: on a swipe it looks for the best focusable item in that direction from the currently focused one, using the geometry of the views on screen, and then calls the delegates so you can update the UI. The vocabulary (UIKit): a UIFocusItem is something focus can land on (a UIView whose canBecomeFocused is true, such as a button or a collection view cell that returns true for collectionView(_:canFocusItemAt:)); a UIFocusEnvironment is a container (a view or view controller) that can influence focus within it; and UIFocusUpdateContext describes a transition (previous and next item, the heading) passed to didUpdateFocus(in:with:), where you apply the focused appearance, usually with the animation coordinator so it blends with the system’s. A plain UIView and a button that is not yet in a visible window both report canBecomeFocused == false: focusability depends on being on screen, visible, enabled and user-interaction enabled. Preferred focus: when a screen appears, focus starts at the item the environment returns from preferredFocusEnvironments (the first focusable item otherwise), and you can steer it by overriding that list or by calling setNeedsFocusUpdate() followed by updateFocusIfNeeded() after changing the layout.

Focus guides solve the case where focus cannot reach an item because nothing focusable is in that direction (a button on the left and one on the right of a large image, with the engine unable to jump diagonally): a UIFocusGuide is an invisible, layout-managed rectangle that redirects focus to its preferredFocusEnvironments (a guide’s isEnabled defaults to true, and assigning a target button gives it one preferred environment), so you place a guide in the empty region and point it at the item you want. In SwiftUI, mark views with .focusable() (buttons, lists and links are focusable already), track and drive focus with @FocusState and .focused($flag) or .focused($enumValue, equals:), set a default with .defaultFocus(_:_:) or a focusScope, and read @Environment(\.isFocused) to style the focused state; the same code compiles on tvOS, and on watchOS where focus relates to the crown. Design rules: every actionable element must be focusable and reachable (test by navigating the whole screen with only the remote), keep a clear focus appearance (scale, shadow, parallax) that is visible from distance, avoid focus traps and dead ends (use guides), remember focus on first appearance and after content reloads (reloading a collection can move focus unexpectedly, so restore it with remembersLastFocusedIndexPath and preferred focus), do not rely on hover or touch gestures, and give grids and rows consistent sizes so directional navigation is predictable. Test with the simulator’s remote and with XCUITest using XCUIRemote.shared.press(.right) and .select, asserting on which element hasFocus.

Views that are not on screen cannot be focused. A guide is enabled by default and returns the environments you assign.

focusable() and @FocusState are the SwiftUI focus tools and are available on tvOS.

Common traps

  • An actionable control that focus cannot reach. Nothing focusable in that direction means a dead end; place a UIFocusGuide or rearrange.
  • Relying on hover or touch gestures. They do not exist on tvOS; design for focus and select.

They might follow up with

  • When can a view become focused? When it is focusable, enabled, user-interaction enabled and visible on screen.
  • What does a focus guide do? It redirects focus passing through its area to its preferred focus environments.

Related questions

  • 20.4.2 What input and layout rules define a good tvOS interface: the Siri Remote, controllers, the 10-foot UI, and…
  • 20.4.3 How do you build a production tvOS media app: AVPlayerViewController, Now Playing, performance on older…
  • 18.6.1 What is VideoToolbox, when would you use it directly, and what is different about media on tvOS?

What input and layout rules define a good tvOS interface: the Siri… →