← Tous les articles

Cinq plateformes Apple, trois fichiers partagés : comment Return livre vraiment du SwiftUI multiplateforme

Return, mon minuteur de méditation, tourne sur cinq plateformes Apple : iPhone, iPad, Mac, Apple Watch et Apple TV.1 La base de code compte 40 fichiers Swift (hors tests). Trois d’entre eux sont partagés entre les cinq plateformes. Le reste est réparti dans des cibles Xcode distinctes qui dupliquent des concepts comme TimerManager, AudioManager et ContentView au lieu de les mutualiser via la compilation conditionnelle #if os(...).

Le taux de partage tourne autour de 7,5 %, et c’est intentionnel.

Cet essai raconte ce à quoi ressemble vraiment la livraison d’une application SwiftUI multiplateforme en 2026, pourquoi le partage de code à outrance est surestimé, et ce qu’ont en commun les trois fichiers qui, eux, se partagent vraiment.

iOS 26 platform tile from Apple Developer iPadOS 26 platform tile from Apple Developer macOS 26 platform tile from Apple Developer watchOS 26 platform tile from Apple Developer tvOS 26 platform tile from Apple Developer

Les cinq plateformes visées par Return, telles qu’Apple les présente sur developer.apple.com. Chacune correspond à une cible de plateforme distincte dans Xcode, et non à une bifurcation à l’exécution.

TL;DR

  • Return : 18 fichiers Swift dans la cible principale (iOS + iPadOS + macOS), 10 dans la cible tvOS, 7 dans la cible watchOS, 2 fichiers de widget (Live Activities) et 3 fichiers réellement multiplateformes dans Return/Shared/. Total : 40.
  • Les trois fichiers partagés sont ceux qui touchent à la persistance : MeditationSession, SessionStore, SessionHistoryView. De l’état qui voyage par iCloud, pas une UI qui s’adapte à la plateforme.
  • tvOS et watchOS forment des cibles Xcode séparées, et non des branches #if os(tvOS) dans la cible principale. Leurs modèles de contrôle sont bien trop éloignés pour tenir dans un seul ContentView.
  • Même au sein de la cible principale iOS/iPadOS/macOS, les blocs #if os prolifèrent : 10 dans ContentView.swift, 8 dans LiveActivityManager.swift, 8 dans VideoBackgroundView.swift, 6 dans AudioManager.swift.
  • Le constat honnête : mutualiser à outrance sur cinq plateformes Apple revient à contracter une dette de maintenance. Un petit noyau partagé (la couche de persistance), plus des UI distinctes propres à chaque plateforme, se livre plus vite et casse moins qu’un unique fichier truffé de #if.

Pour les articles compagnons propres à chaque plateforme, lisez la matrice des plateformes Apple, le contrat d’exécution watchOS et les patterns SwiftUI Liquid Glass.

Les chiffres

Voici la forme de la base de code, en nombre de fichiers Swift, une fois les tests et les tests d’interface écartés :

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

Cinq plateformes, trois fichiers partagés, deux cibles séparées par plateforme plus une cible widget, et une compilation conditionnelle abondante à l’intérieur de la cible principale. Le ratio de partage avoisine 7,5 %. La plupart des tutoriels sur le « SwiftUI multiplateforme » recommandent l’inverse : écrire un unique ContentView qui s’adapte à chaque plateforme via @Environment(\.horizontalSizeClass) et #if os(...).2 Cela fonctionne pour deux plateformes (iPhone + iPad). Cela casse à cinq.

Ce que les trois fichiers partagés ont en commun

Return/Shared/MeditationSession.swift définit le type valeur adjacent à 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
    }
}

Le commentaire d’en-tête du fichier porte une vraie charge : // Add this file to: Return, ReturnTV, ReturnWatch Watch App targets. Le même fichier source est référencé par les trois cibles Xcode, sans lien symbolique et sans Swift package. Le système de build d’Apple compile sans broncher un seul fichier dans trois binaires.

