Return...
Zenowy minutnik do medytacji i skupienia na pięciu ekranach: iPhone, iPad, Apple Watch, Apple TV i Mac.
Wydany 21 kwietnia 2026. Jedna baza kodu. Dwadzieścia siedem języków, w tym arabski i hebrajski. Cztery motywy, trzy dzwonki, zero analityki. Poniżej opis, jak to wszystko powstało: wybory techniczne, kompromisy projektowe i długi, cichy proces sprowadzania setek wygenerowanych przez AI kropel wody do jednej.
Jedna baza kodu, pięć ekranów.
Return to pierwsza aplikacja, jaką wypuściłem, która działa na każdej klasie ekranu Apple z jednego projektu Xcode: iPhone, iPad, Apple Watch, Apple TV i Mac. Pięćdziesiąt siedem plików Swift, około 12 700 linii kodu i zero zewnętrznych zależności. Czyste SwiftUI, AVFoundation, HealthKit, ActivityKit i WidgetKit.
Naiwne podejście to jeden uniwersalny TimerManager z gałęziami #if na każdą różnicę platformową. Nie zrobiłem tego. Return zawiera trzy klasy minutnika (TimerManager na iOS i macOS, TVTimerManager na tvOS, WatchTimerManager na watchOS), które dzielą semantykę stanu, ale szanują to, w czym każda platforma jest naprawdę dobra. Live Activities tylko na iOS. HealthKit tylko tam, gdzie istnieje API. Rozszerzone sesje runtime tylko na zegarku. Każdy manager jest krótszy i bardziej uczciwy niż jedna polimorficzna klasa.
Wspólne tam, gdzie to ma znaczenie.
Jeden folder Shared/ zawiera elementy, co do których wszystkie targety muszą się zgadzać: model danych MeditationSession, opakowanie iCloud SessionStore i SessionHistoryView. Ustawienia synchronizują się między zegarkiem a telefonem przez App Group (group.com.941apps.Return). Reszta jest celowo specyficzna dla platformy.
Najwyraźniejszym przykładem jest jedna linijka, która decyduje, czy sesja została już zapisana do HealthKit. iPhone zapisuje bezpośrednio, więc „zsynchronizowane” jest prawdą w momencie zakończenia sesji. Mac i TV w ogóle nie mogą pisać do HealthKit, więc „zsynchronizowane” jest fałszem do czasu, aż iPhone później odbierze oczekującą sesję. Ta sama intencja, odwrotna wartość logiczna, jeden #if:
/// Save session to SessionStore for cross-device sync and HealthKit syncing private func saveSessionToStore(startTime: Date, endTime: Date) { // On iOS: if healthKitEnabled, we save directly to HealthKit, so mark as synced // On Mac: if healthKitEnabled, we want to sync to iPhone, so mark as NOT synced #if os(iOS) let alreadySynced = settings.healthKitEnabled #else let alreadySynced = !settings.healthKitEnabled #endif let session = MeditationSession( startDate: startTime, endDate: endTime, sourceDevice: .current, syncedToHealthKit: alreadySynced ) SessionStore.shared.addSession(session) }
Wracam do tego wzorca nieustannie: najmniejsza liczba linii, która nadal czyni intencję czytelną. Gdy ta sama wartość logiczna znaczy coś innego na różnych platformach, zapisz ją jako różne wartości logiczne. #if staje się częścią dokumentacji.
Dwadzieścia siedem języków i wsparcie dla pisma od prawej do lewej.
Return to pierwsza aplikacja Apple, jaką wypuściłem w każdym języku, na którym mi zależało. Dwadzieścia siedem lokalizacji przeszło pełny przegląd, w tym arabski i hebrajski. Wszystko to mieści się w jednym pliku Localizable.xcstrings, co brzmi mniej bohatersko, niż się wydaje. Xcode wykonuje większość pracy, jeśli zgodzisz się przestać ręcznie klepać stringi.





