← Retour aux articlesDev
Créer une app iOS seul avec Swift : architecture, offline et gamification
Dev solo, j'ai construit Livate de zéro en Swift natif. Architecture MVVM, cache-first, widgets, Dynamic Island : retour d'expérience complet avec du vrai code.
Zéro expérience en Swift. Aucun budget. Un seul dev. Et pourtant, 8 mois plus tard, Livate était sur l'App Store. Voici exactement comment j'ai fait.
Si tu es développeur, indie hacker, ou simplement curieux de savoir ce qu'il y a sous le capot d'une app mobile en 2026, cet article est pour toi. Pour le contexte, tu peux lire mon parcours et pourquoi j'ai créé Livate.
Swift natif : pourquoi j'ai refusé le cross-platform
La première décision, et probablement la plus structurante : Swift natif ou cross-platform ?
Cross-platform
- Plus rapide au début
- Performance correcte
- Animations limitées
- Widgets pas natifs
- Dynamic Island compliqué
Swift natif — mon choix
- Plus lent au début
- Performance optimale
- Animations ultra fluides
- Widgets accès complet
- Dynamic Island natif
J'ai choisi Swift + SwiftUI pour une raison simple : Livate est une app qu'on ouvre chaque jour. La fluidité, le haptic feedback, les transitions natives — tout ça se ressent. Et surtout, je voulais les widgets iOS, le Dynamic Island et les Live Activities. En cross-platform, ça aurait été un cauchemar.
Comme je le racontais dans mon aventure, je ne connaissais rien à Swift en commençant. J'ai passé les 3 premières semaines à apprendre le langage en parallèle du dev. C'était intense, mais chaque petit progrès était grisant.
L'architecture : des modules isolés par feature
Le projet est structuré en modules par feature. Chaque feature a ses propres Models, ViewModels, Services et Views :
Livate/
├── Features/
│ ├── Flow/ # Objectifs et parcours guidé
│ ├── Task/ # Micro-habitudes quotidiennes
│ ├── Focus/ # Timer Pomodoro + Dynamic Island
│ ├── Stats/ # Statistiques et score quotidien
│ ├── BadgeReward/ # Gamification (5 badges/jour)
│ ├── Home/ # Dashboard + Mood tracking
│ └── Authentication/
│
├── Core/
│ ├── Services/ # Network, Cache, Security, Sync
│ └── Components/ # Design System (LivateDesignTokens)
│
└── LivateWidgetExtension/ # Widgets Home + Lock Screen
Cette séparation a un vrai avantage : quand je bosse sur les badges, je ne casse pas le Flow. Quand j'ajoute un widget, je ne touche pas aux stats. Ça paraît évident, mais au début je mettais tout dans 3 fichiers. Le jour où
FlowViewModel.swift a dépassé 1800 lignes, j'ai compris qu'il fallait restructurer. Ça m'a pris une semaine complète de refactoring. Douloureux, mais nécessaire.Le Flow : le cœur de l'app
Le Flow, c'est le concept central de Livate — celui que j'expliquais dans pourquoi j'ai créé Livate. En quelques taps, tu définis ton objectif, tu le découpes en micro-actions, et tu passes à l'action.
Voici le modèle qui porte tout ça :
struct FlowLivate: Codable, Identifiable { let id: String var libelleAction: String // L'action du jour var objectif: String? // Objectif lié var etat: FlowEtat // créé, en_cours, terminé var energieRequise: Int? // 1-5 var tempsEstime: Int? // Minutes estimées var tempsRealise: Double? // Temps réel passé var contexte: [String]? // ["fitness", "productivity"] var streakDay: Int // Jours consécutifs var etape: [FlowLivateStep] // Étapes de l'action }
Chaque Flow est rattaché à un des 9 objectifs prédéfinis (3 par thématique : Fitness, Productivité, Routine). L'idée, c'est de ne jamais laisser l'utilisateur face à une page blanche. Tu choisis un objectif, et le Flow te guide.
Le
streakDay est là pour la gamification : plus tu enchaînes les jours, plus ton streak grandit. C'est subtil mais ça crée une boucle d'engagement naturelle.
Cliquer pour agrandir

Cliquer pour agrandir
Tu veux voir le Flow en action ? Télécharge Livate gratuitement.
Le cache-first : l'app marche sans internet
Le choix technique dont je suis le plus fier : Livate fonctionne offline. Pas de loader, pas de "vérifiez votre connexion". Tu ouvres l'app, tes données sont là.
Le secret, c'est le pattern cache-first : on affiche d'abord depuis le cache local (instantané), puis on synchronise avec le serveur en arrière-plan.
@MainActor class FlowLivateViewModel: ObservableObject { @Published var flows: [FlowLivate] = [] func loadFlows(for date: String) async { // 1. Affichage instantané depuis le cache if let cached = await cacheManager.getCachedFlows(userId: userId) { self.flows = cached } // 2. Offline ? On s'arrête là. L'app marche quand même guard networkMonitor.isConnected else { return } // 3. Sync API en arrière-plan let freshFlows = try await repository.fetchFlows(date: date) self.flows = freshFlows await cacheManager.cacheFlows(freshFlows, userId: userId) } }
Le
PremiumCacheManager est un actor Swift (thread-safe par design) avec deux niveaux :- L1 (RAM) : dictionnaire en mémoire, ultra rapide
- L2 (UserDefaults) : persistant, survit au redémarrage
Quand l'utilisateur est offline, les opérations (compléter une tâche, logger un mood) sont mises dans une file d'attente (
OfflineOperationQueue). Dès que la connexion revient, tout se synchronise automatiquement. L'utilisateur ne voit rien.Est-ce que UserDefaults est le choix parfait pour du cache L2 ? Non. Core Data ou SwiftData seraient plus robustes à grande échelle. Mais pour un MVP, c'est pragmatique et ça marche. J'itérerai quand le volume de données le justifiera.
Les micro-habitudes : 3 états, c'est tout
Les tâches sont les micro-actions concrètes de ton Flow. J'ai fait un choix radical de simplicité :
enum TaskState: String, Codable { case aFaire = "a_faire" // Rouge case enCours = "en_cours" // Orange case terminee = "terminee" // Vert }
Pas de sous-tâches, pas de labels, pas de priorités à 5 niveaux. À faire, en cours, terminée. Point. Quand une tâche passe au vert, le système de badges se déclenche et les stats se mettent à jour automatiquement.
Un détail technique que j'aime bien : chaque tâche a un double identifiant —
id: Int? (backend cloud) et idLocal: UUID? (local). Les utilisateurs free travaillent uniquement en local. Quand ils passent premium, le LocalDataMigrationService transfère tout vers le cloud de manière transparente.
Cliquer pour agrandir
Le Focus : Pomodoro dans le Dynamic Island
Le mode Focus, c'est un timer Pomodoro avec un twist iOS : il s'affiche dans le Dynamic Island et sur l'écran de verrouillage via les Live Activities.
Tu lances une session, tu verrouilles ton écran, et tu vois ta progression en temps réel sans rouvrir l'app. C'est exactement le genre de feature qui justifie le choix du natif : en Flutter, ça aurait nécessité 3 plugins, 2 bridges, et probablement un sacrifice rituel.
L'implémentation repose sur
ActivityKit :struct FocusTimerAttributes: ActivityAttributes { public struct ContentState: Codable, Hashable { var remainingSeconds: Int var totalSeconds: Int var isRunning: Bool var progress: Double { 1.0 - (Double(remainingSeconds) / Double(totalSeconds)) } } }
Le widget sur l'écran d'accueil se met aussi à jour en temps réel via un App Group partagé. C'est une des features qui revient le plus dans les retours utilisateurs.
La gamification : 5 badges par jour
Pour garder la motivation, chaque jour tu peux débloquer 5 badges. Les 4 premiers sont des défis indépendants, et le 5ème récompense la régularité :
- Connexion matinale — se connecter avant 10h
- 3 actions complétées — terminer 3 tâches dans la journée
- Focus 15 minutes — réaliser une session de concentration
- Flow terminé — compléter un flow du jour
- Badge ultime — obtenir les 4 badges précédents
C'est un système progressif : chaque badge est accessible, mais les obtenir tous les 5 dans la même journée demande un vrai engagement. La mascotte Livate t'accompagne avec une animation de récompense à chaque badge débloqué — un petit détail qui rend le moment satisfaisant.

Cliquer pour agrandir

Cliquer pour agrandir
Tu veux tester la gamification ? Livate est gratuite sur l'App Store.
Les widgets : Livate sur ton écran d'accueil
Livate propose des widgets pour l'écran d'accueil et l'écran de verrouillage. Petit, moyen, grand — chacun affiche ton flow actif et tes tâches en cours.
La synchronisation passe par un App Group. Quand tu complètes une tâche dans l'app, le widget se met à jour instantanément :
func updateFlowWidget(with flow: FlowLivate) { let encoded = try? JSONEncoder().encode(widgetData) let defaults = UserDefaults(suiteName: "group.com.livate.widgets") defaults?.set(encoded, forKey: "flowData") // Rafraîchir le widget immédiatement WidgetCenter.shared.reloadTimelines(ofKind: "FlowWidget") }
Tu vois ta progression avancer directement sur ton écran d'accueil, sans même ouvrir Livate. C'est un rappel visuel constant qui pousse à l'action.

Cliquer pour agrandir
Le Design System : une décision qui m'a sauvé
Au début, je codais mes couleurs et mes espacements en dur. Chaque écran avait ses propres valeurs. Au bout de 3 mois, c'était ingérable : changer une couleur impliquait de modifier 40 fichiers.
J'ai tout centralisé dans des design tokens :
enum LivateDesignTokens { // Couleurs static let accentPrimary = Color(red: 0, green: 0.6, blue: 0.76) // Typo (SF Pro Rounded) static let titleLarge: Font = .system(size: 34, weight: .bold, design: .rounded) static let body: Font = .system(size: 17, weight: .regular, design: .rounded) // Grille 8pt static let spacingS: CGFloat = 16 static let spacingM: CGFloat = 24 // Animations standardisées static let springStandard = Animation.spring(duration: 0.4, bounce: 0.3) // Haptic feedback static func hapticLight() { UIImpactFeedbackGenerator(style: .light).impactOccurred() } }
Maintenant, quand je veux changer une couleur ou un espacement, je le fais à un seul endroit. Ça m'a pris une semaine à mettre en place. Ça m'a fait gagner des mois depuis.
Les galères que je n'oublierai pas
L'article serait incomplet sans les moments où j'ai failli tout casser :
Le NavigationStack qui perd son état. J'ai passé 4 jours sur un bug où la navigation SwiftUI réinitialisait tout le ViewModel à chaque retour en arrière. Le fix ? Un
@StateObject mal placé. 4 jours pour déplacer une ligne de code.Le parsing de dates multi-format. Mon backend envoie des dates en
"yyyy-MM-dd". Sauf quand il envoie du "yyyy-MM-dd HH:mm:ss.SSS". Sauf quand il envoie de l'ISO 8601. J'ai fini par écrire un custom init(from decoder:) de 60 lignes avec 3 formats en fallback. Pas élégant, mais ça marche.La review Apple rejetée. 3 fois. Je pensais que la soumission App Store serait une formalité. En réalité, j'ai essuyé 3 rejets consécutifs : le premier pour des problèmes légaux (mentions manquantes, conditions d'utilisation), le deuxième pour le design system (inconsistances visuelles, guidelines Apple non respectées), et le troisième pour le système premium/free (distinction pas assez claire entre les fonctionnalités). À chaque rejet, il faut corriger, resoumettre, et attendre. Stressant, mais ça m'a forcé à livrer une app beaucoup plus propre que ce que j'aurais fait sans ces retours.
Ce que j'ai appris
1. Le cache-first, c'est non négociable. Personne ne veut attendre 3 secondes qu'une requête API revienne. Charge depuis le cache, affiche instantanément, sync en arrière-plan.
2. Commence avec un design system. J'ai commencé sans, j'ai dû tout reprendre. Avec les
LivateDesignTokens, chaque nouveau composant est cohérent dès le premier rendu.3. SwiftUI est génial, mais les NavigationStack perdent leur état. Les animations déclaratives sont incroyables. Le debugging de la navigation, beaucoup moins.
4. 5 badges par jour et un streak suffisent. La gamification subtile fonctionne mieux que les systèmes complexes. Les utilisateurs reviennent pour ne pas casser leur streak.
5. L'IA aide énormément en solo. Que ce soit pour analyser un problème, brainstormer une architecture, ou accélérer le développement, des outils comme Claude et Copilot m'ont fait gagner un temps précieux. En 2026, c'est devenu indispensable dans mon workflow quotidien.
Et la suite ?
Livate continue d'évoluer : fonctionnalités sociales, suggestions IA, widgets encore plus riches, et peut-être un jour une version Android.
Mais pour l'instant, je me concentre sur une chose : écouter les retours utilisateurs. Si tu utilises Livate, n'hésite pas à me faire un retour — chaque commentaire me permet d'avancer.
Dans le prochain article, je te parlerai de la méthode Flow — le concept central de Livate et comment il peut t'aider à passer à l'action au quotidien.
À très vite !
Alex
- Swift natif plutôt que cross-platform : plus de travail au début, mais performance native, accès complet aux APIs Apple et zéro compromis UX.
- Cache-first = l'app marche sans internet. Pas de loader, pas de 'vérifiez votre connexion'. L'utilisateur ouvre, ses données sont là.
- Le Design System m'a sauvé : 1 source de vérité pour couleurs, typo, espacement. Sans ça, chaque écran aurait été un cauchemar de cohérence.
- Les micro-habitudes en 3 états (à faire, fait, raté) sont le coeur de Livate. Simple à comprendre, addictif à utiliser.
- Zéro expérience Swift au départ. 8 mois plus tard, app en prod sur l'App Store. En 2026, la barrière d'entrée n'existe plus.
Articles similaires

L'actu IA du 11 octobre 2026 : Mistral Large 4 en open-weight, OpenAI publie 722 manuscrits de maths, Haiku 5.5 à 0,10 $
OpenAI publie 722 manuscrits de maths, Mistral lance Large 4 en open-weight, Anthropic sort Haiku 5.5 à 0,10 $ et le revenu d'OpenAI fait reculer le Nasdaq.
Meta lance Muse, Muse Code et Muse Glimmer : panorama de sa nouvelle gamme d'IA
Muse, l'agent personnel de Meta inspiré d'OpenClaw, a attiré 500 000 utilisateurs en 7 jours. Modèles, prix, accessoire Muse Charm, disponibilité : ce qui sort et quand.
L'actu IA du 2 octobre 2026 : OpenAI lance les Dots au DevDay, Gemini 4 Argon sous clé, Sonnet 5.5 au prix plancher
OpenAI lance les Dots et GPT-6.1 Sol au DevDay, Google garde Gemini 4 Argon sous clé, Anthropic sort Sonnet 5.5. Trois modèles, un même prix. Décryptage.Commentaires
Commentaires
Tu as un avis sur cet article ?
Crée un compte gratuit en 10 secondes pour commenter, laisser des likes et recevoir les prochains articles directement dans ta boîte mail.
Pas encore de compte ?