← Alle Beitrage

Fünf Apple-Plattformen, drei geteilte Dateien: Wie Return tatsächlich plattformübergreifend in SwiftUI ausgeliefert wird

Return, mein Meditations-Timer, läuft auf fünf Apple-Plattformen: iPhone, iPad, Mac, Apple Watch und Apple TV.1 Die Codebasis enthält 40 Swift-Dateien (ohne Tests). Drei davon werden über alle fünf Plattformen hinweg geteilt. Der Rest ist auf separate Xcode-Targets aufgeteilt, die Konzepte wie TimerManager, AudioManager und ContentView duplizieren, statt sie über bedingte Kompilierung mit #if os(...) gemeinsam zu nutzen.

Die Sharing-Quote liegt bei rund 7,5 % – und das mit voller Absicht.

In diesem Essay geht es darum, wie das Ausliefern einer plattformübergreifenden SwiftUI-App im Jahr 2026 tatsächlich aussieht, warum aggressives Code-Sharing überschätzt wird und was die drei Dateien, die doch geteilt werden, gemeinsam haben.

iOS 26 platform tile from Apple Developer iPadOS 26 platform tile from Apple Developer macOS 26 platform tile from Apple Developer watchOS 26 platform tile from Apple Developer tvOS 26 platform tile from Apple Developer

Die fünf Plattformen, auf die Return zielt, so wie Apple sie auf developer.apple.com präsentiert. Jede ist ein eigenständiges Plattform-Target in Xcode, kein Laufzeit-Zweig.

TL;DR

  • Return: 18 Swift-Dateien im Haupt-Target (iOS + iPadOS + macOS), 10 Dateien im tvOS-Target, 7 Dateien im watchOS-Target, 2 Widget-Dateien (Live Activities) und 3 wirklich plattformübergreifende Dateien in Return/Shared/. Insgesamt 40.
  • Die drei geteilten Dateien sind die persistenznahen: MeditationSession, SessionStore, SessionHistoryView. Zustand, der via iCloud reist – nicht UI, die sich an die Plattform anpasst.
  • tvOS und watchOS sind eigene Xcode-Targets, keine #if os(tvOS)-Zweige im Haupt-Target. Die Bedienmodelle sind zu unterschiedlich, um in eine ContentView zu passen.
  • Selbst innerhalb des iOS/iPadOS/macOS-Haupt-Targets wuchern #if os-Blöcke: 10 in ContentView.swift, 8 in LiveActivityManager.swift, 8 in VideoBackgroundView.swift, 6 in AudioManager.swift.
  • Ehrliche Einschätzung: Aggressives Sharing über fünf Apple-Plattformen hinweg ist eine Wartungslast. Ein kleiner geteilter Kern (die Persistenzschicht) plus separate plattformspezifische UIs liefert schneller aus und bricht seltener als eine einzige, mit #if durchsetzte Riesendatei.

Die plattformspezifischen Begleitstücke finden Sie in der Apple-Plattformmatrix, im watchOS-Laufzeitvertrag und in den Liquid-Glass-Mustern für SwiftUI.

Die Zahlen

Die Form der Codebasis nach Anzahl der Swift-Dateien, nachdem Tests und UI-Tests entfernt wurden:

