Dev

Créer une app iOS seul avec Swift : architecture, offline et gamification

Alexandre
Alexandre
··
Durée de lecture : 8 min
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.
Sélection du thème et des objectifs dans le Flow Livate

Cliquer pour agrandir

Découpage en micro-actions et dashboard du Flow Livate

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 identifiantid: 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.
Les 3 états de tâches dans Livate : à faire, en cours, terminée

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é :
  1. Connexion matinale — se connecter avant 10h
  2. 3 actions complétées — terminer 3 tâches dans la journée
  3. Focus 15 minutes — réaliser une session de concentration
  4. Flow terminé — compléter un flow du jour
  5. 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.
Badges du jour dans Livate - système de 5 badges quotidiens

Cliquer pour agrandir

Animation de récompense avec la mascotte Livate

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.
Widgets Livate sur l'écran d'accueil iOS - medium et small

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

À retenir

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

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 ?

Ce site utilise des cookies pour les statistiques de visite et la publicité. Aucune donnée personnelle n'est revendue. En savoir plus