import SwiftUI #if canImport(UIKit) import UIKit #else import AppKit #endif /// 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 /// Systeme verschiedene Namen dafür haben, steht der Unterschied hier und /// nirgends sonst. Sonst wandert er in zwanzig Dateien und wird beim /// nächsten Bildschirm vergessen. /// Auf was für einem Gerät läuft das hier gerade? public enum FlaroPlatform { /// Läuft die App auf einem Mac? /// /// **Zwei Wege führen dorthin, und für den Übersetzer sehen sie /// verschieden aus.** Catalyst ist ein eigener Bau und meldet sich als /// `targetEnvironment(macCatalyst)`. „Für iPad angepasst" ist dagegen /// buchstäblich der iOS-Bau, der auf Apple Silicon startet — dort ist /// dieselbe Abfrage schlicht falsch. /// /// 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 os(tvOS) false #elseif targetEnvironment(macCatalyst) true #elseif os(iOS) ProcessInfo.processInfo.isiOSAppOnMac #elseif os(macOS) true #else #error("FlaroPlatform 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("FlaroPlatform kennt diese Plattform nicht. Hier eintragen — nicht raten.") #endif }() } extension View { /// 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 flaroInlineTitle() -> some View { #if os(iOS) navigationBarTitleDisplayMode(.inline) #else self #endif } /// Ein Sheet, das die halbe Höhe einnimmt. /// /// Auf dem Mac ist ein Sheet ein Fenster: dort zählt keine Rastung, /// sondern eine vernünftige Mindestgröße. public func flaroMediumSheet() -> 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(FlaroPlatform.isMac ? [.large] : [.medium]) #endif } /// Dasselbe, aber auf Wunsch bis zur vollen Höhe aufziehbar. public func flaroResizableSheet() -> 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(FlaroPlatform.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 /// Einstellungen auf dem Mac wie ein Systemdialog aus einem anderen /// Jahrzehnt aussahen. public func flaroForm() -> some View { #if os(macOS) formStyle(.grouped) #else self #endif } /// 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 flaroPlainEntry(isAddress: Bool = false) -> some View { #if os(iOS) self .keyboardType(isAddress ? .URL : .default) .textInputAutocapitalization(.never) .autocorrectionDisabled() #else autocorrectionDisabled() #endif } } 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 und auf dem /// Fernseher gibt es die Leiste in dieser Form nicht — beide fallen in /// den `#else`-Zweig. public func flaroClearToolbarBackground() -> some View { #if os(iOS) self .toolbarBackground(.hidden, for: .navigationBar) .toolbarColorScheme(.dark, for: .navigationBar) #else self #endif } } /// Etwas in die Zwischenablage legen. /// /// UIKit und AppKit heißen verschieden und meinen dasselbe; die Diagnose soll /// deswegen nicht mit `#if` beginnen. public enum FlaroClipboard { public static func copy(_ text: String) { #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() NSPasteboard.general.setString(text, forType: .string) #endif } } extension ToolbarItemPlacement { /// Rechts in der Titelzeile. public static var flaroTrailing: ToolbarItemPlacement { #if os(iOS) || os(tvOS) .topBarTrailing #else .primaryAction #endif } /// Links in der Titelzeile. public static var flaroLeading: ToolbarItemPlacement { #if os(iOS) || os(tvOS) .topBarLeading #else .navigation #endif } }