Return...
Ein Zen-Meditations- und Fokus-Timer auf fünf Bildschirmen: iPhone, iPad, Apple Watch, Apple TV und Mac.
Veröffentlicht am 21. April 2026. Eine Codebase. Siebenundzwanzig Sprachen, darunter Arabisch und Hebräisch. Vier Themen, drei Glocken, keine Analytics. Im Folgenden geht es darum, wie das Ganze zusammenkam: die technischen Entscheidungen, die gestalterischen Abwägungen und der lange, stille Prozess, Hunderte KI-generierter Wassertropfen auf einen einzigen zu reduzieren.
Eine Codebase, fünf Bildschirme.
Return ist die erste App, die ich veröffentlicht habe, die aus einem einzigen Xcode-Projekt auf jeder Apple-Bildschirmklasse läuft: iPhone, iPad, Apple Watch, Apple TV und Mac. Siebenundfünfzig Swift-Dateien, rund 12.700 Codezeilen und keine einzige externe Abhängigkeit. Reines SwiftUI, AVFoundation, HealthKit, ActivityKit und WidgetKit.
Der naive Weg wäre ein einziger universeller TimerManager mit #if-Verzweigungen für jeden Plattformunterschied. Den habe ich nicht gewählt. Return enthält drei Timer-Klassen (TimerManager auf iOS und macOS, TVTimerManager auf tvOS, WatchTimerManager auf watchOS), die dieselbe Zustandssemantik teilen, aber respektieren, worin jede Plattform tatsächlich gut ist. Live Activities nur auf iOS. HealthKit nur dort, wo die API existiert. Extended Runtime Sessions nur auf der Watch. Jeder Manager ist kürzer und ehrlicher, als eine einzelne polymorphe Klasse es wäre.
Geteilt, wo es zählt.
Ein einziger Shared/-Ordner trägt die Teile, auf die sich alle Targets einigen müssen: das Datenmodell MeditationSession, den iCloud-Wrapper SessionStore und SessionHistoryView. Einstellungen werden zwischen Watch und iPhone über eine App Group (group.com.941apps.Return) synchronisiert. Der Rest ist bewusst plattformspezifisch.
Das deutlichste Beispiel ist die eine Zeile, die entscheidet, ob eine Sitzung bereits in HealthKit protokolliert wurde. Das iPhone schreibt direkt, also ist „synced“ in dem Moment wahr, in dem die Sitzung endet. Mac und TV können überhaupt nicht in HealthKit schreiben, also ist „synced“ falsch, bis das iPhone die anstehende Sitzung später aufnimmt. Dieselbe Absicht, entgegengesetzter Boolean, ein #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) }
Zu diesem Muster komme ich immer wieder zurück: die wenigsten Zeilen, die die Absicht noch lesbar machen. Wenn derselbe Boolean auf verschiedenen Plattformen unterschiedliche Dinge bedeutet, schreib ihn als unterschiedliche Booleans. Das #if wird Teil der Dokumentation.
Siebenundzwanzig Sprachen und Unterstützung für Rechts-nach-links.
Return ist die erste Apple-App, die ich in jeder Sprache veröffentlicht habe, die mir wichtig war. Siebenundzwanzig Locales haben einen vollständigen Review-Durchlauf durchlaufen, einschließlich Arabisch und Hebräisch. Das alles liegt in einer einzigen Localizable.xcstrings-Datei, was weniger heldenhaft ist, als es klingt. Xcode erledigt den Großteil der Arbeit, wenn man aufhört, Strings von Hand zu basteln.





