Return...
Un minuteur zen de méditation et de concentration sur cinq écrans : iPhone, iPad, Apple Watch, Apple TV et Mac.
Publié le 21 avril 2026. Une seule base de code. Vingt-sept langues, dont l'arabe et l'hébreu. Quatre thèmes, trois cloches, zéro analytique. Ce qui suit, c'est comment tout s'est assemblé : les choix techniques, les compromis de design et le long processus silencieux qui consiste à trier des centaines de gouttes d'eau générées par IA pour n'en garder qu'une.
Une base de code, cinq écrans.
Return est la première app que j'ai publiée qui tourne sur toutes les classes d'écran Apple depuis un seul projet Xcode : iPhone, iPad, Apple Watch, Apple TV et Mac. Cinquante-sept fichiers Swift, environ 12 700 lignes de code et zéro dépendance externe. Du SwiftUI pur, AVFoundation, HealthKit, ActivityKit et WidgetKit.
La manière naïve de faire ça, c'est un TimerManager universel avec des branches #if pour chaque différence de plateforme. Je ne l'ai pas fait. Return embarque trois classes de minuteur (TimerManager sur iOS et macOS, TVTimerManager sur tvOS, WatchTimerManager sur watchOS) qui partagent la même sémantique d'état mais respectent ce que chaque plateforme sait vraiment bien faire. Les Live Activities uniquement sur iOS. HealthKit uniquement là où l'API existe. Les sessions d'exécution prolongée uniquement sur Watch. Chaque manager est plus court et plus honnête qu'une seule classe polymorphe ne le serait.
Partagé là où ça compte.
Un unique dossier Shared/ porte les éléments sur lesquels toutes les cibles doivent s'accorder : le modèle de données MeditationSession, le wrapper iCloud SessionStore et SessionHistoryView. Les réglages se synchronisent entre la Watch et le téléphone via un App Group (group.com.941apps.Return). Le reste est spécifique à chaque plateforme, volontairement.
L'exemple le plus clair, c'est la ligne unique qui décide si une session a déjà été enregistrée dans HealthKit. L'iPhone écrit directement, donc « synced » est vrai dès la fin de la session. Le Mac et la TV ne peuvent pas écrire dans HealthKit du tout, donc « synced » est faux jusqu'à ce que l'iPhone récupère la session en attente plus tard. Même intention, booléen opposé, un seul #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) }
Je reviens sans cesse à ce schéma : le moins de lignes possible tout en gardant l'intention lisible. Quand le même booléen signifie des choses différentes sur des plateformes différentes, écris-le comme deux booléens différents. Le #if devient une partie de la documentation.
Vingt-sept langues, et le support de droite à gauche.
Return est la première app Apple que j'ai publiée dans toutes les langues qui me tenaient à cœur. Vingt-sept locales sont passées par une relecture complète, dont l'arabe et l'hébreu. Tout ça vit dans un seul fichier Localizable.xcstrings, ce qui est moins héroïque que ça en a l'air. Xcode fait l'essentiel du travail si tu acceptes de cesser de bricoler tes chaînes à la main.





