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.

Uniwersalność

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.

Return działa na iPhone 17 Pro Max z motywem Ogień
iPhone
Return działa na macOS z motywem Ogień
Mac
Return działa na Apple Watch Series 11 z motywem Ogień
Watch
Return działa na Apple TV z motywem Ogień
Apple TV
Return działa na iPad Pro 13 cali z motywem Ogień
iPad

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:

Swift · TimerManager.swift:120-138
/// 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.

Lokalizacja

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.

Ekran główny Return, motyw Woda, angielski
EnglishEkran główny · Woda
Ekran główny Return, motyw Ogień, japoński
日本語Ekran główny · Ogień
Ekran główny Return, motyw Las, chiński uproszczony
简体中文Ekran główny · Las
Ekran ustawień Return, niemiecki
DeutschUstawienia
Ekran uprawnień HealthKit w Return, koreański
한국어HealthKit

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.

Ekran główny Return, motyw Las, angielski, układ od lewej do prawej
Angielski · LTR
Ekran główny Return, motyw Las, arabski, układ od prawej do lewej
Arabski · RTL
Ekran główny Return, motyw Las, hebrajski, układ od prawej do lewej
Hebrajski · RTL

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.

Swift · ReturnWatchApp.swift:92-111
/// 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.

HealthKit

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ś.

Swift · HealthKitManager.swift:92-103
/// 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.

SessionStore
iCloud Key-Value Store
Oczekujące sesje
iPhone zapisuje do HealthKit ♥
Wykres słupkowy Uważnych Minut w Apple Health pokazujący średnio 20 minut przez miesiąc
Uważne Minuty w Apple Health, widok słupkowy. Natywna aplikacja Mindfulness od Apple kończy na pięciominutowej sesji Reflect. Leżący pod spodem magazyn nie obchodzi, co do niego zapisujesz.
Kalendarz Uważnych Minut w Apple Health pokazujący 18 dni praktyki w ciągu ostatnich 4 tygodni
Te same dane, widok kalendarza: 18 dni w ciągu ostatnich 4 tygodni, każda sesja zalogowana przez Return.
Ekran Historia Sesji w Return pokazujący listę dwudziestominutowych medytacji
Własna historia sesji Return. Każde urządzenie wnosi swój wkład, a każda sesja niesie znacznik źródła.

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.

Generatywność

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.

Arkusz kontaktowy Midjourney: setki wariantów kropel wody, kilka oznaczonych sercami i trójkątami odtwarzania
Woda · pokazano 128
Arkusz kontaktowy Midjourney: dziesiątki wariantów ognia
Ogień · pokazano 96
Arkusz kontaktowy Midjourney: warianty koron drzew i liści
Las · pokazano 60
Arkusz kontaktowy Midjourney: eksploracja chmur i nieba, która nie została wydana
Niewydana eksploracja · pokazano 128

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.

Praktyka

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óć.

Powściągliwość

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.

Swift · Settings.swift:74-81
@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.