RTL ist ein kostenloser Gewinn, wenn man aufhört, dagegen anzukämpfen.
SwiftUI behandelt .leading und .trailing als semantische Richtungen, nicht .left und .right als feste. Wenn man einen Bildschirm einmal in semantischen Richtungen anlegt, spiegelt sich derselbe Bildschirm automatisch auf Arabisch, Hebräisch, Persisch oder Urdu, ohne einen eigenen Code-Pfad. Beschriftungen in den Einstellungen kippen, das Zurück-Chevron dreht sich um, Schalterpositionen werden invertiert. Themen-Icons (Tropfen, Flamme, Blatt) bleiben, wo sie sind. Ich habe für dieses Verhalten keine Zeile RTL-Code geschrieben.
Eine Ausnahme, die mir beim Release aufgefallen ist: SwiftUI wendet die Layout-Richtung auch auf Text-Views an, was dazu führte, dass der erste Schnitt der arabischen und hebräischen Screenshots den Timer als „00:02“ statt „20:00“ zeigte — lateinische Ziffern von rechts nach links gelayoutet. Ein einziger .environment(\.layoutDirection, .leftToRight)-Modifikator auf jeder Text-View, die Zeit- oder Zahleninhalte enthält, behebt das. Die Screenshots oben stammen aus dem Release, der genau diesen Modifikator eingebaut mitbringt.
Der Screenshot-Satz wurde von fastlane erzeugt, das dieselben UI-Tests mit unterschiedlichen -AppleLanguages-Argumenten ausführt. Das app-eigene effectiveLocale-Muster liest das Flag, baut die View-Hierarchie neu auf und erfasst das Ergebnis. Ein Helfer, siebenundzwanzig Locales, vier Geräteklassen — alles in einem einzigen Lauf über Nacht.
/// 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 } }
Das .id(appLanguage) ist das Detail, das seinen Platz verdient. Ohne das cached SwiftUI die alte View-Hierarchie, und die Strings aktualisieren sich nicht, wenn man zur Laufzeit die Sprache wechselt. Mit ihm wird der gesamte Baum verworfen und neu aufgebaut, und alles liest seine lokalisierten Strings automatisch neu. Eine Zeile, eine ganze Kategorie von Bugs gelöscht.
Endlich Achtsamkeitsminuten.
Apples native Watch-App „Achtsamkeit“ deckelt die eingebauten Reflektions- und Atemsitzungen bei fünf Minuten. Die HealthKit-API selbst hat keine solche Obergrenze. Sie akzeptiert bereitwillig jede HKCategorySample, bei der das Enddatum nach dem Startdatum liegt. Die Grenze liegt im UI, nicht im System. Return stellt auf jedem Gerät einen Auswähler von 5 bis 60 Minuten bereit und schreibt das, was du tatsächlich gesessen hast.
/// 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 ) ... }
Die einzige Validierung lautet end > start. Genau das validiert HealthKit selbst. Apples API war schon immer bereit, eine fünfundvierzigminütige Meditation zu protokollieren. Es fehlte lediglich der Knopf, eine anzufordern.
Geräteübergreifend, obwohl drei davon kein HealthKit haben.
Mac und Apple TV haben überhaupt kein HealthKit. Die naheliegende Reaktion ist: „Dann protokolliere dort eben keine Sitzungen.“ Die weniger naheliegende, richtige Reaktion ist, sie trotzdem zu protokollieren, in den iCloud Key-Value Store, und sie vom iPhone aufnehmen zu lassen, wenn es das nächste Mal aufwacht. Returns SessionStore ist der gemeinsame Speicher, MeditationSession.syncedToHealthKit ist das Ausstehend-Flag, und HealthKitManager.syncPendingSessions() läuft jedes Mal, wenn die iOS-App in den Vordergrund zurückkehrt.
iCloud Key-Value Store
Das ist das Stück, von dem ich finde, dass Apple es selbst liefern sollte: einen ordentlichen plattformübergreifenden Achtsamkeitsminuten-Writer, der kein aktives Telefon voraussetzt, nur weil man auf einem Mac meditieren will. Bis sie das tun, tut Return es.
Woher das Wasser kam.
Vier Themen. Vier Ambient-Loops. Drei Glocken. Alles generiert, das meiste davon weggeworfen. Die Videos stammen aus Midjourney, das Audio aus ElevenLabs, und die Arbeit, auf die es ankam, war nicht das Prompting. Es war das Editieren. Ein Raster aus zweihundert Wassertropfen ansehen und den einen heraussuchen, der sauber ohne sichtbare Naht loopt. Vierzig Variationen einer Tempelglocke hören, bis eine den richtigen Anschlag und das richtige Ausklingen hat und nicht wie eine Telefonbenachrichtigung klingt.