RTL to darmowa wygrana, jeśli przestaniesz z nim walczyć.
SwiftUI traktuje .leading i .trailing jako kierunki semantyczne, a nie .left i .right jako stałe. Rozplanuj ekran raz w kierunkach semantycznych, a ten sam ekran automatycznie lustrzanie się odwróci w arabskim, hebrajskim, perskim czy urdu, bez dedykowanej ścieżki kodu. Etykiety ustawień się odwracają, strzałka powrotu zmienia kierunek, pozycje przełączników się zamieniają. Ikony motywów (kropla, płomień, liść) zostają na miejscu. Nie napisałem ani jednej linii kodu RTL dla tego zachowania.
Jeden wyjątek, który wyłapałem tuż przed wydaniem: SwiftUI stosuje kierunek układu również do widoków Text, co oznaczało, że pierwsza wersja zrzutów ekranu po arabsku i hebrajsku miała minutnik wyświetlający „00:02” zamiast „20:00” — cyfry łacińskie ułożone od prawej do lewej. Jeden modyfikator .environment(\.layoutDirection, .leftToRight) na każdym widoku Text zawierającym czas lub liczby to naprawia. Zrzuty powyżej pochodzą z wydania, w którym ten modyfikator jest już na miejscu.
Zestaw zrzutów ekranu został wygenerowany przez fastlane uruchamiający te same testy UI z różnymi argumentami -AppleLanguages. Własny wzorzec aplikacji effectiveLocale odczytuje flagę, odbudowuje hierarchię widoków i zapisuje wynik. Jeden helper, dwadzieścia siedem lokalizacji, cztery klasy urządzeń — wszystko w jednym nocnym przebiegu.
/// The locale to use for the app - either user-selected or system default /// In snapshot mode, always use system language (set by -AppleLanguages) /// to allow screenshot generation for different locales private var effectiveLocale: Locale { if isSnapshotMode || appLanguage.isEmpty { if let preferredLanguage = Locale.preferredLanguages.first { return Locale(identifier: preferredLanguage) } return .current } return Locale(identifier: appLanguage) } var body: some Scene { WindowGroup { WatchContentView() .preferredColorScheme(.dark) .environment(\.locale, effectiveLocale) .id(appLanguage) // Force rebuild when locale changes } }
.id(appLanguage) to szczegół, który zarabia na swoje utrzymanie. Bez niego SwiftUI cache'uje starą hierarchię widoków i stringi się nie odświeżają, gdy przełączasz języki w czasie działania. Z nim całe drzewo jest odrzucane i odbudowywane, a wszystko automatycznie ponownie odczytuje swoje zlokalizowane stringi. Jedna linia, skasowana cała kategoria błędów.
Uważne minuty, nareszcie.
Natywna aplikacja Apple Mindfulness na Watch ogranicza wbudowane sesje Reflect i Breathe do pięciu minut. Samo API HealthKit nie ma takiego ograniczenia. Z radością przyjmie każdą próbkę HKCategorySample, w której data końcowa jest późniejsza niż początkowa. Limit tkwi w UI, nie w systemie. Return umieszcza wybierak od 5 do 60 minut na każdym urządzeniu i zapisuje to, co faktycznie przesiedziałeś.
/// Save a mindful session with the given start and end time func saveMindfulSession(start: Date, end: Date) async -> Bool { guard isAvailable else { return false } // Don't save if end is before or equal to start guard end > start else { return false } let sample = HKCategorySample( type: mindfulType, value: HKCategoryValue.notApplicable.rawValue, start: start, end: end ) ... }
Jedyna walidacja to end > start. To wszystko, co sam HealthKit waliduje. API Apple zawsze było gotowe zapisać czterdziestopięciominutową medytację. Brakowało tylko przycisku, żeby o nią poprosić.
Międzyurządzeniowość bez HealthKit na trzech z nich.
Mac i Apple TV w ogóle nie mają HealthKit. Oczywistą reakcją jest „to nie zawracaj sobie głowy logowaniem sesji tam”. Mniej oczywistą, poprawną reakcją jest logować je mimo wszystko, do iCloud Key-Value Store, i pozwolić telefonowi je odebrać, gdy następnym razem się obudzi. SessionStore w Return to wspólny magazyn, MeditationSession.syncedToHealthKit to flaga oczekująca, a HealthKitManager.syncPendingSessions() uruchamia się za każdym razem, gdy aplikacja iOS wraca na pierwszy plan.
iCloud Key-Value Store
To element, który moim zdaniem Apple powinno dostarczyć samodzielnie: porządny międzyplatformowy zapis Uważnych Minut, który nie wymaga aktywnego telefonu, gdy chcesz pomedytować na Macu. Dopóki tego nie zrobią, robi to Return.
Skąd wzięła się woda.
Cztery motywy. Cztery pętle dźwięków otoczenia. Trzy dzwonki. Wszystko wygenerowane, większość wyrzucona. Filmy to Midjourney, dźwięk to ElevenLabs, a praca, która się liczyła, nie polegała na promptowaniu. Polegała na edycji. Patrzenie na siatkę dwustu kropel wody i wybieranie tej, która zapętla się czysto, bez widocznego szwu. Słuchanie czterdziestu wariantów dzwonu świątynnego, aż jeden ma właściwe uderzenie i właściwe wybrzmienie, i nie brzmi jak powiadomienie z telefonu.