SessionStore.swift constitue la couche de persistance : une fine surcouche autour de NSUbiquitousKeyValueStore (le Key-Value Store iCloud d’Apple) qui lit et écrit des tableaux de MeditationSession. Le choix compte : cette synchronisation par KV-store donne à Return un historique de séances multi-appareils sans provisionner de conteneur CloudKit, au prix d’un plafond de 1 Mo pour l’ensemble du store.12 Pour une liste de séances de méditation pesant chacune quelques centaines d’octets, ce plafond suffit très largement. SessionHistoryView.swift est une liste SwiftUI qui affiche ces séances. Les deux sont utilisés à l’identique par les cibles iPhone, iPad, Mac, Watch et TV.

Le point commun de ces trois fichiers : ils décrivent de l’état, pas de l’interaction. Une MeditationSession recouvre le même concept sur tous les appareils. La liste des séances passées se lit de la même façon partout. Aucun des trois ne met en jeu une surface de contrôle, un gestionnaire de fenêtres, une décision de routage audio, un moteur de focus ou une Digital Crown. Dès qu’un fichier a besoin de savoir sur quelle plateforme il s’exécute, il cesse d’être partageable.

Pourquoi le reste n’a pas été partagé

Prenez TimerManager. La version iOS/iPadOS/macOS s’appuie sur Timer.publish(every: 1, ...) et fait passer les notifications par UserNotifications. La version tvOS (TVTimerManager) gère le cas où l’utilisateur met en pause avec la Siri Remote et où l’économiseur d’écran s’enclenche. La version watchOS (WatchTimerManager) délègue à une WKExtendedRuntimeSession (via WatchSessionManager) pour que le système garde l’application réactive pendant que l’écran s’assombrit, et reçoit les entrées par la Digital Crown plutôt que par le toucher. Trois plateformes, trois comportements de minuteur profondément différents.

Vous pourriez les unifier sous la forme class TimerManager { #if os(watchOS) ... #elif os(tvOS) ... }. Vous obtiendriez une classe à trois modes, chacun fait de quarante lignes de code sous #if, où toucher au chemin iOS risque de casser le chemin watchOS. Un cauchemar de maintenance.

Trois classes séparées portant trois noms de fichiers, c’est plus de code sur le disque et moins de code dans la tête. La duplication lisible vaut mieux que l’abstraction illisible.

La même logique vaut pour :

  • ContentView face à TVContentView et WatchContentView : les modèles de navigation diffèrent (par empilement sur iPhone, par focus sur TV, par liste sur Watch).
  • AudioManager face à TVAudioManager et WatchAudioManager : les catégories de session audio ne sont pas les mêmes, watchOS impose des règles plus strictes pour l’audio en arrière-plan, et tvOS route différemment vers AirPlay.
  • VideoBackgroundView compte 8 branches #if os(iOS) dans la cible principale (avec un #elseif os(macOS) en pendant), qui couvrent des ressources vidéo distinctes (fire_phone.mp4 contre fire_mac.mp4), des types de couches distincts et des ratios d’image distincts.4

Une précision s’impose : la cible principale Return/ regroupe bel et bien iOS, iPadOS et macOS. Ces trois plateformes ont bien plus de code en commun que de code spécifique. Le NavigationStack de SwiftUI fonctionne sur les trois. .glassEffect() aussi. Les écarts de gestion de fenêtres existent, mais restent traitables au sein d’une même cible. C’est devant tvOS et watchOS que j’ai tracé la frontière de la cible séparée.

Le cas tvOS : pourquoi le moteur de focus a imposé une cible séparée

La navigation sur Apple TV s’organise autour du moteur de focus.5 Chaque élément d’UI avec lequel l’utilisateur peut interagir se déclare focalisable ; les flèches système de la Siri Remote déplacent le focus d’un élément à l’autre ; l’appui sur Sélectionner active l’élément focalisé. Sur tvOS, SwiftUI expose tout cela via .focusable(), .focusEffect et des types ButtonStyle personnalisés qui réagissent à @Environment(\.isFocused) pour produire l’effet d’inclinaison en parallaxe qu’utilisent les applications maison d’Apple. Du vrai code de production, tiré de 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)
    }
}

