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
@@ -5,7 +5,7 @@ import UIKit
import AppKit
#endif
/// Was auf iPhone und Mac verschieden heißt — an einer Stelle.
/// Was auf iPhone, Mac und Fernseher verschieden heißt — an einer Stelle.
///
/// Nicht jede Ansicht soll `#if os(iOS)` tragen. Ein Formular, das auf beiden
/// Plattformen dasselbe meint, soll auf beiden gleich aussehen; wo die
@@ -25,19 +25,49 @@ public enum FlixyPlatform {
/// Wer nur die erste Hälfte prüft, schreibt Code für den Mac, der auf dem
/// Mac nicht läuft: übersetzt sauber, ist aber genau dort abgeschaltet,
/// wo er gebraucht wird. Deshalb hier beide Wege, an einer Stelle.
///
/// **Und warum unten `#error` steht.** Der `#else`-Zweig lautete `true` —
/// eine Antwort auf eine Frage, die niemand gestellt hatte. Auf tvOS fiel
/// er deshalb hindurch und meldete einen Mac: kein Übersetzungsfehler,
/// keine Warnung, nur ein Fernseher, der sich für einen Mac hielt. Eine
/// Vorgabe ist an dieser Stelle immer falsch, weil die nächste Plattform
/// nicht danach fragt, ob sie gemeint war. Jetzt bricht der Bau ab und
/// verlangt eine Entscheidung.
public static let isMac: Bool = {
#if targetEnvironment(macCatalyst)
#if os(tvOS)
false
#elseif targetEnvironment(macCatalyst)
true
#elseif os(iOS)
ProcessInfo.processInfo.isiOSAppOnMac
#else
#elseif os(macOS)
true
#else
#error("FlixyPlatform kennt diese Plattform nicht. Hier eintragen — nicht raten.")
#endif
}()
/// Läuft die App auf einem Fernseher?
///
/// Daneben und nicht in einem Aufzählungstyp: über die Frage wird
/// nirgends verzweigt, sie steht überall in einem Fragezeichen-Ausdruck.
/// Ein Aufzählungstyp wäre Zeremonie — und „iPhone oder iPad" liesse sich
/// darin ohnehin nicht beantworten, weil das `UIDevice.current` verlangt
/// und damit den Hauptakteur.
public static let isTV: Bool = {
#if os(tvOS)
true
#elseif os(iOS) || os(macOS)
false
#else
#error("FlixyPlatform kennt diese Plattform nicht. Hier eintragen — nicht raten.")
#endif
}()
}
extension View {
/// Eine schmale Titelzeile — auf dem Mac gibt es sie nicht.
/// Eine schmale Titelzeile — auf dem Mac und auf dem Fernseher gibt es
/// sie nicht. Beide fallen in denselben `#else`-Zweig, und beide zu Recht.
public func flixyInlineTitle() -> some View {
#if os(iOS)
navigationBarTitleDisplayMode(.inline)
@@ -51,22 +81,37 @@ extension View {
/// Auf dem Mac ist ein Sheet ein Fenster: dort zählt keine Rastung,
/// sondern eine vernünftige Mindestgröße.
public func flixyMediumSheet() -> some View {
#if os(tvOS)
// Auf dem Fernseher ist ein Sheet immer bildfüllend. `presentationDetents`
// übersetzt dort zwar — es gibt sie seit tvOS 16 —, bewirkt aber
// nichts. Und eine Zeile, die nichts bewirkt, liest sich beim nächsten
// Mal wie eine, die etwas bewirkt.
self
#else
// **Zur Laufzeit, nicht beim Übersetzen.** Der `#else`-Zweig für
// macOS griff nie: auf dem Mac läuft der iOS-Bau, und damit bekam ein
// grosses Fenster eine Sheet-Kante in iPhone-Höhe — ein schmaler
// Streifen, der unten im Nichts klebt.
presentationDetents(FlixyPlatform.isMac ? [.large] : [.medium])
#endif
}
/// Dasselbe, aber auf Wunsch bis zur vollen Höhe aufziehbar.
public func flixyResizableSheet() -> some View {
#if os(tvOS)
self
#else
// Siehe oben. Auf dem Mac gibt es kein „halb aufgezogen": ein Fenster
// hat die Höhe, die es hat, und eine Liste soll sie nutzen.
presentationDetents(FlixyPlatform.isMac ? [.large] : [.medium, .large])
#endif
}
/// Ein Formular, das auf beiden Seiten gleich aussieht.
///
/// Auf tvOS gibt es `formStyle` nicht und es ist auch nicht nötig — dort
/// fällt es in denselben Zweig wie iOS.
///
/// Auf dem Mac setzt SwiftUI sonst den Spalten-Stil: Beschriftungen rechts
/// ausgerichtet, keine abgesetzten Abschnitte, alles eng. Der gruppierte
/// Stil ist derselbe, den iOS ohnehin nimmt — und der Grund, warum die
@@ -82,6 +127,9 @@ extension View {
/// Eingabefelder für Adressen und Namen: keine Großschreibung, keine
/// Korrektur — auf dem Mac gibt es beides ohnehin nicht als Tastatur.
///
/// Auf tvOS ebenfalls der `#else`-Zweig: die Bildschirmtastatur dort
/// kennt weder `keyboardType` noch Autokorrektur in dieser Form.
public func flixyPlainEntry(isAddress: Bool = false) -> some View {
#if os(iOS)
self
@@ -98,8 +146,9 @@ extension View {
/// Keine eigene Fläche hinter der Titelzeile.
///
/// Auf der Startseite liegt darunter der Aufmacher — eine weichgezeichnete
/// Leiste verschleierte die Hälfte davon. Auf dem Mac gibt es die Leiste in
/// dieser Form nicht.
/// Leiste verschleierte die Hälfte davon. Auf dem Mac und auf dem
/// Fernseher gibt es die Leiste in dieser Form nicht — beide fallen in
/// den `#else`-Zweig.
public func flixyClearToolbarBackground() -> some View {
#if os(iOS)
self
@@ -117,7 +166,16 @@ extension View {
/// deswegen nicht mit `#if` beginnen.
public enum FlixyClipboard {
public static func copy(_ text: String) {
#if canImport(UIKit)
#if os(tvOS)
// Es gibt keine. `UIPasteboard` ist auf tvOS als nicht verfügbar
// markiert — und ein Fernseher hat auch nichts, wohin man etwas
// einfügen könnte.
//
// **Die Reihenfolge ist der ganze Punkt.** `canImport(UIKit)` ist auf
// tvOS wahr; stünde dieser Zweig darunter, wäre es kein stiller
// Fehler, sondern ein lauter.
_ = text
#elseif canImport(UIKit)
UIPasteboard.general.string = text
#else
NSPasteboard.general.clearContents()
@@ -129,7 +187,7 @@ public enum FlixyClipboard {
extension ToolbarItemPlacement {
/// Rechts in der Titelzeile.
public static var flixyTrailing: ToolbarItemPlacement {
#if os(iOS)
#if os(iOS) || os(tvOS)
.topBarTrailing
#else
.primaryAction
@@ -138,7 +196,7 @@ extension ToolbarItemPlacement {
/// Links in der Titelzeile.
public static var flixyLeading: ToolbarItemPlacement {
#if os(iOS)
#if os(iOS) || os(tvOS)
.topBarLeading
#else
.navigation