tvOS Focus Engine: SwiftUI-Patterns für die Siri Remote
Apple TV ist die einzige Apple-Plattform ohne Touch-Oberfläche. Der Benutzer navigiert über Richtungswischer und Tastendrücke auf der Siri Remote, und jede Interaktion läuft über die Focus Engine: ein System, das anhand von Geometrie, Hierarchie und der vom Entwickler deklarierten Fokusstruktur entscheidet, welches Element als Nächstes den Fokus erhält1. SwiftUI auf tvOS stellt eine fokussierte (man verzeihe das Wortspiel) Vokabelsammlung für die Arbeit mit der Engine bereit: .focusable, @FocusState, .focused, .focusSection, .prefersDefaultFocus und .focusEffectDisabled. Apps, die diese Vokabeln übernehmen, fühlen sich nativ an; Apps, die gegen sie ankämpfen, erzeugen das Erlebnis einer Fernbedienung, die sich weigert, dorthin zu navigieren, wo der Benutzer es erwartet.
Dieser Beitrag durchläuft die API-Oberfläche der Focus Engine mit den Patterns, die sich in der Praxis bewähren. Der Rahmen lautet „Was die Engine annimmt und wie SwiftUI Sie kooperieren lässt”, denn Fokusdesign, das auf iOS mit Tap-and-Scroll funktioniert, scheitert auf tvOS oft. Der Beitrag des Clusters Apple Platform Matrix argumentierte, dass tvOS seinen Platz nur mit fokusbewusster UI verdient.
TL;DR
- Die Focus Engine löst den Fokus über Geometrie auf: Sie wählt die nächstgelegene fokussierbare View in Wischrichtung1. Apps kooperieren, indem sie fokussierbare Views, Focus Sections und Default-Focus-Ziele deklarieren.
@FocusState(mit.focused(_:equals:)) ist das SwiftUI-Primitiv für die programmatische Fokussteuerung. Derselbe Property Wrapper funktioniert auf iOS, macOS, watchOS und tvOS, doch auf tvOS macht er sich bezahlt2..focusSection()gruppiert mehrere fokussierbare Views zu einem einzigen Fokusziel für die Navigation zwischen Sections und überlässt es dann der Engine, innerhalb der Section auszuwählen3. Verwenden Sie das Modifier für Buttonreihen, Card-Grids und Sidebar-Sections..prefersDefaultFocus(_:in:)deklariert, welche View den Fokus erhält, wenn der Benutzer einen Kontext betritt (einen Screen, ein Popover, einen Tab). Kombinieren Sie das Modifier mit@Namespace, um den Default zu scopen4.- Der System-Fokuseffekt (das Highlight, das um die fokussierte View wächst) ist automatisch. Deaktivieren Sie ihn mit
.focusEffectDisabled()nur dann, wenn Sie ein eigenes Fokusvisual implementieren; ansonsten ist der plattformnative Effekt der richtige.
Wie die Focus Engine entscheidet
Die Focus Engine verarbeitet Wischeingaben von der Siri Remote und löst die Frage „Wohin geht der Fokus als Nächstes?” über eine hierarchische Suche auf1:
- Die Wischrichtung lesen (oben, unten, links, rechts).
- Innerhalb des aktuellen Fokuskontexts fokussierbare Views finden, deren Frames sich relativ zur aktuell fokussierten View in dieser Richtung befinden.
- Die geometrisch nächstgelegene entlang der Wischachse auswählen (mit einer leichten Tendenz, an der Mitte der aktuellen View ausgerichtet zu bleiben).
- Befindet sich keine fokussierbare View in dieser Richtung, wird der Wisch konsumiert, ohne dass sich der Fokus bewegt.
Die Implikation: Das visuelle Layout fokussierbarer Views ist genauso wichtig wie ihre logische Hierarchie. Zwei diagonal versetzte Buttons erzeugen mehrdeutige Navigation; zwei vertikal ausgerichtete Buttons erzeugen vorhersagbares Hoch/Runter. Das von den HIG empfohlene Pattern für Grids und Listen lautet: erst Ausrichtung, dann Dekoration.
Apps beteiligen sich über die Fokus-Modifier von SwiftUI an der Engine. Standardmäßig sind Views mit expliziter interaktiver Absicht (Button, NavigationLink, TextField) fokussierbar; statische Views (Text, Image, Container-Views wie VStack) sind es nicht.
Eigene Views fokussierbar machen
Der .focusable()-Modifier markiert eine View als Fokusziel5. Der optionale Boolean-Parameter konditioniert die Fokussierbarkeit:
struct PosterCard: View {
let movie: Movie
@FocusState private var isFocused: Bool
var body: some View {
VStack {
Image(movie.posterName)
.resizable()
.aspectRatio(2/3, contentMode: .fit)
Text(movie.title)
.font(.headline)
}
.focusable(true)
.focused($isFocused)
.scaleEffect(isFocused ? 1.1 : 1.0)
.animation(.spring(), value: isFocused)
}
}
Die View wird zu einem Fokusziel, auf dem die Engine landen kann. Das Pattern eignet sich für klickbare Cards, eigene Buttons und jede zusammengesetzte View, die die Aufmerksamkeit des Benutzers annehmen soll. Ohne .focusable() würde das Cluster aus Image + Text von der Engine übersprungen.
@FocusState und .focused(_:equals:) für programmatische Steuerung
Wenn die App den Fokus lenken muss (nach einem Navigationsübergang, nach einem Search-Submit, nach dem Schließen eines Modals), ist @FocusState das SwiftUI-Primitiv2:
struct LoginView: View {
enum Field { case username, password, submit }
@FocusState private var focusedField: Field?
@State private var username = ""
@State private var password = ""
var body: some View {
VStack {
TextField("Username", text: $username)
.focused($focusedField, equals: .username)
SecureField("Password", text: $password)
.focused($focusedField, equals: .password)
Button("Sign In") { /* ... */ }
.focused($focusedField, equals: .submit)
}
.onAppear {
focusedField = .username
}
}
}
Der @FocusState-Enum-Wert verfolgt, welches Feld fokussiert ist; das programmatische Zuweisen eines neuen Werts verschiebt den Fokus zur entsprechenden View. Das Hashable-Enum-Case ist die Konvention; mehrere Felder mit demselben Case-Wert wären mehrdeutig.
Für eine einzelne fokussierbare View ist @FocusState var isFocused: Bool plus .focused($isFocused) die einfachere Form. Die Boolean-Variante ist die richtige, wenn die Frage lautet: „Ist diese View fokussiert?”; die Enum-Variante ist die richtige für „Welche View aus dieser Menge?”.
.focusSection() zur Gruppierung
Ohne .focusSection() nimmt jede fokussierbare View auf derselben Ebene an der geometrischen Suche der Engine teil. Mit dem Modifier wird ein Container zu einer Fokusgruppe: Die Navigation zur/von der Section ist eine Entscheidung, die Navigation innerhalb der Section eine andere3. Beachten Sie, dass .focusSection() ausschließlich für tvOS und macOS gilt; auf iOS, iPadOS, watchOS oder visionOS hat das Modifier keine Wirkung.
HStack {
VStack {
Button("Settings") { ... }
Button("Profile") { ... }
Button("Logout") { ... }
}
.focusSection()
VStack {
ContentList(items: items)
}
.focusSection()
}
Die beiden VStacks werden als Einheiten navigierbar. Der Benutzer wischt von der Sidebar nach rechts, um im Inhaltsbereich zu landen; sobald er dort ist, übernimmt die Engine die Navigation innerhalb des Bereichs. Ohne .focusSection() könnten Wischer von einem Sidebar-Button auf einem beliebigen Inhaltselement landen, das zufällig geometrisch am nächsten liegt – mit einer UX, die sich willkürlich anfühlt.
Das richtige Pattern: Jede UI-Region mit interner Fokusstruktur (Sidebars, Card-Grids, Tab-Bars, Pagination-Steuerelemente) erhält einen .focusSection()-Modifier auf ihrem Container. Die Engine navigiert dann auf Makroebene zwischen Sections und auf Mikroebene innerhalb von Sections.
.prefersDefaultFocus(_:in:) für den initialen Fokus
Wenn ein Screen erscheint oder ein Popover sich öffnet, muss etwas den initialen Fokus erhalten. Ohne explizite Vorgabe wählt die Engine die erste fokussierbare View im Layout, was häufig falsch ist (der Zurück-Button statt der primären Aktion, eine obskure Listenzelle statt des Play-Buttons)4.
struct MovieDetailView: View {
let movie: Movie
@Namespace private var detailNamespace
var body: some View {
VStack {
HStack {
Button("Back") { ... }
Spacer()
}
PosterImage(movie: movie)
Button("Play") { ... }
.prefersDefaultFocus(in: detailNamespace)
Button("Add to Watchlist") { ... }
}
.focusScope(detailNamespace)
}
}
Das @Namespace zusammen mit .focusScope() definiert die Fokusgrenze, und .prefersDefaultFocus(in:) deklariert den bevorzugten initialen Fokus innerhalb dieses Scopes. Wenn der Screen erscheint, landet der Fokus auf Play.
Das Pattern ist das richtige für jede View, die der Benutzer mit einer offensichtlichen „Was ist als Erstes zu tun?”-Erwartung betritt: Play auf einer Filmdetailseite, Sign In auf einem Login-Screen, Get Started auf einem Onboarding-Screen.
Eigene Fokuseffekte (und wann der Default zu deaktivieren ist)
Der System-Fokuseffekt ist das weichkantige Glühen, das um eine fokussierte View wächst. Es skaliert die View leicht, fügt einen dezenten Schatten hinzu und animiert mit dem Standard-Timing der Plattform. Für die meisten Apps ist der Default richtig; er passt zu jeder anderen tvOS-App und lässt Benutzer die Vokabeln der Plattform lernen.
Für Apps, die ein eigenes Fokusvisual benötigen (ein markenspezifisches Glühen, einen inhaltsbewussten Effekt, einen Fokusring, der mit dem Default kollidiert), entzieht .focusEffectDisabled() der Standardbehandlung6:
Button {
play(movie)
} label: {
PosterImage(movie: movie)
.overlay(focusBorder)
.scaleEffect(isFocused ? 1.05 : 1.0)
}
.focusEffectDisabled()
.focused($isFocused)
Die eigene View ist dafür verantwortlich, den Fokus visuell anzuzeigen; das System greift nicht mehr ein. Der Trade-off: Jedes Fokusvisual muss von der App entworfen und implementiert werden, statt geerbt zu werden. Für die meisten Apps ist der System-Effekt die richtige Wahl.
Häufige tvOS-Fokus-Fehler
Drei Patterns, die schlechte tvOS-UX erzeugen:
Buttons, die keinen Fokus annehmen. Ein eigener Button, der als HStack { Image; Text } ohne .focusable() gerendert wird, ist für die Engine unsichtbar. Die Wischer der Siri Remote überspringen ihn. Lösung: Interaktiven Inhalt in Button einpacken (was die Fokusbeteiligung standardmäßig bietet) oder .focusable() explizit anwenden.
Fokusfallen. Eine View, die Fokus annimmt, aber keinen Ausweg bietet (kein fokussierbares links/rechts/oben/unten-Geschwister, keine Flucht über die Menu-Taste), lässt den Benutzer feststecken. Lösung: Jeder Fokuskontext sollte einen dokumentierten Ausstiegspfad haben. Das .focusSection()-Pattern hilft, weil es der Engine eine Einheit gibt, zu der sie entkommen kann.
Default-Fokus auf dem falschen Element. Eine Filmdetailansicht, die mit Fokus auf Back statt Play öffnet, ist Reibung, die der Benutzer bei jedem Besuch zahlt. Lösung: .prefersDefaultFocus(in:) auf der primären Aktion deklarieren.
Eigene Fokuseffekte, die nicht barrierefrei sind. Ein Fokusring, der nur ein 1pt breiter farbiger Rand mit niedrigem Kontrast ist, scheitert an der Barrierefreiheit. Der System-Fokuseffekt ist kontraststark und bewegungsgetestet; eigene Ersatzlösungen brauchen dieselbe Sorgfalt. Der Beitrag des Clusters Accessibility as platform behandelt das übergeordnete Prinzip.
Wann tvOS seinen Platz verdient
Der Beitrag des Clusters Apple Platform Matrix argumentierte, dass tvOS die Plattform mit der kleinsten installierten Basis im Verhältnis zu iOS ist und Apps einen echten „Lean-Back”- oder „Couch-Mode”-Anwendungsfall brauchen, um die Engineering-Investition zu rechtfertigen. Die Focus Engine ist Teil dieser Investition: Eine tvOS-App, die die Fokusvokabeln nicht ehrt, fühlt sich an wie eine iPad-App, die quer über einen Fernseher gespannt wurde. Die Investition ist real, weil die API-Oberfläche real ist; die Engineering-Arbeit ist sinnvoll, weil die Engine tatsächlich entscheidet, wohin der Fokus geht.
Apps, die ihren tvOS-Platz verdienen, teilen meist drei Eigenschaften: 1. Inhalte, die im Fernsehabstand konsumiert werden. Streaming, Foto-Slideshows, Controller-gesteuerte Spiele. 2. Spärliches Interaktionsmodell. Wenige primäre Aktionen pro Screen, navigiert mit Richtungseingaben. 3. Lean-Back-Anwendungsfall. Der Benutzer sitzt auf einer Couch, möglicherweise im Multitasking mit einem anderen Gerät, möglicherweise nur halb zuschauend.
Für Apps in diesen Kategorien ist die Focus-Engine-Investition richtig. Für Apps, die nicht passen (Produktivitätswerkzeuge, feingranulare Kreativ-Apps, alles mit hoher Texteingabe), ist der richtige Schritt, tvOS auszulassen, wie es der Matrix-Beitrag empfiehlt.
Was dieses Pattern für tvOS-Apps bedeutet
Drei Erkenntnisse.
-
Bauen Sie Fokusabsicht in das Layout ein, nicht in einen nachträglichen Fix. Wo wird der Benutzer starten? Wohin kann er von dort aus gelangen? Was ist die primäre Aktion? Das Design eines Screens auf tvOS beginnt mit dem Fokusfluss, nicht mit der visuellen Komposition. Das Visuelle folgt.
-
Verwenden Sie
.focusSection()aggressiv für jede Region mit interner Struktur. Die geometrische Default-Navigation ist für Grids, Sidebars und Tab-Bars oft falsch. Der Section-Modifier ist klein, der Unterschied groß. -
Behalten Sie den System-Fokuseffekt, sofern Sie keinen echten Grund haben, ihn zu ersetzen. Eigene Fokusvisuals bedeuten echte Engineering-Arbeit plus Barrierefreiheits-Arbeit plus Tests über alle Themes hinweg. Der System-Effekt ist der richtige Default; greifen Sie nur dann zu
.focusEffectDisabled(), wenn das Design tatsächlich eine eigene Behandlung braucht.
Der vollständige Apple-Ecosystem-Cluster: typisierte App Intents; MCP-Server; die Routing-Frage; Foundation Models; die Unterscheidung Runtime vs. Tooling LLM; drei Oberflächen; das Single-Source-of-Truth-Pattern; Two MCP Servers; Hooks für Apple-Entwicklung; Live Activities; die watchOS-Runtime; SwiftUI Internals; RealityKits räumliches mentales Modell; SwiftData-Schema-Disziplin; Liquid Glass Patterns; Multi-Plattform-Shipping; die Plattform-Matrix; Vision-Framework; Symbol Effects; Core ML Inferenz; Writing Tools API; Swift Testing; Privacy Manifest; Accessibility als Plattform; SF Pro Typografie; visionOS Spatial Patterns; Speech-Framework; SwiftData-Migrationen; worüber ich mich weigere zu schreiben. Der Hub liegt unter Apple Ecosystem Series. Für den breiteren Kontext iOS mit AI-Agenten siehe den iOS Agent Development guide.
FAQ
Funktioniert .focusable() auf iOS?
Ja, doch sein Verhalten zielt auf iOS auf Tastatur- und Pointer-Interaktionen (Bluetooth-Tastatur, iPadOS-Pointer, iPad Magic Keyboard) ab, nicht auf die fokus-engine-getriebene Navigation, die tvOS verwendet. Derselbe Code lässt sich plattformübergreifend einsetzen; die nutzerseitige Interaktion unterscheidet sich. Auf tvOS ist .focusable() der primäre Pfad. Auf iOS ist es eine ergänzende Affordance für die Barrierefreiheit.
Was ist der Unterschied zwischen .focusable() und Button?
Button ist ein höherstufiges Konstrukt, das Fokussierbarkeit, Aktionsbehandlung, den System-Button-Stil und Accessibility-Traits umfasst. .focusable() ist der Low-Level-Marker, der eine View lediglich zu einem Fokusziel macht. Verwenden Sie Button, wenn die View logisch ein Button ist; verwenden Sie .focusable(), wenn Sie eine eigene interaktive View bauen (eine Poster-Card, eine Kachel in einem Grid), die nicht in das Button-Mental-Modell passt.
Kann ich mehrere .prefersDefaultFocus-Deklarationen haben?
Ja, gescopt durch @Namespace. Jeder Fokus-Scope kann seinen eigenen bevorzugten Default haben. Das Pattern ist richtig für verschachtelte Kontexte (ein Popover innerhalb eines Screens, ein Tab innerhalb einer Sidebar): Jeder Scope wählt seinen eigenen initialen Fokus.
Wie behandle ich Fokus in einer Liste mit vielen Elementen?
Listen in SwiftUI sind standardmäßig fokussierbar; die Engine handhabt die Hoch-/Runter-Navigation durch Zellen automatisch. Für eigene listenartige Layouts wickeln Sie jede Zelle in einen Button ein oder wenden .focusable() an, und platzieren Sie die gesamte Liste dann innerhalb einer .focusSection(), damit die Engine die Liste als Einheit relativ zu anderen UI-Regionen behandelt.
Was macht die Menu-Taste im Fokusmodell?
Die Menu-Taste der Siri Remote ist die Dismiss/Zurück-Aktion über tvOS hinweg. Sie poppt den Navigationsstack, verlässt Modals, kehrt zum übergeordneten Kontext zurück. SwiftUI behandelt sie automatisch über NavigationStack und das Standard-Modal-Dismissal; Apps fangen sie typischerweise nicht ab. Für eigene Dismiss-Logik erfasst der View-Modifier onExitCommand den Tastendruck.
Wie verhält sich das zu den anderen Plattform-Beiträgen des Clusters?
Die tvOS-Focus-Engine ist die plattformspezifische Navigationsoberfläche, parallel zu visionOS’ Gaze-and-Pinch (behandelt in visionOS spatial patterns) und iOS’ Tap-and-Scroll. Jede Plattform hat ihre eigene Eingabemetapher; der Beitrag Apple Platform Matrix des Clusters argumentiert, dass die Aufnahme einer Plattform das Ehren dieser Metapher voraussetzt – und die Focus Engine ist das, was tvOS verlangt.
Quellen
-
Apple Developer: App Programming Guide for tvOS, Controlling the User Interface with the Apple TV Remote. Das Modell der Focus Engine und die Regeln zur geometrischen Auflösung. ↩↩↩
-
Apple Developer Documentation:
@FocusState. Der Property Wrapper zum Verfolgen und programmatischen Lenken des Fokus über SwiftUI-Plattformen hinweg. ↩↩ -
Apple Developer Documentation:
focusSection(). Der View-Modifier, der fokussierbare Nachfahren zu einem einzigen Fokusziel für die Navigation zwischen Sections gruppiert. ↩↩ -
Apple Developer Documentation:
prefersDefaultFocus(_:in:)undfocusScope(_:). Die Default-Focus-Deklaration, kombiniert mit namespace-gescopten Fokusgrenzen. ↩↩ -
Apple Developer Documentation:
focusable(_:). Der View-Modifier, der eine View mit einem optionalen konditionalen Boolean als Fokusziel markiert. ↩ -
Apple Developer Documentation:
focusEffectDisabled(_:). Der Opt-out für den System-Fokuseffekt (Bool-Defaulttrue); kombinieren Sie ihn bei Bedarf mit eigenen Fokusvisuals. ↩