Pięć platform Apple, trzy współdzielone pliki: jak Return rzeczywiście działa wieloplatformowo w SwiftUI
Return, mój timer do medytacji, działa na pięciu platformach Apple: iPhonie, iPadzie, Macu, Apple Watch i Apple TV.1 Baza kodu liczy 40 plików Swift (z wyłączeniem testów). Trzy z nich są współdzielone przez wszystkie pięć platform. Reszta jest podzielona na osobne cele Xcode, które duplikują koncepcje takie jak TimerManager, AudioManager i ContentView, zamiast współdzielić je za pomocą kompilacji warunkowej #if os(...).
Współczynnik współdzielenia wynosi około 7,5% — i jest to działanie zamierzone.
Niniejszy esej dotyczy tego, jak naprawdę wygląda wydanie wieloplatformowej aplikacji SwiftUI w 2026 roku, dlaczego agresywne współdzielenie kodu jest przeceniane oraz co łączy te trzy pliki, które udało się współdzielić.
Pięć platform, na które celuje Return, w sposobie prezentacji Apple na developer.apple.com. Każda z nich to odrębny cel platformowy w Xcode, a nie gałąź wykonywana w czasie działania.
W skrócie
- Return: 18 plików Swift w głównym celu (iOS + iPadOS + macOS), 10 plików w celu tvOS, 7 plików w celu watchOS, 2 pliki widgetów (Live Activities) oraz 3 naprawdę wieloplatformowe pliki w
Return/Shared/. Łącznie 40. - Te trzy współdzielone pliki to elementy związane z trwałym przechowywaniem danych:
MeditationSession,SessionStore,SessionHistoryView. Stan, który podróżuje przez iCloud — nie interfejs, który adaptuje się do platformy. - tvOS i watchOS to osobne cele Xcode, a nie gałęzie
#if os(tvOS)w głównym celu. Modele sterowania są zbyt różne, by zmieścić je w jednym ContentView. - Nawet w obrębie głównego celu iOS/iPadOS/macOS bloki
#if osmnożą się: 10 wContentView.swift, 8 wLiveActivityManager.swift, 8 wVideoBackgroundView.swift, 6 wAudioManager.swift. - Uczciwy wniosek: agresywne współdzielenie kodu na pięciu platformach Apple to obciążenie konserwacyjne. Mały współdzielony rdzeń (warstwa trwałego przechowywania) plus osobne interfejsy specyficzne dla każdej platformy pozwala wydawać szybciej i psuć mniej niż jeden gigantyczny plik naszpikowany
#if.
W roli komplementarnej dla poszczególnych platform proszę zapoznać się z tekstami: macierz platform Apple, kontrakt środowiska wykonawczego watchOS oraz wzorce Liquid Glass w SwiftUI.
Liczby
Kształt bazy kodu — według liczby plików Swift, po odjęciu testów i testów UI:
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
Pięć platform, trzy współdzielone pliki, dwa odrębne cele dla poszczególnych platform plus cel widgetowy, do tego intensywna kompilacja warunkowa wewnątrz głównego celu. Współczynnik współdzielenia to około 7,5%. Większość poradników o „wieloplatformowym SwiftUI” sugeruje coś przeciwnego: napisać jeden ContentView, który adaptuje się do każdej platformy poprzez @Environment(\.horizontalSizeClass) i #if os(...).2 Działa to dla dwóch platform (iPhone + iPad). Załamuje się przy pięciu.
Co łączy te trzy współdzielone pliki
Return/Shared/MeditationSession.swift definiuje typ wartościowy zbliżony do SwiftData: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
}
}
Komentarz nagłówkowy w pliku jest tu kluczowy: // Add this file to: Return, ReturnTV, ReturnWatch Watch App targets. Ten sam plik źródłowy jest dowiązywany do trzech celów Xcode — nie symlinkowany, nie osadzony w pakiecie Swift. System budowania Apple beztrosko kompiluje jeden plik do trzech binariów.
SessionStore.swift to warstwa trwałego przechowywania: cienkie opakowanie wokół NSUbiquitousKeyValueStore (magazynu typu klucz-wartość iCloud od Apple), które odczytuje i zapisuje tablice MeditationSession. Wybór ma znaczenie: synchronizacja przez magazyn KV zapewnia Returnowi historię sesji w skali wielu urządzeń bez konieczności zakładania kontenera CloudKit, kosztem tego, że całość magazynu jest ograniczona do 1 MB.12 Dla listy sesji medytacji o średniej wadze paru setek bajtów każda, ten limit jest aż nadto wystarczający. SessionHistoryView.swift to lista SwiftUI, która renderuje sesje. Oba te pliki są używane identycznie przez cele iPhone, iPad, Mac, Watch i TV.
Co je łączy: opisują stan, a nie interakcję. MeditationSession to to samo pojęcie na każdym urządzeniu. Lista poprzednich sesji czyta się tak samo na każdym urządzeniu. Żaden z tych plików nie ma do czynienia z powierzchnią sterującą, menedżerem okien, decyzją o trasowaniu dźwięku, silnikiem fokusu czy koronką cyfrową. W chwili, w której plik musi wiedzieć, na jakiej platformie się wykonuje, przestaje nadawać się do współdzielenia.
Dlaczego reszty się nie współdzieliło
Weźmy TimerManager. Wersja iOS/iPadOS/macOS używa Timer.publish(every: 1, ...) i kieruje powiadomienia przez UserNotifications. Wersja tvOS (TVTimerManager) obsługuje sytuację, w której użytkownik wstrzymał za pomocą Siri Remote, a wygaszacz ekranu się włącza. Wersja watchOS (WatchTimerManager) deleguje pracę do WKExtendedRuntimeSession (poprzez WatchSessionManager), aby system utrzymywał aplikację w trybie reaktywnym, gdy ekran się przyciemnia, oraz kieruje wejście przez koronkę cyfrową, a nie dotyk. Trzy platformy — trzy głęboko różne zachowania timera.
Można je zunifikować jako class TimerManager { #if os(watchOS) ... #elif os(tvOS) ... }. Wynikiem byłaby klasa o trzech trybach, każdy po czterdzieści linii kodu osadzonego w #if, gdzie tknięcie ścieżki iOS grozi zepsuciem ścieżki watchOS. To koszmar konserwacyjny.
Trzy osobne klasy z trzema nazwami plików to więcej kodu na dysku, a mniej kodu w głowie. Duplikacja, którą można przeczytać, bije abstrakcję, której zrozumieć nie sposób.
Ta sama logika dotyczy:
ContentViewkontraTVContentViewkontraWatchContentView: modele nawigacji są różne (nawigacja stosowa typu push na iPhonie, oparta na fokusie na TV, oparta na liście na Watchu).AudioManagerkontraTVAudioManagerkontraWatchAudioManager: kategorie sesji audio różnią się, watchOS ma surowsze reguły dotyczące audio w tle, tvOS inaczej kieruje sygnał do AirPlay.VideoBackgroundViewma 8 gałęzi#if os(iOS)w głównym celu (z jedną towarzyszącą gałęzią#elseif os(macOS)), obejmujących różne zasoby wideo (fire_phone.mp4kontrafire_mac.mp4), różne typy warstw oraz różne proporcje obrazu.4
Warto odnotować: główny cel Return/ istotnie spaja iOS, iPadOS i macOS w jedno. Te trzy platformy współdzielą więcej kodu, niż się różnią. NavigationStack z SwiftUI działa na wszystkich trzech. .glassEffect() działa na wszystkich trzech. Różnice w zarządzaniu oknami są realne, lecz dające się ogarnąć w obrębie jednego celu. To tvOS i watchOS były tymi miejscami, w których wytyczyłem linię osobnego celu.
Przypadek tvOS: dlaczego silnik fokusu wymusił osobny cel
Nawigacja na Apple TV opiera się na silniku fokusu.5 Każdy element interfejsu, z którym użytkownik może wejść w interakcję, deklaruje się jako fokusowalny; systemowe strzałki na Siri Remote przesuwają fokus między elementami; naciśnięcie „select” aktywuje element posiadający fokus. SwiftUI na tvOS udostępnia ten mechanizm poprzez .focusable(), .focusEffect oraz niestandardowe typy ButtonStyle, które reagują na @Environment(\.isFocused) w celu uzyskania efektu paralaksy-pochylenia, jakiego używają natywne aplikacje Apple. Produkcyjny kod z 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)
}
}
Ten sam plik definiuje również TVCircleButtonStyle dla kontrolek kwadratowych/okrągłych. Oba style odwracają kolor i półprzezroczystość po uzyskaniu fokusu: niefokusowane przyciski siedzą na .ultraThinMaterial, sfokusowane wypełniają się kolorem akcentu i podbijają skalę oraz cień. Wzorzec jest dla tej aplikacji strukturalnie specyficzny dla tvOS. @Environment(\.isFocused) jest dostępne w iOS, iPadOS, macOS, watchOS i tvOS,13 lecz nawigacja sterowana fokusem stanowi podstawowy model interakcji wyłącznie na tvOS, gdzie Siri Remote nie generuje żadnego zdarzenia kursora ani dotyku. Na iPhonie czy iPadzie odpowiednia kontrolka jest trafiana stuknięciem; na Macu — najechaniem lub kliknięciem. Style przycisków w TVFocusModifier.swift zakładają, że fokus jest głównym sposobem oddziaływania użytkownika, i projektują wokół niego cały odzew wizualny. Nie istnieje dobry sposób, by w jednym miejscu napisać ContentView, który obsługuje dotyk na iOS, najechanie na Macu i nawigację sterowaną fokusem na tvOS. Struktura widoku jest realnie różna: ContentView w tvOS to graf fokusowalnych wierszy, ContentView w iOS to stos „stuknij-aby-zadziałać”.
To samo dotyczy wyboru długości sesji. Na iPhonie wjeżdża od dołu i przyjmuje stuknięcia. Na Apple TV jest poziomym rzędem fokusowalnych komórek, po których użytkownik nawiguje pilotem. TVDurationPicker.swift jest osobnym plikiem, ponieważ projekt oparty na fokusowalnych komórkach nie ma na iPhonie żadnego analogu. Wmuszanie ich w jeden plik oznaczałoby dwa niespokrewnione interfejsy zlepione ze sobą za pomocą #if os(tvOS).
Przypadek watchOS: rozszerzone sesje wykonawcze, HealthKit i mniejsza powierzchnia
watchOS nakłada dwa strukturalne ograniczenia, których pozostałe platformy nie mają:
WKExtendedRuntimeSessionw celu utrzymania aplikacji w trybie reaktywnym, gdy ekran zegarka jest przyciemniony.8 Bez tego watchOS agresywnie wstrzymuje aplikację między każdym tyknięciem sekundy i timer zaczyna dryfować. Return deklarujeWKBackgroundModes: mindfulnessw plikuInfo.plistcelu watchOS, by system rozpoznał ten przypadek użycia i przyznał budżet czasu wykonania; sama sesja wykonawcza tworzona jest domyślnym inicjalizatoremWKExtendedRuntimeSession().- Synchronizacja iCloud przez
NSUbiquitousKeyValueStore, a nie WatchConnectivity. Synchronizacja historii sesji w Returnie korzysta z tego samego magazynu klucz-wartość, którego używają cele iPhone, iPad i Mac — dzięki czemu medytacja zalogowana na zegarku pojawia się w widoku historii na iPhonie bez żadnej bezpośredniej komunikacji watch-to-phone. WatchConnectivity mogłoby być przyszłą opcją do synchronizacji stanu na żywo, lecz Return wybrał prostszy model: każde urządzenie zapisuje do tego samego magazynu KV w iCloud, kolejny odczyt na dowolnym urządzeniu widzi sumę.
WatchTimerManager.swift to timer po stronie zegarka; deleguje pracę związaną z rozszerzonym czasem wykonania do WatchSessionManager, zdefiniowanego w ReturnWatchApp.swift jako final class WatchSessionManager: NSObject, WKExtendedRuntimeSessionDelegate. TimerManager w iOS nie ma odpowiednika, ponieważ aplikacje iOS pozostają reaktywne na pierwszym planie bez jawnej sesji wykonawczej. Wciśnięcie logiki zegarka do TimerManager w iOS poprzez #if os(watchOS) oznaczałoby, że ścieżka kodu iOS importuje symbole WatchKit, których nigdy nie używa, a ścieżka kodu watchOS wymaga ścieżek inicjalizacyjnych, których ścieżka iOS nie posiada.
WatchHealthKitManager.swift to mniejszy wariant głównego HealthKitManager. Loguje minuty uważności w ten sam sposób, lecz UX prośby o autoryzację jest inny (zegarek nie może wyświetlić HealthKitPermissionSheet). Klasa Watch jest mniej więcej o połowę mniejsza od głównej.
Co dzieje się wewnątrz głównego celu iOS/iPadOS/macOS
Nawet w obrębie głównego celu współdzielenie nie jest automatyczne. ContentView.swift ma dziesięć bloków #if os(macOS) lub #if !os(macOS); LiveActivityManager.swift ma osiem; VideoBackgroundView.swift ma osiem; AudioManager.swift ma sześć. Live Activities to funkcja wyłącznie iPhone’owa, więc cały LiveActivityManager opakowany jest w #if os(iOS). Wybór długości sesji na iPhonie używa innego układu niż na iPadzie i Macu, więc ContentView ma równoległe gałęzie układu.
Wzorzec, który się sprawdził: #if os(...) dla małych delt platformowych (różne zachowanie klawiatury, różne dopełnienia, brak API), osobny cel dla dużych delt strukturalnych (fokus kontra dotyk, sesja treningowa kontra timer). Próg, do którego doszedłem, brzmi: „więcej niż około 10 linii rozgałęzień”. Poniżej tego — kompilacja warunkowa jest w porządku. Powyżej — plik wykonuje dwie roboty naraz, a druga z nich należy do innego celu.
Kiedy nie wydawać na wszystkich pięciu platformach
Uczciwa ocena.
Pominąć Apple Watch, jeśli aplikacja jest gęsta informacyjnie. Ekran 46 mm nie ma miejsca na listę 30 elementów, wybór długości sesji i widok ustawień. Return przeżywa na watchOS, ponieważ rdzeniem interakcji jest jeden przycisk (start/stop timera). Aplikacja produktywnościowa, finansowa czy bogata w media tego nie udźwignie.
Pominąć Apple TV, jeśli aplikacja jest interaktywna. Telewizor jest dla doświadczeń otaczających (timer odliczający na ekranie po drugiej stronie pokoju, odtwarzanie muzyki). Cokolwiek, co wymaga częstego wkładu od użytkownika, walczy z platformą. Return jest na tvOS, bo „ustaw 20-minutowy timer i patrz na ogień na ekranie” to dokładnie właściwy przypadek otaczający. Aplikacja do notatek byłaby udręką.
Pominąć Maca, jeśli aplikacja ma interfejs zorientowany na telefon. SwiftUI na Macu działa, lecz model nawigacji push w NavigationStack czyta się jak zabawka w porównaniu z prawdziwym pasem bocznym Maca. Jeśli aplikacja sprawiałaby na Macu wrażenie niedopracowanej, lepiej wydać Catalyst (który konwertuje aplikację iPad) lub odpuścić Maca w ogóle, dopóki nie da się zbudować natywnego dla Maca interfejsu.
Pominąć iPada, jeśli nie wykonano adaptacji do klas rozmiaru. Aplikacja iPhone’owa rozciągnięta tak, by wypełnić iPada, czyta się jako tania. iPad potrzebuje co najmniej NavigationSplitView z paskiem bocznym; idealnie — prawdziwego układu dwupanelowego. Return używa widoków podzielonych na iPadzie i stosów na iPhonie. Kod jest w tym samym celu, lecz interfejs jest realnie inny.
Reguła, którą sobie wytyczyłem: wydawać aplikację na danej platformie, gdy rdzeń jej interakcji odwzorowuje się na model wejścia tej platformy. Wydać timer do medytacji na Apple Watch (jedno stuknięcie, by zacząć). Wydać timer do medytacji na Apple TV (ustaw i zapomnij). Nie wydawać tablicy kanban na żadnej z nich.
Co podróżuje bez wysiłku
Trzy rzeczy, które rzeczywiście udało się współdzielić na wszystkich pięciu platformach w Returnie:
- Model danych (
MeditationSession). Struktura jest identyczna na każdej platformie, jest synchronizowana przezNSUbiquitousKeyValueStore, a każda platforma może czytać to, co zapisała którakolwiek inna. - Widok historii sesji (
SessionHistoryView).Listz poprzednimi sesjami renderuje się identycznie na iPhonie, iPadzie, Macu, Apple Watch i Apple TV.Listw SwiftUI to jeden z nielicznych prymitywów, który czysto adaptuje się do wszystkich pięciu form. - Opakowanie persystencji (
SessionStore). Odczyty i zapisy są agnostyczne platformowo; magazyn pod spodem (NSUbiquitousKeyValueStore) jest wszędzie tym samym API.
Trzy koncepcje. Stan, renderowanie listy i trwałe przechowywanie. Wszystko, co stanowe i prezentacyjne, a nie wikła się w specyficzny dla sprzętu model wejścia, daje się współdzielić. Wszystko, co dotyka wejścia, fokusu, trasowania dźwięku, rozmiaru ekranu czy wykonywania w tle — nie.
Ten wzorzec pojawia się w przewodniku iOS Agent Development, w którym argumentowałem to samo innymi słowami: te części aplikacji iOS, które agent może napisać, w większości współdzielą kod z częściami, które pisze człowiek; te, które wymagają osądu człowieka (podpisywanie, polerowanie wizualne, wydajność) to dokładnie te, które również nie współdzielą się dobrze między platformami.9 Obie te granice się pokrywają. Obie dotyczą tego, gdzie zaczyna się liczyć wiedza dziedzinowa.
Ile kosztuje wieloplatformowość
ROI jest asymetryczny. Dodanie iPada do aplikacji iPhone’owej kosztuje może 20% więcej kodu (gałęzie klasy rozmiaru, gdzieniegdzie widok podzielony). Dodanie Maca do tego samego celu dorzuca kolejne 15-20% (gałęzie #if os(macOS), pasek menu, zarządzanie oknami). Każdy duży cel dodaje około 10 plików w przypadku małej aplikacji.
Apple Watch i Apple TV są tymi drogimi. Dodanie watchOS do Returna wymagało 11 nowych plików w osobnym celu, włącznie z dedykowanymi menedżerami audio, timera i HealthKitu. Dodanie tvOS wymagało 10 nowych plików w kolejnym osobnym celu, włącznie z zarządzaniem fokusem i niestandardowym wyborem długości sesji. Razem niemal podwoiły powierzchnię kodu Swift dla czegoś, co na poziomie funkcji użytkownika jest tą samą aplikacją.
Decyzja o wydaniu na wszystkich pięciu nie brzmiała: „chcemy być wieloplatformowi dla samej zasady”. Była serią osobnych decyzji: Apple Watch, bo timery do medytacji autentycznie należą się nadgarstkowi; Apple TV, bo ambientowy format ekranu pasuje do długich sesji w pokoju; Mac, bo niektórzy użytkownicy medytują przy biurku między spotkaniami. Każda z platform zapracowała na swój cel poprzez realny przypadek użycia.
Jeśli funkcja nie zarobi sobie na swój cel, tańszym ruchem jest odpuszczenie platformy i podwojenie wysiłków na tych, na których aplikacja jest znakomita.
Co to oznacza dla państwa aplikacji
Trzy wnioski.
- Domyślnie jeden cel na większą grupę platform. iOS + iPadOS + macOS w jednym celu działa, ponieważ rdzeń interakcji (dotyk + kursor) jest podobny. tvOS w osobnym celu. watchOS w osobnym celu. Każdy osobny cel kosztuje około 10 plików, lecz oszczędza nam jednej Boskiej Klasy z gałęziami
#if, które rosną bez ograniczeń. - Agresywnie współdzielić stan, nie interakcję. Strukty modeli z
Codable, opakowania persystencji oraz renderowaniaListpodróżują niemal za darmo. Menedżery timera, menedżery audio, widoki treści — nie. - Niech każda platforma sobie zarobi. Nie wydawać na watchOS dlatego, że można. Wydawać, gdy rdzeń interakcji aplikacji odwzorowuje się na model wejścia platformy. Resztę pominąć.
Ten wzorzec działa równolegle do trzech innych powierzchni, o których pisałem dla tej samej rodziny aplikacji: typowanych App Intents dla Apple Intelligence, serwerów MCP dla agentów dzielonych pomiędzy LLM, Liquid Glass dla człowieka przy urządzeniu. Najbardziej zewnętrzną warstwą tego samego stosu jest platforma — to, na jakich ekranach aplikacja w ogóle się uruchamia. Tę decyzję proszę podejmować równie rozważnie, jak wybór powierzchni AI.
FAQ
Dlaczego nie użyć pakietu Swift na współdzielony kod?
Rozważałem to. Dla trzech plików pakiet Swift dodaje więcej ceremoniału, niż oszczędza. System budowania Xcode 26 firmy Apple beztrosko kompiluje jeden plik źródłowy do wielu celów, gdy zaznaczy się odpowiednie pola Target Membership. Pakiet dodaje osobny Package.swift, osobny cel testowy i krok pośredni, przez który każda refaktoryzacja musi przebrnąć. Dla małego współdzielonego rdzenia wygrywa odpowiedź prostsza.10
Czy SwiftData działa na watchOS i tvOS?
SwiftData jest dostępne na iOS 17+, macOS 14+, watchOS 10+ i tvOS 17+, obejmując każdą platformę, na którą celuje Return.11 Struktura MeditationSession jest zwykłym Codable, a nie @Model, ponieważ Return korzysta z NSUbiquitousKeyValueStore do synchronizacji historii sesji, a nie z kontenera SwiftData. Wzorzec działa tak samo dla typów @Model: plik modelu jest współdzielony, kontener persystencji różni się dla każdej platformy, jeśli musi.
Powinienem użyć Mac Catalyst czy natywnego celu Mac?
Catalyst jest właściwym narzędziem, gdy aplikacja iPad jest wystarczająco dobra, by jej macowa wersja przebudowana w Catalyst czytała się jako natywna. Główny cel Returna to prawdziwy cel wieloplatformowy (nie Catalyst), zbudowany w SwiftUI dla iOS, iPadOS i macOS w jednym binarium. Macowy interfejs używa #if os(macOS), by renderować się inaczej niż iPad: pasek boczny zamiast arkusza, ekwiwalenty klawiszowe na przyciskach itd. Catalyst byłby prostszy, lecz interfejs Maca wyglądałby jak aplikacja iPad na Macu — co jest najbardziej rozpoznawalnym trybem porażki Catalyst.
Czy warto wydawać małą aplikację na Apple TV?
Prawdopodobnie nie. Aplikacje na Apple TV mają bardzo specyficzne przypadki użycia (otoczenie, media, lekkie gry). Jeśli dana aplikacja nie pasuje do żadnego z nich, baza użytkowników platformy jest zbyt mała, by uzasadniać 10 plików Swift na aplikację. Return celuje w tvOS właśnie dlatego, że długie sesje medytacji na ekranie po drugiej stronie pokoju to jeden z nielicznych przypadków zbliżonych do produktywności, który pasuje do platformy.
Ile zajmuje wydanie na wszystkich pięciu platformach?
Trudno o precyzyjną liczbę; zależy to od aplikacji. Return wydał wieloplatformowo od pierwszego dnia, zamiast dodawać platformy stopniowo, co jest szybsze niż dorabianie ich post factum. Zasada kciuka: aplikacja MVP wyłącznie na iPhone’a plus wsparcie iPada plus wsparcie Maca to mniej więcej 1,5x czasu wersji iPhone-only. Dodanie Apple Watch to kolejne 0,5x. Dodanie Apple TV — kolejne 0,5x. Czyli pierwsze wydanie pięcioplatformowe to mniej więcej 2,5x wysiłku iPhone-only, z zastrzeżeniem, że było to budowanie wspomagane agentami, w którym większość zduplikowanego kodu została masowo poedytowana przez Claude Code, a nie wpisana ręcznie.
Bibliografia
-
Autorski Return, aplikacja-timer do medytacji opublikowana w App Store 21 kwietnia 2026 roku. Cele natywne: iOS 26+, iPadOS 26+, macOS 26+, watchOS 26+, tvOS 26+. SwiftUI w całości.
NSUbiquitousKeyValueStoredla historii sesji w skali wielu urządzeń. ↩ -
Apple Developer, „Configuring a Multi-Platform App” oraz sesja „SwiftUI essentials” na WWDC 2024. Domyślne wytyczne Apple skłaniają się ku jednemu celowi z adaptacją sterowaną środowiskiem; obrana w niniejszym artykule droga wielu celów to świadome odejście od tej rekomendacji. ↩
-
Kod produkcyjny w
Return/Return/Shared/MeditationSession.swift,SessionStore.swift,SessionHistoryView.swift. Komentarz nagłówkowy wMeditationSession.swiftbrzmi: „Add this file to: Return, ReturnTV, ReturnWatch Watch App targets.” ↩ -
Kod produkcyjny w
Return/Return/VideoBackgroundView.swift(8 gałęzi#if os(iOS)plus jedna gałąź#elseif os(macOS)),Return/Return/ContentView.swift(10 gałęzi#if os),Return/Return/AudioManager.swift(6 gałęzi#if os),Return/Return/LiveActivityManager.swift(8 gałęzi#if os, plik wyłącznie iOS). Liczby gałęzi zostały uzyskane przez uruchomieniegrep -Ec '^\s*#if os\\(' <file>. ↩ -
Apple Developer, „Focus interactions” Human Interface Guidelines. Silnik fokusu tvOS to fundamentalnie odmienny model nawigacji od dotyku w iOS czy kursora w Macu. ↩
-
Kod produkcyjny w
Return/ReturnTV/TVFocusModifier.swift. Definiuje dwa typyButtonStyle(TVCapsuleButtonStyleiTVCircleButtonStyle), które owijają@Environment(\.isFocused), by odwracać kolor i półprzezroczystość po fokusie oraz aplikować skalę i cień. ↩ -
Apple Developer, „WatchConnectivity”. Framework do komunikacji między sparowanymi iPhonem a Watchem; Return go nie używa do synchronizacji sesji, opierając się zamiast tego na magazynie klucz-wartość iCloud. ↩
-
Apple Developer, „WKExtendedRuntimeSession” oraz klucz „WKBackgroundModes” w pliku Info.plist. Wartość
mindfulnessjest dokumentowana jako: „Enables extended runtime sessions for silent meditation” — właściwe dopasowanie do timera medytacji. Return tworzy domyślnąWKExtendedRuntimeSession()i deklarujeWKBackgroundModes: mindfulnessw plikuInfo.plistcelu watchOS. Kod produkcyjny:Return/ReturnWatch Watch App/ReturnWatchApp.swiftdefiniujeWatchSessionManager: NSObject, WKExtendedRuntimeSessionDelegate;WatchTimerManager.swiftdeleguje do niego pracę nad rozszerzonym czasem wykonania. ↩ -
Autorska analiza w Building iOS Apps with AI Agents, praktyczny przewodnik po wspomaganym agentami rozwoju iOS w skali 8 produkcyjnych aplikacji. ↩
-
Apple Developer, „Configuring a Multi-Platform App”. Target membership pozwala jednemu plikowi źródłowemu kompilować się do wielu celów bez pakietu Swift. Właściwe narzędzie dla małych współdzielonych rdzeni. ↩
-
Apple Developer, „SwiftData” platform availability. Dostępne na iOS 17+, iPadOS 17+, macOS 14+, watchOS 10+, tvOS 17+, visionOS 1+, obejmując wszystkie pięć rodzin platform Apple. ↩
-
Apple Developer, „NSUbiquitousKeyValueStore”. Magazyn klucz-wartość iCloud Apple do synchronizacji niewielkich ilości stanu między urządzeniami użytkownika. Łączny rozmiar magazynu ograniczony do 1 MB w sumie wszystkich kluczy zgodnie z opublikowanymi limitami Apple. Kod produkcyjny:
Return/Return/Shared/SessionStore.swift. ↩ -
Apple Developer,
EnvironmentValues.isFocused. Dostępne na iOS 14+, iPadOS 14+, macOS 11+, tvOS 14+, watchOS 7+. API jest wieloplatformowe; różni się to, czy fokus stanowi główny sposób oddziaływania użytkownika na nawigację. ↩