# Une app iPad native en Swift pour la location de véhicules événementiels > Protocoles avec associated types, génériques pour le réseau, vues custom CoreGraphics — les patterns Swift qui ont structuré une app iPad de gestion de flotte, livrée en deux mois. Date : 18/07/2016 Auteur : Aurélien N. Tags : Swift, iOS, iPad, Architecture --- On vient de livrer une app iPad pour un client de l'événementiel automobile. Gestion de flotte sur site : check-out avec état des lieux, signature du conducteur, relevé d'impacts sur la carrosserie, scan QR code, check-in au retour. Le tout synchronisé avec un back-office Rails via une API REST. Chez imagine-app, chaque projet livré donne lieu à un post-mortem interne. On prend le temps de revenir sur ce qui a fonctionné, ce qui a coincé, et ce qu'on ferait différemment. C'est un exercice qu'on s'impose sur chaque mission — pas pour se flageller, mais parce que les leçons qu'on ne formalise pas, on les oublie. Cet article est le résultat de ce post-mortem. Deux mois de développement, Swift 2 puis 3, déploiement enterprise sur cinq iPads. Voici les points qui ont retenu mon attention. ## Le contexte Flottes de véhicules premium pour des événements : lancements de modèles, salons automobiles, essais presse. Chaque véhicule est assigné à un groupe, confié à un conducteur, inspecté au départ et au retour. Avant l'app, tout ça se faisait sur papier — formulaires perdus, signatures illisibles, pas de traçabilité des impacts. L'app devait tourner sur iPad, en natif. Pas de compromis sur la réactivité — les agents sur le terrain enchaînent les opérations toute la journée. ### Pourquoi du natif Natif ou hybride ? L'alternative, c'est Ionic — Cordova + AngularJS. On connaît, on en a fait. Mais ici ça ne colle pas. On a besoin de : - Capture de signature tactile avec rendu CoreGraphics fluide au doigt - Scan QR code en temps réel via la caméra - Upload multipart avec redimensionnement d'images côté client - Un storyboard optimisé pour l'iPad en mode paysage Ionic dans une web view, avec le bridge Cordova, ça veut dire du lag sur le dessin, des plugins tiers pour la caméra, et des workarounds pour chaque spécificité iPad. L'app est mono-plateforme — iPad uniquement. Pas de deuxième plateforme à amortir. Sur cinq iPads déployés sur le terrain, sans app store, sans possibilité de hotfix rapide, la fiabilité n'est pas négociable. ### Pourquoi Swift Swift a deux ans. Le tooling est rugueux — on y reviendra. Mais les bénéfices par rapport à Objective-C sont déjà évidents. Les optionnels rendent le parsing JSON explicite là où `nil` passait silencieusement en Objective-C. Les protocoles avec associated types offrent des abstractions que les `@protocol` ObjC ne peuvent pas exprimer. Le système de types attrape des classes entières de bugs à la compilation. Quand on vient d'Objective-C, où un message envoyé à `nil` retourne silencieusement `nil` (ou 0, ou `NO`, selon le type), la rigueur de Swift est un soulagement. ## L'architecture réseau : protocoles et génériques C'est le morceau d'architecture qui m'a le plus marqué dans ce projet. Tout repose sur un seul protocole. ### Le protocole `JSONLoadable` Parser du JSON en Swift, c'est du `guard let ... as?` en cascade. Pas de `Codable`, pas de `Decodable` — ça n'existe pas encore. En Objective-C, on faisait `[json objectForKey:@"id"]` et on priait pour que ce soit un `NSNumber`. En Swift, le compilateur nous force à vérifier chaque cast. C'est verbeux, mais c'est honnête. On a défini un protocole qui unifie le chargement depuis l'API : ```swift protocol JSONLoadable { static var Collection: [Self] associatedtype ReloadCallback = (_ results: [Self]?, _ error: Error?)->() static func reload(_ callback: ReloadCallback) static func load(_ json: [String:AnyObject]) throws -> Self } ``` Trois choses ont retenu mon attention. **L'`associatedtype` pour le callback.** `Assignment.ReloadCallback` et `Driver.ReloadCallback` sont des types distincts, vérifiés à la compilation. En Objective-C, on aurait eu un `typedef void (^CompletionBlock)(NSArray *, NSError *)` unique — aucun moyen de savoir si le `NSArray` contient des `Assignment` ou des `Driver`. **`Collection` comme propriété statique mutable.** Un cache en mémoire directement sur le type. Pas besoin d'un singleton `DataStore` ou d'un `CoreData` pour une app de cette taille. **`throws` sur `load`.** En Objective-C, le pattern classique c'est `initWithDictionary:error:` avec un `NSError **` qu'on oublie de vérifier. `throws` rend l'erreur impossible à ignorer : il faut un `try`, un `try?` ou un `try!`. ### Les génériques au service du réseau Le vrai gain apparaît dans les fonctions réseau. Deux fonctions génériques suffisent pour charger n'importe quel modèle conforme : ```swift func loadCollection(_ path: String, callback: ((_ results: [T]?, _ error: Error?)->())?) { Alamofire.request("\(APIBase)/\(path)", headers: httpHeaders) .validate(statusCode: 200..<300) .responseJSON { response in processQueue.async { var error: Error? var results: [T]? do { switch response.result { case .success(let value): if let value = value as? [[String:AnyObject]] { results = try value.map } case .failure(let e): Crashlytics.sharedInstance().recordError(e) error = e } } catch let e if let results = results DispatchQueue.main.async } } } ``` Le compilateur infère `T` depuis le type du callback. Impossible en Objective-C — les generics ObjC (`NSArray *`) sont de la documentation, pas des contraintes. Ici, le compilateur *vérifie* : ```swift loadCollection("api/assignments") ``` Parsing typé, mise à jour du cache, gestion d'erreur Crashlytics, dispatch main queue. Zéro duplication entre les modèles. Le traitement se fait sur une background queue pour ne pas bloquer le main thread, puis le callback revient sur la main queue. Un `UITableView.reloadData()` appelé depuis une background queue, c'est un crash silencieux et intermittent — le genre de bug qu'on ne trouve qu'en production. ### Des erreurs typées par modèle Chaque modèle définit ses propres erreurs via une enum imbriquée : ```swift extension Assignment: JSONLoadable { enum Errors: Error static func load(_ json: [String:AnyObject]) throws -> Assignment { guard let id = json["id"] as? Int else // ... } } ``` `Errors.invalidDate("2016-13-42", forField: "planned_time_in")` — on sait exactement ce qui a échoué. Quand ça remonte dans Crashlytics, on n'a pas besoin de deviner. En Objective-C, on aurait un `NSError` avec un `userInfo` dictionary et un `code` entier. Trois informations non typées, que personne ne vérifie à la compilation. Le pattern se répète pour `LoanRecord` avec des validateurs dédiés : ```swift fileprivate static func checkChoices(_ json: [String:AnyObject], parameterName: String) throws -> Int { guard let value = json[parameterName] as? Int else if value < 0 || value > 4 return value } ``` Niveau d'huile, propreté intérieure, propreté extérieure — des champs entre 0 et 4. En production, sur le terrain, "espérer que l'API renvoie des valeurs valides" n'est pas une stratégie. ## L'organisation du code : extensions et séparation ### Extensions par fichier Un choix inspiré par [objc.io](https://www.objc.io/issues/) : le modèle et son parsing vivent dans des fichiers séparés. ``` Assignment.swift → le modèle pur Assignment+JSONLoadable.swift → la conformité au protocole, le parsing JSON ``` `Assignment.swift` ne connaît ni Alamofire, ni JSON, ni Crashlytics : ```swift final class Assignment { let id: Int let brandName: String let licensePlateNumber: String // ... var status: String { if checkInRecord != nil else if checkOutRecord != nil return "in" } } ``` En Objective-C, les catégories (`Assignment+JSONLoadable.h/.m`) ne pouvaient pas ajouter de stored properties ni déclarer de conformités typées. Les extensions Swift le peuvent — conformité, types imbriqués, méthodes `static`. C'est une vraie séparation de préoccupations, pas juste un découpage de fichier. Fichiers courts, diffs lisibles, merge conflicts rares. ### Le logging : surcharge et enums Fonctions libres qui wrappent Crashlytics avec compilation conditionnelle : ```swift func Log(_ msg: T) { #if DEBUG debugPrint(msg) #endif if let msg = msg as? CVarArg } func Log(_ v: Int, forKey key: String) ``` Générique pour les messages, spécialisé pour les métriques. Swift résout la bonne surcharge à la compilation. Pour l'analytics, une enum `ViewType` structure le tracking : ```swift enum ViewType ``` Pas de strings magiques dans les view controllers. Le switch exhaustif garantit qu'on n'oublie pas de tracker un nouvel écran. ## Les composants custom : CoreGraphics et UIKit C'est la partie du projet où le natif justifie pleinement son coût. Trois composants écrits à la main, sans lib tierce. ### La capture de signature Le conducteur signe sur l'iPad. La signature est capturée comme une série de segments de points, stockée en JSON, restituée à l'identique. Le trick central : **une résolution fixe de 400x400 points**. Quelle que soit la taille de la vue, les coordonnées sont normalisées. La signature devient indépendante du device — sérialisable, restituable à n'importe quelle taille. ```swift class SigningView: UIView { fileprivate let resolution: CGFloat = 400 fileprivate var locationScale: CGFloat fileprivate func scaleLocation(_ location: CGPoint) -> CGPoint ``` Le `Segment` implémente `Sequence` tout en gardant la sémantique de référence (`class`, pas `struct`) pour append pendant le tracé : ```swift class Segment: Sequence { var currentPoints: [CGPoint] = [] func makeIterator() -> IndexingIterator<[CGPoint]> func append(_ point: CGPoint) } typealias SignatureData = [Segment] ``` Le rendu utilise `UIGraphicsGetCurrentContext()` directement : ```swift fileprivate func strokeAllsignatureData(_ size: CGSize) { let scale = size.width / resolution let ctx = UIGraphicsGetCurrentContext() ctx?.setLineCap(.round) ctx?.setLineWidth(5) ctx?.setStrokeColor(red: 0, green: 0, blue: 0, alpha: 1) for segment in signatureData { var firstPoint = true for point in segment { if firstPoint else } if !firstPoint } } ``` La même fonction sert pour l'affichage (`draw(_:)`) et pour l'export en `UIImage`. Pas de duplication, pas de divergence entre ce que l'utilisateur voit et ce qui part au serveur. ### Le relevé d'impacts : `didSet` comme réaction Poser des marqueurs sur un schéma de véhicule — chocs, rayures, enfoncements. L'utilisation de `didSet` m'a frappé par sa simplicité : ```swift var impacts: ImpactData = [] { didSet { for v in self.impactViews impactViews.removeAll() for data in impacts } } ``` Modifier `impacts` rafraîchit la vue automatiquement. Pas de `reloadImpacts()` à oublier. En Objective-C, on aurait utilisé KVO — `addObserver:forKeyPath:options:context:`, avec ses strings non vérifiées et ses `removeObserver` oubliés qui crashent. `didSet`, c'est KVO sans la souffrance. Le type `ImpactType` utilise un switch exhaustif pour la sérialisation : ```swift enum ImpactType: StringLiteralType static func load(_ s: String) throws -> ImpactView.ImpactType { switch s } ``` Ajouter un type d'impact ? Le compilateur force à gérer le nouveau cas partout. En Objective-C, un `typedef NS_ENUM` ne vérifie pas l'exhaustivité du switch. ### Le scan QR code : AVFoundation et queues AVFoundation directement, pas de lib tierce. La complexité est encapsulée derrière une API minimale : ```swift class func showIn(_ vc: UIViewController, done: Callback?) { let captureVC = QRCodeCaptureViewController() let nav = UINavigationController(rootViewController: captureVC) vc.present(nav, animated: true, completion: nil) captureVC.callback = { code, error in vc.dismiss(animated: true) } } ``` Un appel, un callback. En coulisses, une serial queue privée reçoit les callbacks AVFoundation pour éviter les race conditions, puis dispatche sur la main queue : ```swift fileprivate let queue = DispatchQueue(label: "QRCodeCaptureViewController", attributes: []) func startCapturing() throws ``` Toujours spécifier explicitement sur quelle queue on reçoit les résultats. C'est une règle de base en iOS, mais AVFoundation ne pardonne pas si on l'oublie. ## L'infrastructure : thème, images et déploiement ### Le thème : `UIAppearance` proxy Un pattern sous-estimé en UIKit — `appearance()` style globalement tous les éléments d'un type : ```swift class Theme { static var currentTheme: Theme { struct Singleton return Singleton.instance } func load() } ``` Un appel dans l'`AppDelegate`, toutes les navigation bars sont stylées. Le `whenContainedInInstancesOf:` cible les labels *à l'intérieur* des headers — un sélecteur CSS avant l'heure. Le `struct Singleton` est le singleton thread-safe idiomatique en Swift — `dispatch_once` sous le capot. En Objective-C, c'était sept lignes de boilerplate. ### Le cache d'images : Kingfisher Chaque contexte d'affichage a son `ImageProcessor` adapté à l'écran : ```swift static let impactProcessor: ImageProcessor = () static let backgroundProcessor: ImageProcessor = () ``` 12 points logiques = 24px en Retina, 36px sur iPad Pro. Le cache stocke l'image déjà redimensionnée. Les closures `static let` avec `= ()` sont évaluées une seule fois, lazily. ### Fastlane : le pipeline enterprise Pas d'App Store pour ce projet — distribution enterprise. Fastlane gère le pipeline complet : ```ruby lane :alpha do ensure_git_status_clean prebuild notes = build_notes increment_build_number badge(alpha: true) gym(scheme: "Release-Staging", use_legacy_build_api: true) crashlytics(notes: notes, api_token: ENV["CRASHLYTICS_API_TOKEN"], build_secret: ENV["CRASHLYTICS_BUILD_SECRET"]) slack(message: "New alpha available on Crashlytics !") reset_git_repo(force: true, skip_clean: true, exclude: "artifacts") end ``` iPads enregistrés par UDID, builds taggés dans Git, icône badgée "ALPHA", distribution Crashlytics Beta, notification Slack. Le `reset_git_repo` en fin de lane annule les modifications temporaires. Le repo reste propre. Deux schemes séparent les environnements : `Release-Staging` et `Release-Prod`. L'URL de l'API vit dans `Info.plist`, pas dans le code. ### CocoaPods + Carthage Les deux. CocoaPods pour Fabric et Crashlytics (scripts de build-phase obligatoires). Carthage pour Alamofire en binaire — le build incrémental est plus rapide, on ne recompile pas la lib à chaque clean build. Un vrai gain quand le compilateur Swift est déjà lent. Swift Package Manager ne supporte pas encore iOS. On verra. ## La migration Swift 2 → Swift 3 Swift 3 vient de sortir avec Xcode 8. On migre en cours de projet. L'outil automatique fait 80% du travail, le reste est du nettoyage manuel. Les changements les plus marquants : - **Signatures de méthodes.** `NSNotificationCenter.defaultCenter().addObserver(...)` → `NotificationCenter.default.addObserver(...)`. Chaque fichier est touché. - **`DispatchQueue` au lieu de `dispatch_async`.** `dispatch_async(dispatch_get_main_queue()) ` → `DispatchQueue.main.async `. Plus lisible, mais ça casse tout le code concurrent. - **`AnyObject` vs `Any`.** Swift 3 durcit la distinction. `[String:AnyObject]` doit parfois devenir `[String:Any]`. Erreurs en cascade. - **Enum cases en lowerCamelCase.** `.Sinking` devrait devenir `.sinking`. On garde l'ancienne convention sur `ImpactType` pour ne pas casser la sérialisation — compromis pragmatique. ## Bilan et frustrations ### Ce que Swift apporte Le fil rouge de ce projet : Swift nous *force* à être explicites. Comparé à Objective-C, le gain n'est pas dans la productivité brute — on écrit plus de lignes pour le même résultat. Le gain est dans la *confiance*. Quand le projet compile, on sait que les types sont cohérents, que les optionnels sont gérés, que les switch sont exhaustifs. Ce que je retiens de ce post-mortem : 1. **Protocoles avec `associatedtype` pour le réseau.** `loadCollection` vaut mieux que dix méthodes copiées-collées. Impossible en ObjC. 2. **Extensions par fichier.** `Model.swift` pour le métier, `Model+JSONLoadable.swift` pour le parsing. Plus puissant que les catégories ObjC. 3. **Enums avec valeurs associées pour tout.** Erreurs, logging, analytics. Le compilateur vérifie l'exhaustivité. 4. **Résolution fixe pour les composants graphiques.** 400x400, coordonnées normalisées. Indépendant du device. 5. **`didSet` pour synchroniser état et affichage.** KVO sans la souffrance. 6. **Queues dédiées pour les callbacks système.** Toujours spécifier explicitement. ### Les frustrations Swift est meilleur qu'Objective-C pour ce type de projet. Mais "meilleur" ne veut pas dire "agréable au quotidien". Pas encore. **Temps de compilation.** 46 fichiers Swift, 45 secondes à une minute en clean build. Le compilateur peine sur les closures complexes. Ajouter une annotation de type explicite peut faire passer un fichier de 8 secondes à moins d'une seconde. On apprend à écrire du Swift que le compilateur *aime*. **SourceKit.** "SourceKit terminated, editor functionality temporarily limited" — plusieurs fois par jour. L'autocomplétion disparaît, la coloration syntaxique devient grise. Running gag dans la communauté. Moins drôle sous pression. **La migration en plein projet.** `UIColor.redColor()` → `UIColor.red`. `NSBundle.mainBundle()` → `Bundle.main`. `dispatch_async(dispatch_get_main_queue())` → `DispatchQueue.main.async`. Chaque fichier touché. L'outil automatique laisse des cas ambigus — à résoudre à la main, dans un éditeur dont l'autocomplétion vient de crasher. **Le parsing JSON.** La frustration la plus profonde. 30 lignes de `guard let ... as?` par modèle. Correct, explicite, sûr — et épuisant. Des libs comme SwiftyJSON ou Argo existent, mais elles ajoutent des abstractions pour un problème que le langage devrait résoudre. On espère qu'Apple y travaille. --- Malgré tout ça, on ne reviendrait pas en arrière. Le système de types rattrape en fiabilité ce que le tooling perd en confort. Et sur le terrain, un agent qui utilise l'app huit heures par jour ne voit pas les temps de compilation — il voit une app qui ne crashe pas. C'est le genre de conclusion qu'on n'écrit pas dans un post-mortem interne. Mais c'est celle qui compte.