Return/                            18 files   (iPhone + iPad + Mac, single target)
├── Shared/                         3 files     cross-platform truth   ├── MeditationSession.swift   ├── SessionStore.swift   └── SessionHistoryView.swift
├── ContentView.swift              (10 #if os branches)
├── TimerManager.swift             (2 #if os branches)
├── AudioManager.swift             (6 #if os branches)
├── HealthKitManager.swift
├── LiveActivityManager.swift      (8 #if os branches, iOS-only)
├── ThemeManager.swift
├── VideoBackgroundView.swift      (8 #if os branches)
├── GlassTextShape.swift           (Liquid Glass, see prior post)
├── GlassTimerText.swift
└──  (settings, theme, audio assets, etc.)

ReturnTV/                          10 files   (tvOS, separate target)
├── TVContentView.swift
├── TVTimerManager.swift            duplicates main TimerManager
├── TVAudioManager.swift            duplicates main AudioManager
├── TVDurationPicker.swift
├── TVFocusModifier.swift           tvOS button styles for focus
├── TVSettingsView.swift
└── ReturnWatch Watch App/              7 files   (watchOS, separate target)
├── WatchContentView.swift
├── WatchTimerManager.swift         duplicates main TimerManager
├── WatchAudioManager.swift         duplicates main AudioManager
├── WatchHealthKitManager.swift     duplicates main HealthKitManager (mostly)
├── WatchSettingsView.swift
└── ReturnWidgets/                      2 files   (Live Activity + bundle)
├── ReturnLiveActivity.swift
└── ReturnWidgetsBundle.swift

Fünf Plattformen, drei geteilte Dateien, zwei separate plattformspezifische Targets plus ein Widget-Target, dazu reichlich bedingte Kompilierung innerhalb des Haupt-Targets. Die Sharing-Quote liegt bei etwa 7,5 %. Die meisten Tutorials zu „Multi-Platform-SwiftUI” empfehlen das Gegenteil: eine ContentView schreiben, die sich über @Environment(\.horizontalSizeClass) und #if os(...) an jede Plattform anpasst.2 Das funktioniert für zwei Plattformen (iPhone + iPad). Bei fünf bricht es zusammen.

Was die drei geteilten Dateien gemeinsam haben

Return/Shared/MeditationSession.swift definiert den SwiftData-nahen Werttyp:3

struct MeditationSession: Codable, Identifiable, Equatable {
    let id: UUID
    let startDate: Date
    let endDate: Date
    let durationSeconds: Int
    let sourceDevice: DeviceType
    var syncedToHealthKit: Bool

    enum DeviceType: String, Codable, CaseIterable {
        case iPhone, iPad, mac, appleTV, appleWatch
    }
}

Der Header-Kommentar der Datei ist tragend: // Add this file to: Return, ReturnTV, ReturnWatch Watch App targets. Dieselbe Quelldatei wird von allen drei Xcode-Targets referenziert – nicht per Symlink, nicht in ein Swift-Package eingebettet. Apples Build-System kompiliert eine Datei klaglos in drei Binaries.

SessionStore.swift ist die Persistenzschicht: ein dünner Wrapper um NSUbiquitousKeyValueStore (Apples iCloud-Key-Value-Store), der MeditationSession-Arrays liest und schreibt. Diese Wahl ist entscheidend: Die KV-Store-Synchronisierung gibt Return geräteübergreifende Sitzungs-Historie ohne Bereitstellung eines CloudKit-Containers, mit dem Trade-off, dass der gesamte Store auf 1 MB insgesamt begrenzt ist.12 Für eine Liste von Meditations-Sitzungen mit durchschnittlich ein paar hundert Bytes pro Eintrag ist das Limit mehr als ausreichend. SessionHistoryView.swift ist eine SwiftUI-Liste, die die Sitzungen rendert. Beide werden von den iPhone-, iPad-, Mac-, Watch- und TV-Targets identisch genutzt.

Was diese drei Dateien gemeinsam haben: Sie beschreiben Zustand, nicht Interaktion. Eine MeditationSession ist auf jedem Gerät dasselbe Konzept. Die Liste vergangener Sitzungen liest sich auf jedem Gerät gleich. Keiner der beiden Punkte berührt eine Bedienoberfläche, einen Fenstermanager, eine Audiorouting-Entscheidung, eine Focus-Engine oder eine Digital Crown. Sobald eine Datei wissen muss, auf welcher Plattform sie läuft, hört sie auf, teilbar zu sein.

Warum der Rest nicht geteilt wurde

Nehmen wir TimerManager. Die iOS/iPadOS/macOS-Variante nutzt Timer.publish(every: 1, ...) und leitet Benachrichtigungen über UserNotifications. Die tvOS-Variante (TVTimerManager) behandelt den Fall, dass der Benutzer per Siri Remote pausiert hat und der Bildschirmschoner einsetzt. Die watchOS-Variante (WatchTimerManager) delegiert an eine WKExtendedRuntimeSession (über WatchSessionManager), damit das OS die App reaktionsfähig hält, während der Bildschirm abdunkelt, und sie führt Eingaben über die Digital Crown statt über Touch entgegen. Drei Plattformen, drei zutiefst unterschiedliche Timer-Verhalten.

Sie könnten das alles als class TimerManager { #if os(watchOS) ... #elif os(tvOS) ... } vereinheitlichen. Heraus käme eine Klasse mit drei Modi, jeder mit etwa vierzig Zeilen #if-eingeschränktem Code, in der eine Änderung am iOS-Pfad den watchOS-Pfad zu zerschießen droht. Das ist ein Wartungs-Albtraum.

Drei separate Klassen mit drei Dateinamen sind mehr Code auf der Festplatte und weniger Code im Kopf. Duplikation, die Sie lesen können, schlägt eine Abstraktion, die Sie nicht durchschauen.

Dieselbe Logik gilt für:

  • ContentView vs. TVContentView vs. WatchContentView: Die Navigationsmodelle sind unterschiedlich (Push-basiert auf dem iPhone, Focus-basiert auf dem TV, listenbasiert auf der Watch).
  • AudioManager vs. TVAudioManager vs. WatchAudioManager: Audio-Sitzungs-Kategorien unterscheiden sich, watchOS hat strengere Regeln für Hintergrund-Audio, tvOS leitet anders an AirPlay weiter.
  • VideoBackgroundView hat im Haupt-Target 8 #if os(iOS)-Zweige (mit einem #elseif os(macOS)-Pendant), die unterschiedliche Video-Assets (fire_phone.mp4 vs. fire_mac.mp4), unterschiedliche Layer-Typen und unterschiedliche Seitenverhältnisse abdecken.4

Ich möchte anmerken: Das Haupt-Target Return/ fasst iOS, iPadOS und macOS tatsächlich zusammen. Diese drei Plattformen teilen mehr Code als nicht. SwiftUIs NavigationStack funktioniert auf allen dreien. .glassEffect() funktioniert auf allen dreien. Die Unterschiede beim Window-Management sind real, aber innerhalb eines Targets handhabbar. tvOS und watchOS waren die Stelle, an der ich die Trennung in eigene Targets gezogen habe.

Der tvOS-Fall: Warum die Focus-Engine ein eigenes Target erzwang

Die Apple-TV-Navigation ist um die Focus-Engine herum gebaut.5 Jedes UI-Element, mit dem der Benutzer interagieren kann, deklariert sich als fokussierbar; die Pfeile auf der Siri Remote bewegen den Fokus zwischen Elementen; das Drücken der Auswahltaste aktiviert das fokussierte Element. SwiftUI auf tvOS macht dies über .focusable(), .focusEffect und benutzerdefinierte ButtonStyle-Typen zugänglich, die auf @Environment(\.isFocused) reagieren – für den Parallaxen-Tilt-Effekt, den Apples eigene Apps verwenden. Echter Produktionscode aus TVFocusModifier.swift:6

struct TVCapsuleButtonStyle: ButtonStyle {
    var accentColor: Color = .white
    @Environment(\.isFocused) private var isFocused

    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .colorMultiply(isFocused ? focusedTextColor : accentColor)
            .background(
                Capsule().fill(isFocused
                    ? AnyShapeStyle(accentColor)
                    : AnyShapeStyle(.ultraThinMaterial))
            )
            .clipShape(Capsule())
            .scaleEffect(isFocused ? 1.1 : 1.0)
            .scaleEffect(configuration.isPressed ? 0.95 : 1.0)
            .shadow(color: .black.opacity(isFocused ? 0.3 : 0.1),
                    radius: isFocused ? 20 : 5, y: isFocused ? 10 : 2)
            .animation(.easeInOut(duration: 0.2), value: isFocused)
    }
}

Dieselbe Datei definiert auch TVCircleButtonStyle für quadratische bzw. runde Bedienelemente. Beide Stile invertieren Farbe und Transluzenz beim Fokussieren: Unfokussierte Buttons sitzen auf .ultraThinMaterial, fokussierte Buttons füllen sich mit der Akzentfarbe und erhöhen Skalierung und Schatten. Das Muster ist für diese App strukturell tvOS-spezifisch. @Environment(\.isFocused) ist über iOS, iPadOS, macOS, watchOS und tvOS hinweg verfügbar,13 aber Focus-getriebene Navigation ist nur auf tvOS das primäre Bedienmodell, weil die Siri Remote weder Zeiger- noch Touch-Ereignisse erzeugt. Auf iPhone oder iPad wird das entsprechende Bedienelement per Tipp getroffen, auf dem Mac wird es überfahren oder geklickt. Die Button-Stile in TVFocusModifier.swift setzen voraus, dass der Fokus die zentrale Bedien-Affordance des Benutzers ist, und gestalten die gesamte visuelle Reaktion darum herum. Es gibt keine gute Möglichkeit, an einer Stelle eine ContentView zu schreiben, die Touch auf iOS, Hover auf dem Mac und Focus-getriebene Navigation auf tvOS gleichermaßen behandelt. Die View-Struktur ist tatsächlich grundlegend anders: Eine tvOS-ContentView ist ein Graph aus fokussierbaren Reihen, eine iOS-ContentView ist ein Tipp-zum-Handeln-Stack.

Dasselbe gilt für den Dauer-Picker. Auf dem iPhone fährt er von unten herein und nimmt Tipps entgegen. Auf dem Apple TV ist es eine horizontale Reihe fokussierbarer Zellen, durch die der Benutzer mit der Fernbedienung navigiert. TVDurationPicker.swift ist eine eigene Datei, weil das zellenbasierte Focus-Design auf dem iPhone keine Entsprechung hat. Sie in eine Datei zu zwingen, hieße, zwei nicht zusammengehörige UIs per #if os(tvOS) aneinanderzukleben.

Der watchOS-Fall: Erweiterte Laufzeit-Sitzungen, HealthKit und eine kleinere Oberfläche

watchOS bringt zwei strukturelle Einschränkungen mit, die die anderen Plattformen nicht haben:

  1. WKExtendedRuntimeSession, um die App reaktionsfähig zu halten, während das Watch-Display abgedunkelt ist.8 Ohne sie suspendiert watchOS die App zwischen jedem Sekunden-Tick aggressiv, und der Timer driftet. Return deklariert WKBackgroundModes: mindfulness in der Info.plist des watchOS-Targets, damit das OS den Anwendungsfall erkennt und das Laufzeitbudget gewährt; die Laufzeit-Sitzung selbst wird mit dem Standard-Initializer WKExtendedRuntimeSession() erzeugt.
  2. iCloud-Synchronisierung über NSUbiquitousKeyValueStore, nicht WatchConnectivity. Returns Sitzungs-Historie-Sync läuft über denselben Key-Value-Store, den auch die iPhone-, iPad- und Mac-Targets verwenden, sodass eine auf der Watch protokollierte Meditation in der Verlaufsansicht des iPhones erscheint, ohne dass eine direkte Watch-zu-Phone-Kommunikation stattfindet. WatchConnectivity könnte künftig eine Option für Live-Zustands-Synchronisierung sein, doch Return entschied sich für das einfachere Modell: Jedes Gerät schreibt in denselben iCloud-KV-Store, der nächste Lesevorgang auf einem beliebigen Gerät sieht die Vereinigung.

WatchTimerManager.swift ist der Watch-seitige Timer; er delegiert die erweiterte Laufzeit-Arbeit an WatchSessionManager, der in ReturnWatchApp.swift als final class WatchSessionManager: NSObject, WKExtendedRuntimeSessionDelegate definiert ist. Der iOS-TimerManager hat kein Pendant, weil iOS-Apps im Vordergrund auch ohne explizite Laufzeit-Sitzung reaktionsfähig bleiben. Würde man die Watch-Logik per #if os(watchOS) in den iOS-TimerManager packen, müsste der iOS-Codepfad WatchKit-Symbole importieren, die er nie verwendet – und der watchOS-Codepfad bräuchte Initialisierungspfade, die der iOS-Pfad nicht hat.

WatchHealthKitManager.swift ist eine kleinere Variante des Haupt-HealthKitManager. Er protokolliert achtsame Minuten auf dieselbe Weise, doch die UX der Autorisierungs-Eingabeaufforderung unterscheidet sich (die Watch kann kein HealthKitPermissionSheet anzeigen). Die Watch-Klasse ist ungefähr halb so groß wie die Hauptklasse.

Was im iOS/iPadOS/macOS-Haupt-Target passiert

Selbst innerhalb des Haupt-Targets ist Sharing nicht automatisch. ContentView.swift enthält zehn #if os(macOS)- bzw. #if !os(macOS)-Blöcke; LiveActivityManager.swift acht; VideoBackgroundView.swift acht; AudioManager.swift sechs. Live Activities sind eine reine iPhone-Funktion, daher ist der gesamte LiveActivityManager in #if os(iOS) gewickelt. Der Dauer-Picker auf dem iPhone verwendet ein anderes Layout als der Dauer-Picker auf iPad und Mac, weshalb ContentView parallele Layout-Zweige hat.

Das Muster, das sich bewährt hat: #if os(...) für kleine Plattform-Deltas (anderes Tastaturverhalten, andere Abstände, fehlende API), separates Target für große strukturelle Deltas (Fokus vs. Touch, Workout-Sitzung vs. Timer). Die Schwelle, bei der ich gelandet bin, lautet „mehr als rund 10 Zeilen Verzweigung”. Darunter ist bedingte Kompilierung in Ordnung. Darüber erledigt die Datei zwei Aufgaben gleichzeitig, und die zweite Aufgabe gehört in ein anderes Target.

Wann man nicht auf allen fünf Plattformen ausliefern sollte

Die ehrliche Einschätzung.

Verzichten Sie auf die Apple Watch, wenn Ihre App informationsdicht ist. Der 46-mm-Bildschirm hat keinen Platz für eine 30-Punkte-Liste, einen Dauer-Picker und eine Einstellungsseite. Return überlebt auf watchOS, weil die Kerninteraktion ein einziger Knopf ist (Timer starten/stoppen). Eine Produktivitäts-App, eine Finanz-App oder eine medienreiche App wird das nicht.

Verzichten Sie auf das Apple TV, wenn Ihre App interaktiv ist. Das TV ist für ambiente Erlebnisse gedacht (Timer, der quer durchs Zimmer auf einem Bildschirm läuft, Musikwiedergabe). Alles, was häufige Eingaben des Benutzers braucht, kämpft gegen die Plattform an. Return ist auf tvOS, weil „einen 20-Minuten-Timer setzen und auf Feuer auf dem Bildschirm schauen” exakt der richtige ambiente Fall ist. Eine Notiz-App wäre dort eine Qual.

Verzichten Sie auf den Mac, wenn Ihre App eine Phone-First-Oberfläche ist. SwiftUI auf dem Mac funktioniert, doch das Push-Modell von NavigationStack wirkt im Vergleich zu einer richtigen Mac-Sidebar wie ein Spielzeug. Wenn die App auf dem Mac unterentwickelt wirken würde, liefern Sie Catalyst aus (das die iPad-App konvertiert) oder lassen Sie den Mac vollständig weg, bis Sie eine Mac-native UI bauen können.

Verzichten Sie auf das iPad, wenn Sie keine Größenklassen-Anpassung gemacht haben. Eine iPhone-App, die auf das iPad gestreckt wird, wirkt billig. Das iPad braucht mindestens eine NavigationSplitView mit Sidebar, idealerweise ein echtes zweispaltiges Layout. Return verwendet auf dem iPad Split-Views und auf dem iPhone Stacks. Der Code liegt im selben Target, doch die UI ist tatsächlich verschieden.

Die Regel, die ich gezogen habe: Liefern Sie auf einer Plattform aus, wenn die Kerninteraktion der App auf das Eingabemodell dieser Plattform abbildet. Liefern Sie einen Meditations-Timer auf der Apple Watch aus (ein Tipp zum Starten). Liefern Sie einen Meditations-Timer auf dem Apple TV aus (einstellen und vergessen). Liefern Sie kein Kanban-Board auf einer der beiden aus.

Was mühelos mitwandert

Die drei Dinge, die in Return tatsächlich über alle fünf Plattformen geteilt wurden:

  1. Das Datenmodell (MeditationSession). Das Struct ist auf jeder Plattform identisch, wird per NSUbiquitousKeyValueStore synchronisiert, und jede Plattform kann lesen, was eine andere geschrieben hat.
  2. Die Sitzungs-Historien-Ansicht (SessionHistoryView). Eine List vergangener Sitzungen rendert auf iPhone, iPad, Mac, Apple Watch und Apple TV identisch. SwiftUIs List ist eines der wenigen Primitive, die sauber auf alle fünf Formfaktoren skalieren.
  3. Der Persistenz-Wrapper (SessionStore). Lese- und Schreibvorgänge sind plattformagnostisch; der zugrunde liegende Speicher (NSUbiquitousKeyValueStore) ist überall dieselbe API.

Drei Konzepte. Zustand, Listen-Rendering und Persistenz. Alles Zustandshaltige und Präsentative, das kein hardware-spezifisches Eingabemodell berührt, ist teilbar. Alles, was Eingabe, Fokus, Audiorouting, Bildschirmgröße oder Hintergrundausführung berührt, ist es nicht.

Dieses Muster taucht auch im iOS-Agent-Development-Guide auf, wo ich dasselbe in anderen Worten argumentiert habe: Die Teile einer iOS-App, die ein Agent schreiben kann, teilen den meisten Code mit den Teilen, die ein Mensch schreibt; die Teile, die menschliches Urteilsvermögen erfordern (Signing, visueller Schliff, Performance), sind exakt die Teile, die sich auch zwischen Plattformen schlecht teilen lassen.9 Die beiden Grenzen fallen zusammen. In beiden Fällen geht es darum, wo Domänenwissen zu zählen beginnt.

Was Multi-Platform kostet

Der ROI ist asymmetrisch. Das Hinzufügen von iPad zu einer iPhone-App kostet vielleicht 20 % mehr Code (Größenklassen-Zweige, an manchen Stellen eine Split-View). Den Mac dem gleichen Target hinzuzufügen, kommt mit weiteren 15–20 % daher (#if os(macOS)-Zweige, Menüleiste, Window-Management). Jedes große Target fügt für eine kleine App rund 10 Dateien hinzu.

Apple Watch und Apple TV sind die teuren Posten. Watch-Support für Return bedeutete 11 neue Dateien in einem separaten Target, einschließlich dedizierter Audio-, Timer- und HealthKit-Manager. Apple TV hinzuzufügen, erforderte 10 neue Dateien in einem weiteren separaten Target, einschließlich Fokus-Verwaltung und einem benutzerdefinierten Dauer-Picker. Zusammen verdoppelten sie die Swift-Oberfläche nahezu – für etwas, das auf der Ebene der Benutzerfunktionen dieselbe App ist.

Die Entscheidung, auf allen fünf Plattformen auszuliefern, war nicht „wir wollen aus Selbstzweck Multi-Platform sein”. Es war eine Reihe einzelner Entscheidungen: Apple Watch, weil Meditations-Timer ehrlich ans Handgelenk gehören; Apple TV, weil das ambiente Bildschirmformat zu langen Sitzungen in einem Raum passt; Mac, weil manche Benutzer am Schreibtisch zwischen Meetings meditieren. Jede Plattform hat ihr Target durch einen echten Anwendungsfall verdient.

Verdient sich eine Funktion ihr Target nicht, ist der billigere Schritt, die Plattform zu überspringen und sich auf die Plattformen zu konzentrieren, auf denen die App exzellent ist.

Was das für Ihre App bedeutet

Drei Erkenntnisse.

  1. Standardmäßig ein Target pro großer Plattformgruppe. iOS + iPadOS + macOS in einem Target funktioniert, weil die Kerninteraktion (Touch + Cursor) ähnlich ist. tvOS in einem separaten Target. watchOS in einem separaten Target. Jedes separate Target kostet rund 10 Dateien, erspart Ihnen aber eine Gott-Klasse mit #if-Zweigen, die ungebremst wuchern.
  2. Aggressiv Zustand teilen, nicht Interaktion. Codable-Modell-Structs, Persistenz-Wrapper und List-Renderings reisen nahezu kostenlos mit. Timer-Manager, Audio-Manager, Content-Views nicht.
  3. Verdienen Sie sich jede Plattform. Liefern Sie nicht auf watchOS aus, weil Sie es können. Liefern Sie aus, wenn die Kerninteraktion Ihrer App auf das Eingabemodell der Plattform abbildet. Lassen Sie den Rest weg.

Dieses Muster funktioniert zusammen mit den drei anderen Oberflächen, über die ich für dieselbe App-Familie geschrieben habe: typisierten App Intents für Apple Intelligence, MCP-Servern für plattformübergreifende LLM-Agenten, Liquid Glass für den Menschen am Gerät. Die äußerste Schicht desselben Stacks ist die Plattform: auf welchen Bildschirmen die App überhaupt läuft. Wählen Sie diese genauso bewusst wie die KI-Oberfläche.

FAQ

Warum kein Swift-Package für geteilten Code?

Habe ich erwogen. Für drei Dateien fügt ein Swift-Package mehr Zeremonie hinzu, als es spart. Apples Xcode-26-Build-System kompiliert eine Quelldatei klaglos in mehrere Targets, sobald Sie die Target-Membership-Häkchen setzen. Ein Package fügt eine eigene Package.swift, ein eigenes Test-Target und einen Indirektionsschritt hinzu, den jede Refaktorierung durchlaufen muss. Für einen kleinen geteilten Kern gewinnt die einfachere Antwort.10

Funktioniert SwiftData auf watchOS und tvOS?

SwiftData ist auf iOS 17+, macOS 14+, watchOS 10+ und tvOS 17+ verfügbar – damit auf jeder Plattform, auf die Return zielt.11 Das MeditationSession-Struct ist schlichtes Codable, kein @Model, weil Return für die Sitzungs-Historien-Synchronisierung NSUbiquitousKeyValueStore statt eines SwiftData-Containers verwendet. Das Muster funktioniert für @Model-Typen genauso: Die Modell-Datei wird geteilt, der Persistenz-Container unterscheidet sich pro Plattform, falls erforderlich.

Sollte ich Mac Catalyst oder ein natives Mac-Target verwenden?

Catalyst ist das richtige Werkzeug, wenn die iPad-App so gut ist, dass eine Catalyst-rebuilte Mac-Variante als nativ wirkt. Das Haupt-Target von Return ist ein echtes Multi-Platform-Target (kein Catalyst), gebaut mit SwiftUI für iOS, iPadOS und macOS in einem Binary. Die Mac-UI verwendet #if os(macOS), um anders zu rendern als das iPad: Sidebar statt Sheet, Tasten-Äquivalente an Buttons usw. Catalyst wäre einfacher gewesen, doch die Mac-UI hätte wie eine iPad-App auf dem Mac ausgesehen – genau die Versagensart, für die Catalyst am bekanntesten ist.

Lohnt sich Apple TV für eine kleine App?

Wahrscheinlich nicht. Apple-TV-Apps haben sehr spezifische Anwendungsfälle (ambient, Medien, lockere Spiele). Wenn Ihre App in keinen davon passt, ist die Zielgruppe zu klein, um die zehn Swift-Dateien pro App zu rechtfertigen. Return zielt gezielt auf tvOS, weil lange Meditations-Sitzungen auf einem Bildschirm quer durch den Raum einer der wenigen produktivitätsnahen Anwendungsfälle ist, der zur Plattform passt.

Wie aufwendig ist das Ausliefern auf allen fünf Plattformen?

Schwer eine präzise Zahl zu nennen; das hängt von der App ab. Return wurde von Tag eins an plattformübergreifend ausgeliefert, statt Plattformen schrittweise hinzuzufügen, was schneller ist als nachträgliches Aufrüsten. Als grobe Faustregel: Ein iPhone-only-MVP plus iPad-Support plus Mac-Support entspricht ungefähr dem 1,5-fachen der iPhone-only-Variante. Apple-Watch-Support kommt mit weiteren 0,5x dazu. Apple-TV-Support nochmal 0,5x. Eine Erstveröffentlichung für fünf Plattformen liegt also bei etwa dem 2,5-fachen des iPhone-only-Aufwands – mit dem Vorbehalt, dass dies ein Agent-gestützter Build war, bei dem der größte Teil des duplizierten Codes von Claude Code massenbearbeitet statt manuell getippt wurde.

Quellenangaben


  1. Return des Autors, eine Meditations-Timer-App, am 21. April 2026 im App Store veröffentlicht. Native Targets: iOS 26+, iPadOS 26+, macOS 26+, watchOS 26+, tvOS 26+. Durchgängig SwiftUI. NSUbiquitousKeyValueStore für die geräteübergreifende Sitzungs-Historie. 

  2. Apple Developer, „Configuring a Multi-Platform App” sowie die Session „SwiftUI essentials” auf der WWDC 2024. Apples Standardempfehlung tendiert zu einem einzigen Target mit umgebungsgesteuerter Anpassung; der Multi-Target-Weg, den dieser Artikel einschlägt, ist eine bewusste Abweichung. 

  3. Produktionscode in Return/Return/Shared/MeditationSession.swift, SessionStore.swift, SessionHistoryView.swift. Der Header-Kommentar in MeditationSession.swift lautet: „Add this file to: Return, ReturnTV, ReturnWatch Watch App targets.” 

  4. Produktionscode in Return/Return/VideoBackgroundView.swift (8 #if os(iOS)-Zweige plus ein #elseif os(macOS)-Zweig), Return/Return/ContentView.swift (10 #if os-Zweige), Return/Return/AudioManager.swift (6 #if os-Zweige), Return/Return/LiveActivityManager.swift (8 #if os-Zweige, Datei ist iOS-only). Zweig-Zählungen via grep -Ec '^\s*#if os\\(' <file>

  5. Apple Developer, „Focus interactions” Human Interface Guidelines. Die tvOS-Focus-Engine ist ein grundsätzlich anderes Navigationsmodell als Touch auf iOS oder Zeiger auf dem Mac. 

  6. Produktionscode in Return/ReturnTV/TVFocusModifier.swift. Definiert zwei ButtonStyle-Typen (TVCapsuleButtonStyle und TVCircleButtonStyle), die @Environment(\.isFocused) umschließen, um Farbe und Transluzenz beim Fokussieren zu invertieren und Skalierung sowie Schatten anzuwenden. 

  7. Apple Developer, „WatchConnectivity”. Das Framework für die gepaarte iPhone-Watch-Kommunikation; Return verwendet es nicht für die Sitzungs-Synchronisierung, sondern stützt sich stattdessen auf den iCloud-Key-Value-Store. 

  8. Apple Developer, „WKExtendedRuntimeSession” und der Info.plist-Schlüssel „WKBackgroundModes”. Der Wert mindfulness ist dokumentiert als: „Enables extended runtime sessions for silent meditation” – passend für einen Meditations-Timer. Return erzeugt eine standardmäßige WKExtendedRuntimeSession() und deklariert WKBackgroundModes: mindfulness in der Info.plist des watchOS-Targets. Produktionscode: Return/ReturnWatch Watch App/ReturnWatchApp.swift definiert WatchSessionManager: NSObject, WKExtendedRuntimeSessionDelegate; WatchTimerManager.swift delegiert die erweiterte Laufzeit-Arbeit dorthin. 

  9. Analyse des Autors in Building iOS Apps with AI Agents, dem Praktikerleitfaden zur Agent-gestützten iOS-Entwicklung über 8 produktive Apps hinweg. 

  10. Apple Developer, „Configuring a Multi-Platform App”. Über Target-Membership lässt sich eine Quelldatei ohne Swift-Package in mehrere Targets kompilieren. Das richtige Werkzeug für kleine geteilte Kerne. 

  11. Apple Developer, „SwiftData” platform availability. Verfügbar auf iOS 17+, iPadOS 17+, macOS 14+, watchOS 10+, tvOS 17+, visionOS 1+ – damit auf allen fünf Apple-Plattformfamilien. 

  12. Apple Developer, „NSUbiquitousKeyValueStore”. Apples iCloud-Key-Value-Store für die Synchronisierung kleiner Zustandsmengen über die Geräte eines Benutzers hinweg. Gesamt-Store-Größe laut Apples veröffentlichten Limits auf 1 MB über alle Schlüssel hinweg gedeckelt. Produktionscode: Return/Return/Shared/SessionStore.swift

  13. Apple Developer, EnvironmentValues.isFocused. Verfügbar auf iOS 14+, iPadOS 14+, macOS 11+, tvOS 14+, watchOS 7+. Die API ist plattformübergreifend; was sich unterscheidet, ist, ob der Fokus die primäre Navigations-Affordance des Benutzers ist. 

Verwandte Beiträge

Die Apple-Plattform-Matrix: Welche Targets verdienen welche App

iOS, iPad, Mac, Watch, Vision, TV. Sechs Plattformen, sechs Verpflichtungen. Die Wahl der Apple-Targets ist eine Produkt…

15 Min. Lesezeit

HealthKit + SwiftUI auf iOS 26: Autorisierung, Sample-Typen und plattformübergreifende Muster aus zwei ausgelieferten Apps

Echte Produktionsmuster aus Water (Wassertracking, HKQuantitySample) und Return (Achtsamkeitssitzungen, HKCategorySample…

13 Min. Lesezeit

Loop Engineering: Schleifen gewinnen dort, wo Verifikation billig ist

Loop Engineering, geprüft an Boris Chernys vollständigen Transkripten: Jede Schleife, die er nennt, hat billige Verifika…

15 Min. Lesezeit