Fiche 06.07 — Environnements dev / staging / prod
Objectif
Séparer proprement les configs (base URL d'API, clés, feature flags, comportements) entre Debug, Staging et Release pour ne jamais taper la prod par accident. Tu sors d'ici avec un AppEnvironment typé qui lit ses valeurs depuis un .xcconfig via Info.plist, et les réflexes attendus en mission.
1. Le problème concret
Une app a au minimum deux back-ends : un environnement de dev/staging (données poubelle, on peut tout casser) et la prod (vrais users, vrais paiements). Le code doit pointer la bonne URL sans recompiler manuellement ni risquer de livrer un build qui tape localhost.
Mauvais réflexe fréquent (et red flag en entretien) :
L'objectif : la valeur change selon la Build Configuration, automatiquement.
2. Build Configurations + Schemes
Xcode fournit Debug et Release. Tu ajoutes une 3e config : Staging.
- Project → Info → Configurations → "+" → Duplicate "Release" → nomme-la
Staging. - Tu te retrouves avec
Debug,Staging,Release.
Un Scheme dit quelle config utiliser pour chaque action (Run, Archive…). Crée un scheme par environnement pour switcher en un clic.
- Product → Scheme → Manage Schemes → "+" →
MonApp-Staging. - Édite-le : Run → Build Configuration = Staging, Archive → Build Configuration = Staging.
Réflexe UIKit → identique : Build Configurations et Schemes existent depuis toujours, rien de SwiftUI ici. Ce qui change souvent en mission c'est qu'on formalise Staging au lieu de bricoler avec #if DEBUG.
Résultat : 3 schemes (Debug via le scheme principal, Staging, Release), chacun mappé sur sa config.
3. Fichiers .xcconfig
Un .xcconfig est un fichier texte de Build Settings. On y déclare une variable par config.
Config/Debug.xcconfig :
Config/Staging.xcconfig :
Config/Release.xcconfig :
Piège classique : // démarre un commentaire dans un .xcconfig, donc https:// est tronqué. L'astuce $() (variable vide) casse la séquence // sans rien afficher. Tu stockes donc l'URL sans schéma et tu le rajoutes côté Swift, ou tu utilises ce hack.
Branche chaque fichier : Project → Info → Configurations, déplie Debug/Staging/Release et assigne le .xcconfig correspondant à la cible.
4. Exposer la valeur via Info.plist
Les Build Settings ne sont pas lisibles à l'exécution. On les fait transiter par Info.plist, qui supporte la substitution $(VARIABLE).
Dans Info.plist (clic droit → Add Row, ou édition source) :
À la compilation, Xcode remplace $(API_BASE_URL) par la valeur du .xcconfig de la config active. À l'exécution, Bundle.main les lit.
5. Le type AppEnvironment
On centralise tout dans un type qui lit Bundle.main une seule fois.
Usage :
fatalError ici est volontaire : si une clé manque, c'est une erreur de configuration de build, pas un cas runtime à gérer mollement. Mieux vaut un crash immédiat et explicite qu'une URL nil silencieuse.
6. #if DEBUG : utile mais à doser
#if DEBUG est une compilation conditionnelle (le flag DEBUG est posé par défaut sur la config Debug). Utile pour exclure du code de la prod :
À retenir :
#if DEBUGest binaire (Debug vs le reste). Staging compile comme Release →DEBUGy est faux. Pour distinguer 3 environnements, passe parAppEnvironment, pas par#if.- Si tu veux un flag sur Staging, ajoute
-DSTAGINGdansOTHER_SWIFT_FLAGSdu.xcconfigStaging, puis teste#if STAGING. - N'enferme jamais une logique métier dans
#if DEBUG: le code non compilé en Release n'est pas testé et diverge. Réserve-le au logging/debug.
7. Secrets et .gitignore
Les .xcconfig peuvent contenir des valeurs sensibles (clés tierces). Règle non négociable :
Tu committes un Secrets.example.xcconfig (clés vides, documenté) et chaque dev/CI fournit le vrai. Aucun secret en clair dans le repo, même historique.
Point crucial : une clé d'API embarquée dans l'app n'est jamais secrète — un .ipa se décompile en 2 minutes. Pour les vrais secrets (clés de signature serveur, tokens privilégiés), le pattern correct est un backend proxy : l'app appelle ton serveur, ton serveur détient la clé et appelle le tiers. Voir la fiche Sécurité. Le .xcconfig ne sert qu'à séparer les configs publiques par environnement.
8. Feature flags simples
Un flag = un comportement activable sans rebuild de logique. Version locale, suffisante pour démarrer :
(value(for:) est private mais reste accessible depuis une extension déclarée dans le même fichier. Si tu sépares l'extension dans un autre fichier, passe la fonction en fileprivate→internal ou regroupe les flags dans le corps de l'enum.)
Au-delà, on branche un service distant (Firebase Remote Config, LaunchDarkly) pour basculer un flag sans nouvelle release. Le principe reste : la vue lit un bool, jamais une condition de build dispersée.
9. Sélecteur d'environnement caché (QA)
En Debug/Staging, la QA aime switcher d'URL sans réinstaller un autre build. On stocke un override en UserDefaults, lu en priorité par AppEnvironment, et exposé via un écran de réglages masqué.
Côté AppEnvironment, lis l'override avant la valeur du bundle :
Réflexe UIKit → l'écran caché se déclenchait souvent via un geste (tap multiple sur le logo, secousse via motionEnded). En SwiftUI tu fais pareil avec .onShake custom ou un tap répété sur un élément, et tu présentes DebugSettingsView en .sheet. Le tout sous #if DEBUG pour qu'il disparaisse de la prod.
Points à connaître
//dans un.xcconfigest un commentaire : ton URL est tronquée silencieusement. Utilise le hack$()ou stocke l'URL sanshttps://.$(VAR)non substitué : siInfo.plistaffiche littéralement$(API_BASE_URL), le.xcconfign'est pas branché sur la config, ou la clé n'existe pas dans ce.xcconfig. Vérifie l'onglet Configurations.#if DEBUGne couvre pas Staging : Staging compile en Release,DEBUGy est faux. Ne crois pas qu'un override#if DEBUGprotège ta QA Staging — il faut un flag dédié ou la lecture runtime.- Secret committé = secret brûlé : même supprimé ensuite, il reste dans l'historique Git.
.gitignoreAVANT le premier commit, et toute clé sensible passe par un backend proxy.
Exercice (15 min)
- Dans un projet neuf, ajoute la config Staging (Duplicate Release) et crée 3
.xcconfig:Debug,Staging,Release, chacun avecAPI_BASE_URLdistinct. - Branche les
.xcconfigsur les configs et ajoute la ligneAPI_BASE_URL = $(API_BASE_URL)dansInfo.plist. - Écris l'enum
AppEnvironmentavecstatic let baseURL: URLlu depuisBundle.main. - Affiche
AppEnvironment.baseURL.absoluteStringdans unText. - Crée un scheme
MonApp-Stagingmappé sur la config Staging. Lance le scheme principal puis le scheme Staging : l'URL affichée doit changer sans toucher au code.
Bonus : ajoute ENABLE_LOGGING et un print conditionné par AppEnvironment.isLoggingEnabled.
Question d'entretien
Q : Comment gères-tu dev vs prod (URL d'API, clés) dans une app iOS ?
R : Je crée des Build Configurations (Debug, Staging, Release) avec un .xcconfig par config définissant API_BASE_URL et mes flags. Je les expose via Info.plist ($(API_BASE_URL)) et je les lis à l'exécution dans un type AppEnvironment centralisé, typé, qui crashe si une clé manque. Un scheme par environnement permet de switcher en un clic, y compris pour l'archive. Les .xcconfig de secrets sont hors Git ; et comme une clé embarquée n'est jamais réellement secrète, tout ce qui est sensible passe par un backend proxy, pas par l'app.
Q : Pourquoi pas juste #if DEBUG partout ?
R : Parce que c'est binaire (Debug vs reste) : Staging compile comme Release, donc DEBUG y est faux, et je ne peux pas distinguer 3 environnements. Surtout, le code sous #if DEBUG n'est pas compilé en Release, donc jamais testé en conditions de prod — il diverge. Je le réserve au logging et aux outils QA, jamais à la logique métier ni au choix d'environnement, qui passent par une lecture runtime.
Résumé
- 3 Build Configurations (Debug / Staging / Release) + 1 scheme chacune pour switcher proprement.
- Valeurs par config dans des
.xcconfig→ exposées viaInfo.plist$(VAR)→ lues dans unAppEnvironmenttypé viaBundle.main. //est un commentaire en.xcconfig: attention aux URLs tronquées.#if DEBUG= binaire, ne couvre pas Staging ; réserve-le au debug/QA, pas à la logique.- Secrets hors Git (
.gitignore) ; une clé embarquée n'est pas secrète → backend proxy pour le sensible. - Sélecteur d'environnement caché en Debug (override
UserDefaults) pour la QA.