import SwiftUI /// Die Schriftskala der App. /// /// Auf iOS die Systemstile, damit Dynamic Type wirkt. Die Rolle steht im /// Namen, nicht die Größe — sonst führt jede Anpassung zu einer Suche quer /// durch alle Views. /// /// **Auf dem Mac stehen feste Größen.** Dieselben Stile sind dort deutlich /// kleiner: `largeTitle` misst 26 Punkt statt 34, `body` 13 statt 17. Eine /// Oberfläche, die auf dem iPad stimmt, wirkt damit auf dem Mac zusammen- /// geschrumpft — und genau das war sie. Die Zahlen hier sind die von iOS. /// /// 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. public static let haloHero = Font.system(size: 34, weight: .bold) /// Titel auf Detailseiten. public static let haloTitle = Font.system(size: 22, weight: .semibold) /// Überschrift einer Reihe auf der Startseite. public static let haloRowHeader = Font.system(size: 17, weight: .semibold) /// Titel unter einem Poster. public static let haloCardTitle = Font.system(size: 15, weight: .medium) /// Fließtext, Beschreibungen. public static let haloBody = Font.system(size: 17) /// Jahreszahl, Laufzeit, Auflösung — alles, was neben dem Titel steht. public static let haloCaption = 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 haloHero = Font.system(.title, design: .default, weight: .bold) /// Titel auf Detailseiten. public static let haloTitle = Font.system(.title2, design: .default, weight: .semibold) /// Überschrift einer Reihe auf der Startseite. public static let haloRowHeader = 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 haloCardTitle = Font.system(.caption, design: .default, weight: .medium) /// Fließtext, Beschreibungen. public static let haloBody = Font.system(.body) /// Jahreszahl, Laufzeit, Auflösung — alles, was neben dem Titel steht. public static let haloCaption = Font.system(.caption2) #else /// Der Titel im Aufmacher-Bild. public static let haloHero = Font.system(.largeTitle, design: .default, weight: .bold) /// Titel auf Detailseiten. public static let haloTitle = Font.system(.title2, design: .default, weight: .semibold) /// Überschrift einer Reihe auf der Startseite. public static let haloRowHeader = Font.system(.headline, design: .default, weight: .semibold) /// Titel unter einem Poster. public static let haloCardTitle = Font.system(.subheadline, design: .default, weight: .medium) /// Fließtext, Beschreibungen. public static let haloBody = Font.system(.body) /// Jahreszahl, Laufzeit, Auflösung — alles, was neben dem Titel steht. public static let haloCaption = Font.system(.caption) #endif } /// Abstände. /// /// Eine Skala statt frei gewählter Zahlen: dadurch stimmen die Ränder zwischen /// Bildschirmen überein, ohne dass man sie einzeln nachmisst. public enum HaloSpacing { #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 — **vom Bildschirmrand aus**. /// /// Achtzig Punkte Überscan plus achtundvierzig Punkte Rinne. /// /// **Warum die achtzig hier drinstehen.** SwiftUI legt sie von selbst als /// Sicherheitsrand an, und normalerweise nähme man sie geschenkt. Der /// Aufmacher muss aber bis an die Bildschirmkante reichen — ein Bild, das /// achtzig Punkte vor dem Rand aufhört, sieht auf einem Fernseher nach /// Fehler aus. Also laufen die Seiten dieser App bildfüllend /// (`ignoresSafeArea`), und dann muss der Rand hier wieder dazu. /// /// Das heisst zugleich: wer diesen Wert benutzt, ignoriert den /// Sicherheitsrand. Beides gehört zusammen. public static let screenMargin: CGFloat = 128 #else public static let tight: CGFloat = 4 public static let snug: CGFloat = 8 public static let regular: CGFloat = 12 public static let loose: CGFloat = 16 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. /// /// **Nachgemessen, nicht abgeschrieben.** Im Simulator eines Apple TV 4K /// meldet SwiftUI bei 1920×1080 genau diese Ränder — sechzig oben und unten, /// **achtzig** seitlich, Nutzfläche also 1760×960. Die verbreitete Angabe /// „neunzig" stimmt nicht; eine zweite Quelle nannte achtzig und hatte recht. /// /// **Und man bekommt sie geschenkt.** SwiftUI legt sie von selbst als /// Sicherheitsrand an — deshalb steht in `HaloSpacing.screenMargin` nur die /// Rinne innerhalb davon. Die Zahl hier ist für die Gegenrichtung: ein /// bildfüllender Aufmacher, der den Sicherheitsrand absichtlich übergeht und /// dessen Schrift trotzdem drinbleiben muss. public enum HaloSafeArea { public static let tv = EdgeInsets(top: 60, leading: 80, bottom: 60, trailing: 80) } public enum HaloRadius { /// 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 /// auf dem iPad zeichnet; daran misst sich der Nachbau. public static let panel: CGFloat = 22 } /// Die Seitenverhältnisse, in denen Plex seine Bilder liefert. /// /// An einer Stelle, weil ein falscher Wert nicht auffällt, sondern nur /// beschneidet: ein 16:9-Hintergrundbild in einem 3:4-Rahmen verliert links /// und rechts je ein Viertel, und man sieht dem Ergebnis nicht an, dass etwas /// fehlt. public enum HaloAspectRatio { /// Kinoplakate im Hochformat. public static let poster: CGFloat = 2.0 / 3.0 /// Szenenbilder und Hintergrundbilder. public static let landscape: CGFloat = 16.0 / 9.0 /// Der Aufmacher auf der Startseite — dasselbe wie `landscape`, weil Plex /// dafür `art` liefert. public static let hero: CGFloat = landscape /// Der Aufmacher auf einem schmalen Bildschirm. /// /// **Quer, aber beschnitten — und beides hat einen Grund.** /// /// Quer, weil dort ein Hintergrundbild liegt. Vorher lag hier ein Plakat /// (2:3), und das ist für einen Aufmacher das falsche Bild: ein Plakat /// trägt den Titel schon selbst, und darüber liegt dann noch der /// freigestellte Schriftzug — zweimal derselbe Name. /// /// Beschnitten, weil 16:9 auf einem iPhone keine zweihundertfünfzig /// Punkte Höhe sind, und darin sollen Schriftzug, Zeile mit Jahr und /// Länge, ein Satz Beschreibung und zwei Knöpfe Platz finden. Sie fanden /// ihn nicht und lagen übereinander. 4:3 gibt dem Bild rund ein Drittel /// mehr Höhe; dafür fehlt ihm links und rechts zusammen ein Viertel. public static let heroCompact: CGFloat = 4.0 / 3.0 /// Sendersymbole. Breiter als hoch, weil Senderlogos Schriftzüge sind — /// quadratisch beschnitten fehlt der halbe Name. public static let channelLogo: CGFloat = 5.0 / 3.0 }