Le RTL est un gain gratuit si tu arrêtes de lutter contre.
SwiftUI traite .leading et .trailing comme des directions sémantiques plutôt que .left et .right comme des directions fixes. Mets un écran en forme avec des directions sémantiques une fois, et le même écran se reflète automatiquement en arabe, en hébreu, en persan ou en ourdou sans chemin de code dédié. Les étiquettes des réglages se retournent, le chevron de retour s'inverse, la position des interrupteurs bascule. Les icônes de thème (goutte, flamme, feuille) restent en place. Je n'ai pas écrit une seule ligne de code RTL pour ce comportement.
Une exception que j'ai rattrapée au moment de publier : SwiftUI applique la direction de mise en page aux vues Text aussi, ce qui faisait que la première version des captures d'écran en arabe et en hébreu affichait le minuteur « 00:02 » au lieu de « 20:00 » — des chiffres latins disposés de droite à gauche. Un simple modificateur .environment(\.layoutDirection, .leftToRight) sur chaque vue Text contenant du temps ou du contenu numérique corrige le problème. Les captures ci-dessus proviennent de la version livrée avec ce modificateur en place.
Le jeu de captures d'écran a été généré par fastlane en exécutant les mêmes tests UI avec différents arguments -AppleLanguages. Le propre modèle effectiveLocale de l'app lit le drapeau, reconstruit la hiérarchie des vues et capture le résultat. Un seul utilitaire, vingt-sept locales, quatre classes d'appareils, le tout en une nuit.
/// 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 } }
Le .id(appLanguage) est le détail qui gagne sa place. Sans lui, SwiftUI met en cache l'ancienne hiérarchie de vues et les chaînes ne se rafraîchissent pas quand tu changes de langue à l'exécution. Avec lui, tout l'arbre est détruit et reconstruit, et tout relit ses chaînes localisées automatiquement. Une ligne, une catégorie de bugs supprimée.
Les minutes de pleine conscience, enfin.
L'app Mindfulness native de la Watch d'Apple plafonne les sessions intégrées Réfléchir et Respirer à cinq minutes. L'API HealthKit elle-même n'a pas une telle limite. Elle acceptera volontiers n'importe quel HKCategorySample dont la date de fin est postérieure à la date de début. La limite vit dans l'UI, pas dans le système. Return propose un sélecteur de 5 à 60 minutes sur tous les appareils et écrit exactement le temps que tu as passé assis.
/// 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 ) ... }
La seule validation, c'est end > start. C'est tout ce que HealthKit lui-même valide. L'API d'Apple a toujours été prête à enregistrer une méditation de quarante-cinq minutes. Il manquait seulement le bouton pour en demander une.
Multi-appareils sans HealthKit sur trois d'entre eux.
Le Mac et l'Apple TV n'ont pas HealthKit du tout. La réponse évidente, c'est « alors ne t'embête pas à y enregistrer les sessions ». La réponse moins évidente, et correcte, c'est de les enregistrer quand même, dans l'iCloud Key-Value Store, et de laisser le téléphone les récupérer la prochaine fois qu'il se réveille. Le SessionStore de Return est le stockage partagé, MeditationSession.syncedToHealthKit est le drapeau d'attente et HealthKitManager.syncPendingSessions() s'exécute chaque fois que l'app iOS revient au premier plan.
iCloud Key-Value Store
C'est la pièce que je pense qu'Apple devrait livrer eux-mêmes : un vrai enregistreur multiplateforme de Minutes de pleine conscience qui ne nécessite pas qu'un téléphone soit actif quand tu veux méditer sur un Mac. En attendant, Return le fait.
D'où vient l'eau.
Quatre thèmes. Quatre boucles d'ambiance. Trois cloches. Tout généré, la majeure partie mise à la poubelle. Les vidéos viennent de Midjourney, l'audio d'ElevenLabs, et le travail qui comptait, ce n'était pas le prompting. C'était le montage. Regarder une grille de deux cents gouttes d'eau et choisir celle qui boucle proprement sans raccord visible. Écouter quarante variations d'une cloche de temple jusqu'à en trouver une qui a la bonne attaque et le bon déclin et ne sonne pas comme une notification de téléphone.