Jede Kachel ist eine Generierung. Die Herzen sind diejenigen, die den ersten Durchgang überlebt haben. Die Abspiel-Dreiecke sind die, die ich ins Video überführt habe. Vier Themen sind veröffentlicht. Alles andere blieb im Raster, und genau darum geht es im Prozess: Das Verhältnis zählt.
Die Glocken folgten im Audio demselben Bogen. Prompten, hören, verfeinern, erneut prompten. Drei habe ich behalten: Singing Bowl, Temple Bell, Soft Chime. Jede iteriert, bis sie aufhörte, synthetisch zu klingen.
Ich werde nicht so tun, als würde ich die Gesamtzahl der Generierungen zählen. Hunderte pro Thema ist eine ehrliche Schätzung. Die Disziplin liegt nicht im Prompting. Sie liegt darin, alles wegzuwerfen, was bloß gut ist, und nur das zu behalten, was zwanzig stille Minuten hinter einem Timer sitzen kann, ohne jemals das zu werden, was man bemerkt.
Warum ein Timer, kein Lehrer.
Dieser Teil ist persönlich. Ich habe Return gebaut, weil ich bereits eine Meditationspraxis habe und keinen Timer finden konnte, der mir aus dem Weg geht. Worin ich sitze, ist japanisches Zen in seiner kriegerischen Strömung: Takuan, Yagyu, Musashi, Dogen, Hakuin. Nicht die therapeutische Achtsamkeit, wie sie die großen Apps liefern. Andere Absicht, andere Textur.
Was in einer beliebigen Woche rotiert:
- Susokukan (Atemzählen). Beim Atem von eins bis zehn zählen, zur Eins zurückkehren, sobald die Zählung verloren geht. Fundament. Konzentration, joriki, zuerst.
- Shikantaza (einfach nur sitzen). Objektlos. Kein Zählen, keine Frage, keine Visualisierung. Der Geist, der sich nicht festmacht. Dogens zentrale Form des zazen und die formale Annäherung, die dem Zustand, den ich tatsächlich will, am nächsten kommt.
- Koan. Vor allem Joshus Mu. Eine Frage, die sich nicht durch Denken lösen lässt, gehalten, bis das Denken aufgibt.
- Maranasati (Todesbetrachtung). Hagakure-Rahmung. Sparsam verwendet. Überleben spannt den Geist an; das schneidet hindurch.
- Isshin (ein Geist). Takuans und Yagyus Terrain: entspannt, aber engagiert, gefestigt, aber beweglich. Die Brücke zwischen dem Kissen und dem, was danach kommt.
- Integrationstage. Dankbarkeit, Mitgefühl, Linie. Jihi. Katsujinken: das lebensspendende Schwert, nicht das tötende Schwert. Meistens samstags.
- Sakki (Bewusstsein feindseliger Absicht). An jede Sitzung werden fünf Minuten offenes Zuhören im freien Feld angehängt. Das holt shikantaza vom Kissen und testet es in gewöhnlichen Umgebungen unter Druck.
Die Rotation ist nicht starr. Atemzählen, wenn ich mich stabilisieren muss. Koan, wenn ich durchbrechen muss. Shikantaza, wenn ich in Offenheit ruhen muss. Todesbetrachtung, wenn der Einsatz geklärt werden muss. Die Vielfalt gehört zum Training.
Return ist ein Timer, weil ich auf meinem Telefon keinen Lehrer brauche. Ich brauche etwas, das die Uhr hält, damit ich es nicht muss, das Anfang und Ende mit einer Glocke markiert, die ich respektiere, und sich dazwischen aus dem Weg hält. Wenn du bereits eine Praxis hast, ist das wahrscheinlich auch das, was du willst. Wenn du ganz neu bist, such einen Lehrer in einem Raum. Dann komm zurück.
Was nicht in Return ist.
Return ist nicht Calm. Es ist nicht Headspace. Da ist kein britischer Erzähler, der dich sanft in einen Body-Scan einführt. Da ist kein Comic-Avatar, der deine Streak feiert. Da ist kein Abo, das neue geführte Programme freischaltet. Return ist ein Timer. Die Idee ist: Wenn du bereits eine Praxis hast, brauchst du keinen Lehrer in der App. Du brauchst ein Werkzeug, das die Zeit für dich hält und aus dem Weg geht.
- Keine geführte Stimme, keine Erzählung
- Keine Streaks, Punkte oder Gamification
- Kein Abo, keine In-App-Käufe
- Niemals Werbung
- Keine Analytics; die App trackt nichts
- Kein Social-Login, kein Teilen
- Keine Nerv-Screens, keine Cold-Start-Modals
- Keine Dark Patterns im IAP-Flow, weil es keinen IAP-Flow gibt
Was bewusst klein gehalten in Return steckt: vier Wiederholungsmodi (Einmal, Bis gestoppt, Bis Zeit, N-mal wiederholen), eine zweisekündige Atempause zwischen Zyklen, ein bis drei Glockenschläge bei jedem Übergang, eine Auswahl aus drei Glocken, vier Themen, HealthKit-Opt-in und eine Sprachauswahl. Das ist das gesamte Produkt.
Der Preis dieser Strenge zeigt sich im Einstellungsmodell. Jede benutzerseitige Einstellung wird von der Property selbst auf einen gültigen Bereich begrenzt, nicht von der UI-Validierung. UI-Validierung ist ein weiteres Dark Pattern, wenn man nicht aufpasst. Der bellRepeatCount-Getter kann nichts anderes als 1, 2 oder 3 zurückgeben. Schreibt man 0 oder 47 in das darunterliegende @AppStorage, wird stillschweigend auf den zulässigen Bereich geklemmt.
@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 kostet 2,99 $. Man bezahlt einmal und besitzt es. Keine Serverkosten zu stützen, kein Abo zu verlängern, keine Analytics-Pipeline, die zusieht, was du tust. Das Produkt ist das Produkt. Wenn du die längere Version davon willst, warum ich Apps so baue, lies Minimum Worthy Product und The Steve Test. Die kurze Version steht in diesem Abschnitt.
Return.
Jetzt im App Store verfügbar für iPhone, iPad, Apple Watch, Apple TV und Mac.