Le même fichier définit également TVCircleButtonStyle pour les contrôles carrés ou circulaires. Les deux styles inversent couleur et translucidité au focus : les boutons non focalisés reposent sur .ultraThinMaterial, les boutons focalisés se remplissent de la couleur d’accent et gagnent en échelle comme en ombre. Pour cette application, la construction est structurellement propre à tvOS. @Environment(\.isFocused) existe sur iOS, iPadOS, macOS, watchOS et tvOS,13 mais la navigation pilotée par le focus n’est le modèle d’interaction principal que sur tvOS, où la Siri Remote ne produit ni événement de pointeur ni événement tactile. Sur iPhone ou iPad, le contrôle équivalent se résout par un appui ; sur Mac, par un survol ou un clic. Les styles de bouton de TVFocusModifier.swift posent le focus comme affordance principale de l’utilisateur et bâtissent toute la réponse visuelle autour. Il n’existe aucune bonne manière d’écrire au même endroit un ContentView qui gère le toucher sur iOS, le survol sur Mac et la navigation par focus sur tvOS. La structure des vues est réellement différente : un ContentView tvOS est un graphe de rangées focalisables, un ContentView iOS est une pile où l’on appuie pour agir.

Le sélecteur de durée raconte la même histoire. Sur iPhone, il glisse depuis le bas et accepte les appuis. Sur Apple TV, c’est une rangée horizontale de cellules focalisables que l’utilisateur parcourt à la télécommande. TVDurationPicker.swift existe à part parce que ce design par cellules focalisables n’a aucun équivalent sur iPhone. Les réunir dans un même fichier reviendrait à coller ensemble, avec un #if os(tvOS), deux UI sans rapport.

Le cas watchOS : sessions d’exécution étendue, HealthKit et une surface réduite

watchOS ajoute deux contraintes structurelles absentes des autres plateformes :

  1. WKExtendedRuntimeSession pour garder l’application réactive pendant que l’écran de la montre s’assombrit.8 Sans elle, watchOS suspend agressivement l’application entre chaque tic de seconde et le minuteur dérive. Return déclare WKBackgroundModes: mindfulness dans le Info.plist de la cible watchOS pour que le système reconnaisse le cas d’usage et accorde le budget d’exécution ; la session d’exécution elle-même est créée avec l’initialiseur par défaut WKExtendedRuntimeSession().
  2. La synchronisation iCloud via NSUbiquitousKeyValueStore, et non WatchConnectivity.7 La synchronisation de l’historique de séances de Return emprunte le même Key-Value Store que les cibles iPhone, iPad et Mac : une méditation enregistrée sur la montre apparaît donc dans la vue d’historique de l’iPhone sans le moindre message direct de la montre vers le téléphone. WatchConnectivity resterait une option pour synchroniser l’état en direct un jour, mais Return a choisi le modèle le plus simple : chaque appareil écrit dans le même KV-store iCloud, et la lecture suivante, depuis n’importe lequel d’entre eux, en voit l’union.

WatchTimerManager.swift est le minuteur côté montre ; il délègue le travail d’exécution étendue à WatchSessionManager, défini dans ReturnWatchApp.swift sous la forme final class WatchSessionManager: NSObject, WKExtendedRuntimeSessionDelegate. Le TimerManager iOS n’a pas d’équivalent, puisqu’une application iOS reste réactive au premier plan sans session d’exécution explicite. Faire entrer la logique de la montre dans le TimerManager iOS via #if os(watchOS) obligerait le chemin de code iOS à importer des symboles WatchKit qu’il n’utilise jamais, et le chemin watchOS exigerait des séquences d’initialisation que le chemin iOS n’a pas.

WatchHealthKitManager.swift est une variante réduite du HealthKitManager principal. Elle journalise les minutes de pleine conscience de la même manière, mais l’UX de la demande d’autorisation change (la montre ne peut pas afficher de HealthKitPermissionSheet). La classe Watch fait environ la moitié de la taille de la principale.