Chaque tuile est une génération. Les cœurs sont celles qui ont survécu à un premier tri. Les triangles de lecture sont celles que j'ai portées en vidéo. Quatre thèmes livrés. Tout le reste est resté dans la grille, et c'est tout l'enjeu du processus : le ratio compte.
Les cloches ont suivi le même arc en audio. Prompt, écoute, affinage, prompt à nouveau. J'en ai gardé trois : Singing Bowl, Temple Bell, Soft Chime. Chacune itérée jusqu'à ce qu'elle cesse de sonner synthétique.
Je ne vais pas prétendre compter le total des générations. Des centaines par thème, c'est honnête. La discipline n'est pas dans les prompts. Elle est dans le fait de jeter tout ce qui n'est que bon, et de ne garder que ce qui peut rester derrière un minuteur pendant vingt minutes silencieuses sans jamais devenir la chose qu'on remarque.
Pourquoi un minuteur, pas un enseignant.
Cette partie est personnelle. J'ai construit Return parce que j'ai déjà une pratique de méditation et que je n'arrivais pas à trouver un minuteur qui sache s'effacer. Ce avec quoi je m'assois, c'est le zen japonais dans sa veine martiale : Takuan, Yagyu, Musashi, Dogen, Hakuin. Pas la pleine conscience thérapeutique que livrent les grandes apps. Intention différente, texture différente.
Ce qui tourne au fil d'une semaine donnée :
- Susokukan (compte de la respiration). Compter de un à dix sur la respiration, revenir à un chaque fois que le compte se perd. Fondation. La concentration, joriki, d'abord.
- Shikantaza (simplement s'asseoir). Sans objet. Pas de compte, pas de question, pas de visualisation. L'esprit qui ne fixe rien. La forme centrale de zazen chez Dogen et l'approximation formelle la plus proche de l'état que je recherche vraiment.
- Koan. Principalement le Mu de Joshu. Une question qu'on ne peut pas résoudre en pensant, tenue jusqu'à ce que la pensée abandonne.
- Maranasati (contemplation de la mort). Cadrage Hagakure. Utilisé avec parcimonie. La survie resserre l'esprit ; ceci le transperce.
- Isshin (un seul esprit). Territoire de Takuan et Yagyu : détendu mais engagé, posé mais mobile. Le pont entre le coussin et ce qui vient après.
- Jours d'intégration. Gratitude, compassion, lignée. Jihi. Katsujinken : l'épée qui donne la vie, pas l'épée qui tue. Les samedis, en général.
- Sakki (conscience de l'intention hostile). Cinq minutes d'écoute en champ ouvert ajoutées à chaque session. Fait sortir le shikantaza du coussin et le met à l'épreuve dans des environnements ordinaires.
La rotation n'est pas rigide. Compte de la respiration quand j'ai besoin de me stabiliser. Koan quand j'ai besoin de percer. Shikantaza quand j'ai besoin de reposer dans l'ouverture. Contemplation de la mort quand les enjeux ont besoin d'être clarifiés. La variété appartient à l'entraînement.
Return est un minuteur parce que je n'ai pas besoin d'un enseignant sur mon téléphone. J'ai besoin de quelque chose qui tienne l'horloge pour moi, qui marque le début et la fin avec une cloche que je respecte, et qui s'efface entre les deux. Si tu as déjà une pratique, c'est probablement aussi ce que tu veux. Si tu es tout nouveau, trouve un enseignant dans une salle. Puis reviens.
Ce qui n'est pas dans Return.
Return n'est pas Calm. Ce n'est pas Headspace. Il n'y a pas de narrateur britannique qui te guide doucement dans un body scan. Il n'y a pas d'avatar en dessin animé qui célèbre ta série. Il n'y a pas d'abonnement qui débloque de nouveaux programmes guidés. Return est un minuteur. L'idée, c'est que si tu as déjà une pratique, tu n'as pas besoin d'un enseignant dans l'app. Tu as besoin d'un outil qui tienne le temps pour toi et qui s'efface.
- Pas de voix guidée ni de narration
- Pas de séries, de scores ni de gamification
- Pas d'abonnement ni d'achats intégrés
- Pas de publicités, jamais
- Pas d'analytique ; l'app ne suit rien
- Pas de connexion ni de partage via les réseaux sociaux
- Pas d'écrans de nag, pas de modales au démarrage à froid
- Pas de dark patterns dans le flux d'achat intégré, parce qu'il n'y a pas de flux d'achat intégré
Ce qui est dans Return, gardé délibérément petit : quatre modes de répétition (Une fois, Jusqu'à l'arrêt, Jusqu'à l'heure, Répéter N fois), une pause respiratoire de deux secondes entre les cycles, une à trois sonneries de cloche à chaque transition, un choix parmi trois cloches, quatre thèmes, l'opt-in HealthKit et un sélecteur de langue. C'est tout le produit.
Le coût d'une telle rigueur se voit dans le modèle des réglages. Chaque préférence exposée à l'utilisateur est bornée à une plage valide par la propriété elle-même, pas par une validation UI. La validation UI est un autre dark pattern si tu n'y prends pas garde. Le getter bellRepeatCount ne peut rien retourner d'autre que 1, 2 ou 3. Écrire 0 ou 47 dans le @AppStorage sous-jacent ramène silencieusement la valeur dans la plage autorisée.
@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 coûte 2,99 $. Tu paies une fois et elle t'appartient. Pas de coûts serveur à supporter, pas d'abonnement à renouveler, pas de pipeline d'analytique qui surveille ce que tu fais. Le produit est le produit. Si tu veux la version longue de pourquoi je continue à construire des apps comme ça, lis Minimum Worthy Product et The Steve Test. La version courte vit dans cette section.
Return.
Disponible dès maintenant sur l'App Store pour iPhone, iPad, Apple Watch, Apple TV et Mac.