# Simone.paris — développer une app iOS soignée en équipe > Retour d'expérience sur le développement de l'app Simone.paris pour iOS : architecture réseau avec le pattern Decorator, animations synchronisées au clavier, vues custom CoreGraphics et UICollectionViewLayout sur mesure. Date : 12/05/2015 Auteur : Aurélien N. Tags : Swift, iOS, Architecture, UI, Animation --- Simone.paris, c'est un service de coiffure à domicile à Paris. L'app iOS permet de choisir une prestation, sélectionner un créneau, renseigner ses coordonnées et payer — le tout en quelques écrans. On a développé l'app fin 2014, livrée début 2015. L'objectif était clair : une app fluide, avec un parcours de réservation sans friction. Le design était ambitieux pour une v1 — animations de scroll, champs de texte avec états visuels, layout custom pour les créneaux horaires. Rien d'insurmontable individuellement, mais le tout devait être cohérent et agréable. Cet article revient sur les choix techniques et les petites astuces qui ont rendu ça possible. ## Le contexte L'équipe : Emmanuelle au développement, Aurélien en encadrement technique. Le backend est une API Rails classique qui expose des endpoints JSON. Côté iOS, on part sur Swift — le langage a moins d'un an, mais les bénéfices par rapport à Objective-C sont déjà évidents sur un projet neuf. Le projet cible iOS 7.1. On utilise des Storyboards pour la navigation, AFNetworking pour le réseau, et MFSideMenu pour le menu latéral. Pas de reactive, pas de MVVM — du MVC classique, mais pensé à l'avance. ### Les dépendances Le `Podfile` reste sobre : - **AFNetworking** — le standard pour le HTTP en 2015 - **MBProgressHUD** — indicateurs de chargement - **UICKeyChainStore** — persistance sécurisée du token - **HexColors** — couleurs hex dans le code - **TTTAttributedLabel** — texte enrichi - **Reachability** — état du réseau Pas de librairie miracle. Chaque pod est là pour un besoin précis, et aucun n'empiète sur l'architecture de l'app. ## Le réseau : un Singleton décoré C'est le choix d'architecture qui a le mieux vieilli. Tout le réseau passe par un `SimoneHTTPManager` — un singleton qui hérite de `AFHTTPRequestOperationManager` et expose une méthode par endpoint. ### Le parsing générique Plutôt que de répéter la logique de succès/erreur dans chaque appel, on a factorisé avec des fonctions génériques. Le principe : une seule fonction `successBlock` reçoit un bloc de parsing et wrappe la gestion d'erreur : ```swift let success = successBlock(completion: completion) { (op, obj) -> [Service]! in if let array = obj as? [[String: AnyObject]] { return array.map } return nil } super.GET("services.json", parameters: params, success: success, failure: failureBlockWithCompletion(completion: completion)) ``` Chaque endpoint se résume à son bloc de parsing. La vérification du status code, l'extraction du message d'erreur, la construction de `NSError` — tout est centralisé dans `checkForProtocolError`. Le ViewController qui consomme l'API ne voit rien de cette mécanique. ### Le pattern Decorator pour le HUD Le `SimoneWrapper` décore le manager en wrappant chaque completion. L'astuce : le HUD ne s'affiche qu'après 250ms, et un flag `fired` empêche de l'afficher si la requête a déjà abouti : ```swift private func prepareWrappedCompletion( completion: (U!, NSError!) -> Void ) -> (U!, NSError!) -> Void { var fired = false dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(0.25 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) { [weak self] in if fired MBProgressHUD.showHUDAddedTo(self?.vc?.view, animated: true) } return } ``` Côté ViewController, l'accès est transparent via une extension qui utilise `objc_getAssociatedObject` pour créer un wrapper par écran, à la volée. Un simple `self.api.getServices(...)` suffit — le loader s'ajoute tout seul. ## Les animations UI ### Synchroniser avec le clavier Adapter le scroll quand le clavier apparaît, avec une animation parfaitement calée. La plupart des tutoriels Swift de l'époque se contentent d'un `animateWithDuration(0.3)` codé en dur. Le résultat : une animation de scroll qui ne colle jamais tout à fait avec celle du clavier. La vraie durée, la vraie courbe — elles sont dans le `userInfo` de la notification, mais la documentation Swift ne les mentionne pas. C'est notre expérience d'Objective-C qui nous a mis sur la piste. En ObjC, lire `UIKeyboardAnimationDurationUserInfoKey` et `UIKeyboardAnimationCurveUserInfoKey` est un pattern établi depuis iOS 4. Mais en Swift 1.2, les valeurs sont masquées derrière des casts `AnyObject` et des conversions numériques non triviales : ```swift let duration = (info[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue let curve = (info[UIKeyboardAnimationCurveUserInfoKey] as! NSNumber).integerValue let options = UIViewAnimationOptions(rawValue: UInt(curve << 16)) UIView.animateWithDuration(duration, delay: 0, options: options, animations: , completion: nil) ``` Le même pattern sert pour `keyboardWillHide` : on relit durée et courbe depuis la notification de disparition. Le résultat est une synchronisation pixel-perfect entre le clavier système et notre scroll — un détail que l'utilisateur ne remarque pas consciemment, mais qui fait toute la différence de polish. ### Le header qui s'efface au scroll Sur l'écran des prestations, les en-têtes de section deviennent transparents quand leurs cellules scrollent en dessous. L'idée : calculer le chevauchement entre le header et sa première cellule, dans le repère de la vue. Le code essentiel tient en quelques lignes : ```swift func scrollViewDidScroll(scrollView: UIScrollView) { if let header = tableView.headerViewForSection(section) as? TreatmentTableHeaderView { let headerRect = view.convertRect(header.bounds, fromView: header) if let firstCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: 0, inSection: section)) } } ``` `convertRect:fromView:` fait tout le travail — on ramène les frames dans un repère commun. Quand le header recouvre entièrement la cellule, alpha vaut 1 ; quand la cellule défile plus loin, alpha tend vers 0. ## Les vues custom ### SimoneTextField — un champ avec des états Le design demandait des champs de texte dont la bordure change de couleur : gris au repos, foncé quand actif, rouge en erreur. L'idée : un `switch` sur un tuple Swift qui capture les deux dimensions d'état : ```swift private func borderColor() -> UIColor { switch (isError, isFirstResponder()) } ``` La bordure est mise à jour à trois moments : changement d'état d'erreur via `didSet`, et changement de focus via les overrides de `becomeFirstResponder` / `resignFirstResponder`. Le pattern matching sur tuple remplace une cascade de `if/else` — c'est une construction Swift qui nous a beaucoup servi dans ce projet. ### StrikedLabel et Separator — du sur-mesure sans librairie Pour afficher un ancien prix barré, une diagonale CoreGraphics par-dessus le texte suffit : ```swift override func drawTextInRect(rect: CGRect) { super.drawTextInRect(rect) guard strikeEnabled else let ctx = UIGraphicsGetCurrentContext() CGContextMoveToPoint(ctx, rect.origin.x, rect.maxY) CGContextAddLineToPoint(ctx, rect.maxX, rect.origin.y) CGContextStrokePath(ctx) } ``` Pour les séparateurs en pointillés, un `patternImage` et une hauteur d'exactement un pixel physique — `1.0 / UIScreen.mainScreen().scale` — donnent un trait net sur tous les écrans, Retina ou non. ## Le layout custom pour les créneaux horaires L'écran de sélection de créneau affiche les disponibilités en colonnes — matin, après-midi, soir. Un `UICollectionViewFlowLayout` ne convient pas : on veut des colonnes de tailles variables avec un header par période. Le layout se résume à deux méthodes : `collectionViewContentSize` calcule la hauteur à partir de la colonne la plus longue, et `layoutAttributesForElementsInRect` positionne chaque élément : ```swift override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? { let sectionWidth = collectionView!.bounds.width / CGFloat(sections) for section in 0.. C'est du MVC classique, mais chaque niveau de la hiérarchie a une responsabilité unique. On évite les "God ViewControllers" de 500 lignes. ### Storyboards multiples et callbacks typés On a découpé l'app en storyboards par domaine fonctionnel : Login, Menu, Cards (paiement), UserProfile, Contact, Root. Chaque storyboard reste lisible — cinq ou six écrans maximum. Pour le couplage entre écrans, des factory methods avec callbacks typés. Le `LoginViewController` en est un bon exemple : ```swift class func loginController( loginCallback: UserAuthorizationCallback, cancel: UserAuthorizationCancelCallback ) -> UIViewController ``` N'importe quel écran peut présenter le login sans connaître les détails d'implémentation. Le jour où on a ajouté l'inscription Facebook, seul le `LoginViewController` a changé. ## Ce qu'on en retient Le projet Simone nous a confirmé plusieurs intuitions : - **Swift tient ses promesses** même en version 1.2. Les optionnels, le pattern matching sur les tuples, les génériques — tout ça rend le code plus sûr sans le rendre plus verbeux. - **Le pattern Decorator sur le réseau** a été le meilleur investissement. Séparer la logique métier du feedback visuel, c'est ce qui a permis à Emmanuelle de travailler sur les écrans sans se soucier du loading. - **Les vues custom restent le meilleur outil** pour des designs précis. Sous-classer `UILabel` ou `UITextField`, override une méthode de dessin — c'est souvent plus simple et plus fiable qu'empiler des sous-vues. - **Un `UICollectionViewLayout` custom** demande un peu de réflexion, mais le résultat est incomparablement plus propre qu'un détournement de `FlowLayout`. L'app a été livrée dans les temps, avec un rendu fidèle aux maquettes. La base de code est restée maintenable — on a pu la reprendre sereinement un an plus tard pour des ajustements. --- *Lire aussi : [Pourquoi Rails pour le backend d'une app iOS](/blog/simone-paris-rails-backend-app-ios) — le retour de Raphaël sur l'architecture côté serveur, ActiveAdmin et la machine à états.*