Die Pakete wissen jetzt, was ein Fernseher ist

Schritt eins zur Apple-TV-Fassung: nichts davon ist sichtbar, aber ohne
das lässt sich für tvOS nicht einmal übersetzen.

**Der stille Fehler zuerst.** `FlixyPlatform.isMac` hatte einen
`#else`-Zweig, der `true` zurückgab — eine Antwort auf eine Frage, die
niemand gestellt hatte. Auf tvOS fiel er dort hindurch: kein
Übersetzungsfehler, keine Warnung, nur ein Fernseher, der sich für einen
Mac hielt. Jetzt steht dort `#error`. Die nächste Plattform bricht den
Bau ab und verlangt eine Entscheidung, statt sich eine auszusuchen. Ein
Test hält fest, dass nicht beides zugleich gelten darf; er hätte genau
diesen Fehler gefangen.

**Ein echter Übersetzungsfehler**, und er lag an der Reihenfolge:
`UIPasteboard` steht hinter `canImport(UIKit)`, und das ist auf tvOS
wahr. Der tvOS-Zweig steht deshalb jetzt davor — ein Fernseher hat
ohnehin nichts, wohin man etwas einfügen könnte.

**Drei Vermutungen waren falsch**, nachgeschlagen im tvOS-SDK statt
geraten: `presentationDetents` gibt es dort seit tvOS 16 (übersetzt,
bewirkt nur nichts — jetzt steht es ausdrücklich da), `.topBarTrailing`
seit 14.0, und `isMac` hatte acht Fundstellen in fünf Dateien, nicht
fünfzig in siebenundzwanzig.

**Schrift und Abstände** bekommen einen eigenen Zweig statt durchzufallen.
Nicht feste Grössen: Dynamic Type gibt es auf tvOS auch, und dass `body`
dort grösser ausfällt, ist der Sinn der Sache. Falsch war nur die
Zuordnung der Rollen — die Skala ist anders gespreizt, und eine
Beschriftung in `subheadline` unter einem Plakat ist aus drei Metern
Zierrat. Der Aufmacher geht auf `.title`, weil UIKit `largeTitle` auf
tvOS gar nicht kennt und nirgends steht, was SwiftUI daraus macht.
`screenMargin` steht auf 48 und nicht auf 90: die neunzig Punkte des
Überscan-Bereichs liefert das System als Sicherheitsrand, und wer sie
hier aufschlägt, hat hundertachtzig.