Ce qui se passe à l’intérieur de la cible principale iOS/iPadOS/macOS

Même au sein de la cible principale, le partage n’a rien d’automatique. ContentView.swift compte dix blocs #if os(macOS) ou #if !os(macOS) ; LiveActivityManager.swift en compte huit ; VideoBackgroundView.swift, huit ; AudioManager.swift, six. Les Live Activities étant réservées à l’iPhone, l’intégralité de LiveActivityManager est enveloppée dans un #if os(iOS). Le sélecteur de durée de l’iPhone n’utilise pas la même mise en page que celui de l’iPad et du Mac, d’où des branches de mise en page parallèles dans ContentView.

Ce qui a fonctionné : #if os(...) pour les petits écarts de plateforme (comportement clavier différent, marge différente, API manquante), cible séparée pour les gros écarts structurels (focus contre toucher, session d’entraînement contre minuteur). Le seuil que j’ai fini par retenir tient en une phrase : « plus de ~10 lignes de branchement ». En dessous, la compilation conditionnelle convient. Au-dessus, le fichier fait deux métiers à la fois, et le second appartient à une autre cible.

Quand ne pas livrer sur les cinq plateformes

L’évaluation honnête, maintenant.

Faites l’impasse sur l’Apple Watch si votre application est dense en informations. L’écran de 46 mm n’a pas la place d’accueillir une liste de 30 éléments, un sélecteur de durée et une page de réglages. Return survit sur watchOS parce que son interaction principale tient dans un bouton unique (démarrer ou arrêter un minuteur). Une application de productivité, une application financière ou une application riche en médias n’y arrivera pas.

Faites l’impasse sur l’Apple TV si votre application est interactive. La TV est faite pour l’ambiant : un minuteur qui tourne sur un écran à l’autre bout de la pièce, une lecture musicale. Tout ce qui exige des saisies fréquentes se bat contre la plateforme. Return est sur tvOS parce que « régler un minuteur de 20 minutes et regarder du feu à l’écran » est précisément le bon cas ambiant. Une application de prise de notes y serait un supplice.

Faites l’impasse sur le Mac si votre interface a été pensée d’abord pour le téléphone. SwiftUI fonctionne sur Mac, mais le modèle d’empilement de NavigationStack fait jouet à côté d’une vraie barre latérale Mac. Si l’application donne l’impression d’être inachevée sur Mac, livrez en Catalyst (qui convertit l’application iPad) ou renoncez au Mac jusqu’à pouvoir construire une UI nativement Mac.

Faites l’impasse sur l’iPad si vous n’avez pas travaillé l’adaptation par classes de taille. Une application iPhone étirée pour remplir un iPad fait bas de gamme. L’iPad réclame au minimum un NavigationSplitView avec barre latérale, idéalement une vraie disposition à deux volets. Return utilise des vues partagées sur iPad et des piles sur iPhone : même cible, mais une UI réellement différente.

La règle que j’ai tracée : livrez sur une plateforme quand l’interaction principale de l’application épouse le modèle d’entrée de cette plateforme. Livrez un minuteur de méditation sur Apple Watch (un appui pour démarrer). Livrez un minuteur de méditation sur Apple TV (régler, puis oublier). Ne livrez de tableau kanban ni sur l’une ni sur l’autre.

Ce qui voyage sans effort

Les trois choses qui, dans Return, se sont bel et bien partagées entre les cinq plateformes :

  1. Le modèle de données (MeditationSession). La struct est identique partout, se synchronise via NSUbiquitousKeyValueStore, et chaque plateforme peut lire ce qu’une autre a écrit.
  2. La vue d’historique des séances (SessionHistoryView). Une List des séances passées s’affiche à l’identique sur iPhone, iPad, Mac, Apple Watch et Apple TV. La List de SwiftUI fait partie des rares primitives qui s’adaptent proprement aux cinq facteurs de forme.
  3. La surcouche de persistance (SessionStore). Lectures et écritures sont indépendantes de la plateforme ; le stockage sous-jacent (NSUbiquitousKeyValueStore) repose partout sur la même API.