Każdy kafelek to generacja. Serca to te, które przetrwały pierwszą selekcję. Trójkąty odtwarzania to te, które zabrałem do filmu. Wydane zostały cztery motywy. Cała reszta pozostała w siatce — i o to właśnie chodzi w tym procesie: liczy się proporcja.
Dzwonki poszły tym samym łukiem w audio. Prompt, słuchaj, doszlifuj, zapromptuj jeszcze raz. Zostawiłem trzy: Singing Bowl, Temple Bell, Soft Chime. Każdy iterowany, aż przestał brzmieć syntetycznie.
Nie będę udawał, że liczę łączną liczbę generacji. Setki na motyw to uczciwa odpowiedź. Dyscyplina nie leży w promptach. Leży w wyrzucaniu wszystkiego, co jest jedynie dobre, i zachowywaniu tylko tych, które mogą siedzieć za minutnikiem przez dwadzieścia cichych minut, nigdy nie stając się tym, co zauważasz.
Dlaczego minutnik, a nie nauczyciel.
Ta część jest osobista. Zbudowałem Return, bo już mam praktykę medytacyjną i nie mogłem znaleźć minutnika, który by schodził z drogi. Z czym siedzę, to japoński Zen w jego nurcie wojowników: Takuan, Yagyu, Musashi, Dogen, Hakuin. Nie terapeutyczna uważność, którą sprzedają wielkie aplikacje. Inna intencja, inna tekstura.
Co rotuje w typowym tygodniu:
- Susokukan (liczenie oddechów). Licz od jednego do dziesięciu na oddechu, wracaj do jedynki za każdym razem, gdy gubisz rachubę. Fundament. Koncentracja, joriki, najpierw.
- Shikantaza (po prostu siedzenie). Bez obiektu. Bez liczenia, bez pytania, bez wizualizacji. Umysł, który się nie fiksuje. Centralna forma zazen Dogena i najbliższe formalne przybliżenie stanu, którego faktycznie chcę.
- Koan. Głównie Mu Joshu. Pytanie, którego nie da się rozwiązać myśleniem, trzymane aż myślenie się podda.
- Maranasati (kontemplacja śmierci). W oprawie Hagakure. Używana oszczędnie. Przetrwanie zaostrza umysł; to przecina go na wylot.
- Isshin (jeden umysł). Terytorium Takuana i Yagyu: rozluźniony, lecz zaangażowany, osadzony, lecz mobilny. Most między poduszką a tym, co nadejdzie później.
- Dni integracji. Wdzięczność, współczucie, linia przekazu. Jihi. Katsujinken: miecz dający życie, a nie miecz zabijający. Zwykle soboty.
- Sakki (świadomość wrogich intencji). Pięć minut otwartego nasłuchiwania dołączone do każdej sesji. Zabiera shikantaza z poduszki i poddaje ją próbie ciśnieniowej w zwykłych środowiskach.
Rotacja nie jest sztywna. Liczenie oddechów, gdy potrzebuję się ustabilizować. Koan, gdy potrzebuję się przebić. Shikantaza, gdy potrzebuję odpocząć w otwartości. Kontemplacja śmierci, gdy stawki wymagają wyjaśnienia. Różnorodność należy do treningu.
Return jest minutnikiem, bo nie potrzebuję nauczyciela na telefonie. Potrzebuję czegoś, co pilnuje zegara, żebym nie musiał ja, oznacza początek i koniec dzwonkiem, który szanuję, a pomiędzy schodzi z drogi. Jeśli masz już praktykę, to prawdopodobnie też tego chcesz. Jeśli jesteś zupełnie nowy, znajdź nauczyciela w pokoju. Potem wróć.
Czego nie ma w Return.
Return nie jest Calm. Nie jest Headspace. Nie ma brytyjskiego lektora wprowadzającego cię delikatnie w skanowanie ciała. Nie ma kreskówkowego awatara świętującego twoją serię. Nie ma subskrypcji odblokowującej nowe programy prowadzone. Return jest minutnikiem. Idea jest taka, że jeśli masz już praktykę, nie potrzebujesz nauczyciela w aplikacji. Potrzebujesz narzędzia, które pilnuje czasu i schodzi z drogi.
- Bez głosu prowadzącego ani narracji
- Bez serii, punktów ani grywalizacji
- Bez subskrypcji ani zakupów wewnątrz aplikacji
- Bez reklam, nigdy
- Bez analityki; aplikacja niczego nie śledzi
- Bez logowania społecznościowego ani udostępniania
- Bez ekranów nagabujących, bez modalek startowych
- Bez ciemnych wzorców w ścieżce IAP, bo nie ma ścieżki IAP
Co jest w Return, celowo małe: cztery tryby powtarzania (Raz, Do zatrzymania, Do czasu, Powtórz N razy), dwusekundowa przerwa na oddech między cyklami, od jednego do trzech uderzeń dzwonka przy każdym przejściu, wybór trzech dzwonków, cztery motywy, opcja włączenia HealthKit i wybierak języka. To cały produkt.
Koszt takiej surowości widać w modelu ustawień. Każda preferencja widoczna dla użytkownika jest ograniczona do prawidłowego zakresu przez samą właściwość, a nie przez walidację UI. Walidacja UI to kolejny ciemny wzorzec, jeśli nie jesteś ostrożny. Getter bellRepeatCount nie może zwrócić nic poza 1, 2 lub 3. Zapisanie 0 lub 47 do leżącego pod spodem @AppStorage po cichu przycina wartość z powrotem do dozwolonego zakresu.
@ObservationIgnored @AppStorage("bellRepeatCount") private var _bellRepeatCount = 1 /// Validated bell repeat count (1-3) var bellRepeatCount: Int { get { max(1, min(3, _bellRepeatCount)) } set { _bellRepeatCount = max(1, min(3, newValue)) } }
Return kosztuje 2,99 USD. Płacisz raz i jest twoja. Brak kosztów serwerów do utrzymania, brak subskrypcji do odnowienia, brak pipeline'u analitycznego obserwującego, co robisz. Produkt jest produktem. Jeśli chcesz dłuższej wersji tego, dlaczego nadal buduję aplikacje w ten sposób, przeczytaj Minimum Worthy Product i The Steve Test. Krótka wersja mieści się w tej sekcji.
Return.
Dostępne teraz w App Store na iPhone, iPad, Apple Watch, Apple TV i Mac.