**Zwei Dinge, die sich erst beim Bauen zeigten.** `FlixyDownloads`
übersetzte nicht — `volumeAvailableCapacityForImportantUsage` gibt es
auf tvOS nicht. Das Paket wird dort trotzdem mitverlinkt, damit
`PlayerModel` den Koordinator nennen darf, ohne dass zweitausend Zeilen
Wiedergabelogik von `#if` durchzogen werden; angelegt wird nie einer.
Und `FlixyProbe` liess das ganze Paket scheitern: ein
Kommandozeilenwerkzeug mit `getpass` und `Host`, das auf einem Fernseher
nie laufen sollte, hielt den Bau für alle anderen auf. Es ist jetzt
ausdrücklich macOS-only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-29 17:09:18 +02:00
co-authored by Claude Opus 5
parent 4576eac67c
commit 6010229b80
7 changed files with 197 additions and 11 deletions
@@ -14,6 +14,13 @@ import SwiftUI
/// Das kostet auf dem Mac Dynamic Type. Das ist vertretbar: macOS hat keine
/// systemweite Textgrößen-Einstellung, auf die Apps reagieren müssten, und ein
/// Fenster lässt sich stattdessen größer ziehen.
///
/// **Auf dem Fernseher wieder Systemstile, aber andere Rollen.** Dynamic Type
/// gibt es dort auch, und dass `body` grösser ausfällt als am Telefon, ist
/// der Sinn der Sache — drei Meter Abstand. Falsch wäre nur, die iOS-Zuordnung
/// einfach durchfallen zu lassen: die Skala ist dort anders gespreizt, und
/// eine Beschriftung in `subheadline` unter einem Plakat ist aus drei Metern
/// Zierrat.
extension Font {
#if os(macOS)
/// Der Titel im Aufmacher-Bild.
@@ -28,6 +35,29 @@ extension Font {
public static let flixyBody = Font.system(size: 17)
/// Jahreszahl, Laufzeit, Auflösung — alles, was neben dem Titel steht.
public static let flixyCaption = Font.system(size: 12)
#elseif os(tvOS)
/// Der Titel im Aufmacher-Bild.
///
/// **`.title` und nicht `.largeTitle`.** In SwiftUI gibt es den Stil auf
/// tvOS, in UIKit nicht — `UIFontTextStyleLargeTitle` ist dort als nicht
/// verfügbar markiert. Was SwiftUI daraus macht, steht nirgends, und eine
/// Schrift, deren Grösse niemand nennen kann, gehört nicht in die grösste
/// Zeile der App.
public static let flixyHero = Font.system(.title, design: .default, weight: .bold)
/// Titel auf Detailseiten.
public static let flixyTitle = Font.system(.title2, design: .default, weight: .semibold)
/// Überschrift einer Reihe auf der Startseite.
public static let flixyRowHeader = Font.system(.headline, design: .default, weight: .semibold)
/// Titel unter einem Poster — auf tvOS nur bei Fokus sichtbar.
///
/// Eine Stufe tiefer als auf iOS, und das ist gewollt: auf tvOS ist
/// `caption` schon gross, und ein Raster aus sechs Spalten mit
/// `subheadline`-Titeln hätte mehr Schrift als Bild.
public static let flixyCardTitle = Font.system(.caption, design: .default, weight: .medium)
/// Fließtext, Beschreibungen.
public static let flixyBody = Font.system(.body)
/// Jahreszahl, Laufzeit, Auflösung — alles, was neben dem Titel steht.
public static let flixyCaption = Font.system(.caption2)
#else
/// Der Titel im Aufmacher-Bild.
public static let flixyHero = Font.system(.largeTitle, design: .default, weight: .bold)
@@ -49,6 +79,22 @@ extension Font {
/// Eine Skala statt frei gewählter Zahlen: dadurch stimmen die Ränder zwischen
/// Bildschirmen überein, ohne dass man sie einzeln nachmisst.
public enum FlixySpacing {
#if os(tvOS)
// Durchweg doppelt: ein Fernseher ist 1920 Punkte breit und wird aus drei
// Metern gelesen. Acht Punkte Abstand sind dort keine Trennung mehr.
public static let tight: CGFloat = 8
public static let snug: CGFloat = 16
public static let regular: CGFloat = 24
public static let loose: CGFloat = 32
public static let section: CGFloat = 48
/// Der seitliche Rand des Inhalts — **zusätzlich** zum Sicherheitsrand.
///
/// **Nicht 90.** Die neunzig Punkte des Überscan-Bereichs liefert das
/// System als Sicherheitsrand; wer sie hier noch einmal aufschlägt, hat
/// hundertachtzig und ein Raster, das in der Bildmitte klebt. Hier steht
/// nur die Rinne innerhalb des sicheren Bereichs.
public static let screenMargin: CGFloat = 48
#else
public static let tight: CGFloat = 4
public static let snug: CGFloat = 8
public static let regular: CGFloat = 12
@@ -56,10 +102,34 @@ public enum FlixySpacing {
public static let section: CGFloat = 24
/// Der seitliche Rand des Inhalts zum Bildschirmrand.
public static let screenMargin: CGFloat = 16
#endif
}
/// Der Überscan-Bereich eines Fernsehers.
///
/// **Was das ist.** Ein Fernseher zeigt nicht zwingend das ganze Bild: je nach
/// Gerät und Einstellung schneidet er am Rand ab, und was dort steht, sieht
/// niemand. Apple nennt für 1920×1080 einen sicheren Bereich von neunzig
/// Punkten seitlich und sechzig oben wie unten.
///
/// Ein **Bild** darf darüber hinausgehen — ein Aufmacher soll das sogar.
/// Schrift und Knöpfe nicht. Die Zahl steht hier für die Fälle, in denen der
/// Sicherheitsrand absichtlich übergangen wird und der Text trotzdem drinnen
/// bleiben muss.
public enum FlixySafeArea {
public static let tv = EdgeInsets(top: 60, leading: 90, bottom: 60, trailing: 90)
}
public enum FlixyRadius {
/// Die Ecke einer Kachel.
///
/// Auf dem Fernseher grösser: eine Kachel, die sich beim Fokus anhebt,
/// wirkt mit acht Punkten bei 1920 Punkten Breite kantig.
#if os(tvOS)
public static let card: CGFloat = 12
#else
public static let card: CGFloat = 8
#endif
public static let sheet: CGFloat = 16
public static let control: CGFloat = 10
/// Eine schwebende Fläche — die Seitenleiste. So rund wie das System sie