Trois concepts. L’état, le rendu de liste, la persistance. Tout ce qui relève de l’état et de la présentation, sans dépendre d’un modèle d’entrée matériel, est partageable. Tout ce qui touche à l’entrée, au focus, au routage audio, à la taille d’écran ou à l’exécution en arrière-plan ne l’est pas.

Cette frontière revient dans le guide sur le développement iOS assisté par agents, où je défendais la même idée avec d’autres mots : les parties d’une application iOS qu’un agent sait écrire partagent l’essentiel de leur code avec celles qu’un humain écrit ; les parties qui exigent du jugement humain (signature, finition visuelle, performance) sont exactement celles qui se partagent mal d’une plateforme à l’autre.9 Les deux frontières coïncident. Toutes deux marquent l’endroit où la connaissance du domaine se met à compter.

Ce que coûte le multiplateforme

Le ROI est asymétrique. Ajouter l’iPad à une application iPhone coûte peut-être 20 % de code en plus (branches par classe de taille, vue partagée par endroits). Ajouter le Mac à cette même cible en rajoute 15 à 20 % (branches #if os(macOS), barre de menus, gestion des fenêtres). Chaque cible majeure représente environ 10 fichiers pour une petite application.

L’Apple Watch et l’Apple TV, elles, coûtent cher. Ajouter watchOS à Return a réclamé 11 nouveaux fichiers dans une cible séparée, dont des gestionnaires audio, minuteur et HealthKit dédiés. Ajouter tvOS en a réclamé 10 de plus dans une autre cible séparée, dont la gestion du focus et un sélecteur de durée sur mesure. À elles deux, elles ont presque doublé la surface Swift pour ce qui reste, au niveau des fonctionnalités utilisateur, la même application.

Le choix de livrer sur les cinq ne relevait pas d’un « on veut être multiplateforme pour le principe ». C’était une série de décisions distinctes : l’Apple Watch parce qu’un minuteur de méditation a vraiment sa place au poignet, l’Apple TV parce que le format d’écran ambiant convient aux longues séances dans une pièce, le Mac parce que certains utilisateurs méditent à leur bureau entre deux réunions. Chaque plateforme a mérité sa cible en apportant un vrai cas d’usage.

Quand une fonctionnalité ne mérite pas sa cible, le geste le moins coûteux consiste à renoncer à la plateforme et à redoubler d’efforts là où l’application excelle.

Ce que cela signifie pour votre application

Trois enseignements.

  1. Par défaut, une cible par grand groupe de plateformes. iOS + iPadOS + macOS dans une seule cible tient la route parce que l’interaction principale (toucher et curseur) se ressemble. tvOS dans une cible séparée. watchOS dans une cible séparée. Chaque cible séparée coûte une dizaine de fichiers, mais vous épargne une classe fourre-tout dont les branches #if grossissent sans fin.
  2. Partagez l’état sans retenue, jamais l’interaction. Les structs Codable du modèle, les surcouches de persistance et les rendus de List voyagent presque gratuitement. Les gestionnaires de minuteur, les gestionnaires audio et les vues de contenu, non.
  3. Faites mériter sa place à chaque plateforme. Ne livrez pas sur watchOS parce que c’est possible. Livrez quand l’interaction principale de votre application épouse le modèle d’entrée de la plateforme. Renoncez au reste.

Cette approche complète les trois autres surfaces dont j’ai parlé pour la même famille d’applications : des App Intents typés pour Apple Intelligence, des serveurs MCP pour les agents multi-LLM, Liquid Glass pour l’humain devant l’appareil. La couche la plus extérieure de cette pile, c’est la plateforme : les écrans sur lesquels l’application tourne, tout simplement. Choisissez-la aussi délibérément que vous choisissez la surface IA.

FAQ

Pourquoi ne pas passer par un Swift package pour le code partagé ?

J’y ai pensé. Pour trois fichiers, un Swift package apporte plus de cérémonie qu’il n’en fait gagner. Le système de build de Xcode 26 compile sans difficulté un même fichier source dans plusieurs cibles dès que vous cochez les cases Target Membership. Un package, lui, ajoute un Package.swift séparé, une cible de tests séparée et une indirection que chaque refactorisation devra traverser. Pour un petit noyau partagé, la réponse la plus simple l’emporte.10

SwiftData fonctionne-t-il sur watchOS et tvOS ?

SwiftData est disponible sur iOS 17+, macOS 14+, watchOS 10+ et tvOS 17+, soit toutes les plateformes visées par Return.11 La struct MeditationSession reste un simple Codable, et non un @Model, parce que Return synchronise son historique de séances par NSUbiquitousKeyValueStore plutôt que par un conteneur SwiftData. Le principe vaut de la même manière pour les types @Model : le fichier de modèle se partage, et le conteneur de persistance diffère par plateforme s’il le faut.

Faut-il choisir Mac Catalyst ou une cible Mac native ?

Catalyst est le bon outil quand l’application iPad est assez aboutie pour qu’une version Mac reconstruite par Catalyst passe pour native. La cible principale de Return est une véritable cible multiplateforme (pas Catalyst), construite en SwiftUI pour iOS, iPadOS et macOS dans un seul binaire. L’UI Mac emploie #if os(macOS) pour s’afficher autrement que sur iPad : barre latérale au lieu de feuille, équivalents clavier sur les boutons, etc. Catalyst aurait été plus simple, mais l’UI Mac aurait eu l’air d’une application iPad posée sur un Mac, exactement le mode d’échec pour lequel Catalyst est le plus connu.

Cela vaut-il le coup de livrer sur Apple TV pour une petite application ?

Probablement pas. Les applications Apple TV répondent à des cas d’usage très précis : l’ambiant, les médias, le jeu occasionnel. Si la vôtre n’entre dans aucun d’eux, l’audience de la plateforme est trop réduite pour justifier une dizaine de fichiers Swift supplémentaires. Return vise tvOS précisément parce que de longues séances de méditation sur un écran à l’autre bout de la pièce forment l’un des rares usages proches de la productivité qui collent à la plateforme.

Combien de temps faut-il pour livrer sur les cinq plateformes ?

Difficile de donner un chiffre précis ; tout dépend de l’application. Return a été livré multiplateforme dès le premier jour au lieu d’ajouter les plateformes par incréments, ce qui va plus vite qu’un rattrapage après coup. En règle générale : MVP limité à l’iPhone, plus la prise en charge de l’iPad, plus celle du Mac, représente environ 1,5 fois le temps d’un projet iPhone seul. L’Apple Watch en ajoute 0,5. L’Apple TV encore 0,5. Une première livraison sur cinq plateformes revient donc à peu près à 2,5 fois l’effort d’un projet iPhone seul, avec cette réserve qu’il s’agissait d’un développement assisté par agent où l’essentiel du code dupliqué a été édité en masse par Claude Code plutôt que tapé à la main.

Références


  1. Return de l’auteur, application de minuteur de méditation publiée sur l’App Store le 21 avril 2026. Cibles natives : iOS 26+, iPadOS 26+, macOS 26+, watchOS 26+, tvOS 26+. SwiftUI de bout en bout. NSUbiquitousKeyValueStore pour l’historique de séances multi-appareils. 

  2. Apple Developer, « Configuring a Multi-Platform App » et la session « SwiftUI essentials » de la WWDC 2024. La recommandation Apple par défaut penche pour une cible unique avec adaptation pilotée par l’environnement ; la voie multi-cibles empruntée par cet article est un écart délibéré. 

  3. Code de production dans Return/Return/Shared/MeditationSession.swift, SessionStore.swift, SessionHistoryView.swift. Le commentaire d’en-tête de MeditationSession.swift indique : « Add this file to: Return, ReturnTV, ReturnWatch Watch App targets. » 

  4. Code de production dans Return/Return/VideoBackgroundView.swift (8 branches #if os(iOS) plus une branche #elseif os(macOS)), Return/Return/ContentView.swift (10 branches #if os), Return/Return/AudioManager.swift (6 branches #if os), Return/Return/LiveActivityManager.swift (8 branches #if os, fichier réservé à iOS). Comptages de branches obtenus avec grep -Ec '^\s*#if os\\(' <file>

  5. Apple Developer, Human Interface Guidelines, « Focus interactions ». Le moteur de focus de tvOS repose sur un modèle de navigation fondamentalement différent du toucher sur iOS ou du pointeur sur Mac. 

  6. Code de production dans Return/ReturnTV/TVFocusModifier.swift. Il définit deux types ButtonStyle (TVCapsuleButtonStyle et TVCircleButtonStyle) qui s’appuient sur @Environment(\.isFocused) pour inverser couleur et translucidité au focus, puis appliquer échelle et ombre. 

  7. Apple Developer, « WatchConnectivity ». Le framework dédié à la communication appariée entre iPhone et Watch ; Return ne l’utilise pas pour la synchronisation des séances et s’appuie sur le Key-Value Store iCloud. 

  8. Apple Developer, « WKExtendedRuntimeSession » et la clé Info.plist « WKBackgroundModes ». La valeur mindfulness est documentée ainsi : « Enables extended runtime sessions for silent meditation » — exactement ce qu’il faut pour un minuteur de méditation. Return crée une WKExtendedRuntimeSession() par défaut et déclare WKBackgroundModes: mindfulness dans le Info.plist de la cible watchOS. Code de production : Return/ReturnWatch Watch App/ReturnWatchApp.swift définit WatchSessionManager: NSObject, WKExtendedRuntimeSessionDelegate ; WatchTimerManager.swift lui délègue le travail d’exécution étendue. 

  9. Analyse de l’auteur dans Créer des applications iOS avec des agents IA, le guide du praticien sur le développement iOS assisté par agents, tiré de 8 applications de production. 

  10. Apple Developer, « Configuring a Multi-Platform App ». La Target Membership permet de compiler un même fichier source dans plusieurs cibles sans Swift package. Le bon outil pour les petits noyaux partagés. 

  11. Apple Developer, disponibilité par plateforme de « SwiftData ». Disponible sur iOS 17+, iPadOS 17+, macOS 14+, watchOS 10+, tvOS 17+ et visionOS 1+, soit les cinq familles de plateformes Apple. 

  12. Apple Developer, « NSUbiquitousKeyValueStore ». Le Key-Value Store iCloud d’Apple, conçu pour synchroniser de petites quantités d’état entre les appareils d’un utilisateur. Taille totale plafonnée à 1 Mo pour l’ensemble des clés, selon les limites publiées par Apple. Code de production : Return/Return/Shared/SessionStore.swift

  13. Apple Developer, EnvironmentValues.isFocused. Disponible sur iOS 14+, iPadOS 14+, macOS 11+, tvOS 14+ et watchOS 7+. L’API est multiplateforme ; ce qui change, c’est de savoir si le focus constitue ou non la principale affordance de navigation pour l’utilisateur. 

Articles connexes

La matrice des plateformes Apple : quelles cibles méritent quelle application

iOS, iPad, Mac, Watch, Vision, TV. Six plateformes, six obligations. Choisir ses cibles Apple relève du produit avant de…

25 min de lecture

HealthKit + SwiftUI sur iOS 26 : autorisation, types d'échantillons et patterns multi-plateformes tirés de la mise en production de deux apps

Patterns HealthKit réels issus de Water (HKQuantitySample) et Return (HKCategorySample) : UX des permissions, wrappers a…

21 min de lecture

Installer et mettre à jour Codex CLI : Mac, Linux, Windows

Toutes les façons d'installer, de mettre à jour, d'épingler et de désinstaller la CLI OpenAI Codex -- script d'installat…

22 min de lecture