Développement d'applications iPhone
Conventions tirées de l'expérience de livraison d'une application SwiftUI de bout en bout, du dépôt vide à une version signée fonctionnant sur un téléphone physique.
Contexte supposé : applications à petite échelle, un compte Apple personnel gratuit, aucune distribution sur l'App Store. Un compte développeur payant modifie plusieurs règles ci-dessous — la section sur la signature indique lesquelles.
Architecture par défaut
- SwiftUI + SwiftData, local-first. Une petite application ne devrait pas nécessiter de serveur accessible. Les données résident sur l'appareil dans SwiftData ; les appels réseau sont réservés aux API publiques uniquement (https, pas d'exceptions ATS). Si l'application commence comme un client léger pour un backend existant, prévoyez le pivot « peut-on se passer du serveur ? » — portez la logique du backend en fonctions Swift pures manipulant les tableaux SwiftData plutôt que de maintenir un serveur pour un seul téléphone.
- Modélisation SwiftData : relations d'objets réels, pas de clés étrangères entières.
@Attribute(.unique)pour les clés naturelles. Les tableaux to-many de SwiftData ne préservent pas l'ordre — ajoutez un champsortOrderet une propriété calculéesortedXlorsque l'ordre est important. Les dates sont desDate, pas des chaînes. Les énumérations sontCodableavec une valeur brute de typeString. - Modifications de schéma sur une installation existante : l'ajout de modèles ou de propriétés
optionnelles effectue une migration légère silencieuse au prochain lancement ; les renommages,
suppressions ou changements de type sans
SchemaMigrationPlanempêchent l'ouverture du magasin — l'application déployée plante alors au démarrage avec les données de l'utilisateur intactes mais inaccessibles. Les redéploiements (même bundle ID) n'effacent jamais le conteneur de données ; seule la suppression de l'application le fait. Prévoyez un chemin d'exportation/ sauvegarde avant que les utilisateurs n'accumulent des données réelles. - Les vues dialoguent directement avec
@Query+modelContext. Pas de couche Store / repository à moins que quelque chose ne nécessite réellement un cache. La logique pure (correspondance, analyse, importation) vit dans des espaces de noms d'énumération (enum Foo { static func … }) — testable sans interface utilisateur. - Cible de déploiement : version majeure actuelle moins un sauf si une API nécessaire
dit autrement. Une seule cible,
TARGETED_DEVICE_FAMILY: "1", portrait sauf si l'application mérite la rotation.
Génération de projet — XcodeGen, jamais de .xcodeproj versionné
ios/project.yml est la source de vérité ; le bundle .xcodeproj est
généré et gitignoré (gitignore aussi ios/build/ et le Info.plist généré).
Régénérez après avoir ajouté ou supprimé des fichiers — xcodegen parcourt
le répertoire des sources, donc les modifications de fichiers existants ne nécessitent rien.
- Installez xcodegen depuis l'archive zip de GitHub (
PREFIX=~/.local ./install.sh) — pas besoin de Homebrew, fonctionne sur un Mac restreint. - Les cibles de tests unitaires nécessitent
GENERATE_INFOPLIST_FILE: YESsinon le bundle de test échoue à la signature. - Définissez le schéma dans
project.ymlavecgatherCoverageData: trueet la cible de test attachée, sinonxcodebuild testn'a rien à exécuter.
La boucle de développement — CLI uniquement, simulateur par défaut
Chaque projet iOS obtient ces cibles Makefile. Paramétrez le nom du
simulateur, l'ID de l'appareil et l'ID de l'équipe avec des variables ?= afin que rien
ne soit codé en dur pour une machine spécifique :
| Cible | Quoi |
|---|---|
ios-project | génération xcodegen + ouverture dans Xcode |
ios-sim | build non signé pour simulateur, démarrage du sim, installation, lancement — la boucle par défaut ; zéro signature, zéro demande de mot de passe |
ios-sim-seed | ios-sim, puis relancement avec --seed-demo |
ios-test | xcodebuild test dans le simulateur + rapport de couverture xccov par cible |
ios-deploy | build signé pour le téléphone branché + installation devicectl + lancement |
ios-test affiche la couverture à chaque exécution — la règle de la maison est que
make test montre toujours le nombre, sans seuil fixe attaché.
Builds pour simulateur : CODE_SIGNING_ALLOWED=NO convient pour les vérifications de compilation
generic/platform=iOS ; les destinations simulateur n'ont besoin d'aucune signature.
Rappelez-vous que le simulateur et le téléphone ont des bases de données séparées.
Commodités de débogage qui paient toujours
- Seeder de données d'exemple (
DemoData.swift,#if DEBUG) : jeu de données organisé avec des champs de date/expiration calculés relativement à.now, idempotent (une ré-exécution n'ajoute rien), exposé de trois manières — un bouton dans la section « Développeur » des Réglages, un argument de lancement--seed-demovérifié viaProcessInfo.processInfo.arguments, et la cibleios-sim-seed. Ajoutez un test vérifiant que la seed exerce les principaux états de l'application afin qu'elle ne pourrisse pas silencieusement. - Sélecteurs recherchables plutôt que des sélecteurs à roue dès qu'une liste peut dépasser quelques dizaines d'entrées (les ensembles de données importés garantissent cela).
- Fonctionnalités appareil photo/scanner : vérifiez
DataScannerViewController.isSupported && .isAvailableet fournissez une solution de repli pour la saisie manuelle — cela se dégrade gracieusement aussi dans le simulateur.
Tests
- XCTest dans le simulateur ; lors du portage de logique depuis une autre pile, portez aussi la suite de tests de référence afin que la sémantique soit figée entre les langages.
- Aide partagée :
makeInMemoryContext()construisant un ModelContainer avecisStoredInMemoryOnly: true; classes de test@MainActoravecsetUp() async throws. - Lisez le résultat succès/échec depuis le bundle de résultats :
xcrun xcresulttool get test-results summary --path build/test.xcresult.
Signature & déploiement — les pièges (chacun a coûté du temps réel)
- L'ID d'équipe n'est PAS la parenthèse dans
security find-identity— c'est l'ID du certificat lui-même. Obtenez le vrai ID d'équipe depuis un profil d'approvisionnement (security cms -D -i <profile>→ le préfixeapplication-identifier) ou le volet Comptes d'Xcode. Le mauvais ID donne une erreur trompeuse « No Account for Team ». -allowProvisioningUpdatesne voit pas la session Apple ID depuis un terminal simple. Créez le profil d'approvisionnement une fois dans l'interface graphique d'Xcode (Signing & Capabilities → définir l'équipe → Try Again) ; après cela, les builds purement CLI fonctionnent avecDEVELOPMENT_TEAM=<id> CODE_SIGN_STYLE=Automatic.- Première danse avec l'appareil : activez le mode développeur sur le téléphone (Réglages → Confidentialité et sécurité ; l'interrupteur n'apparaît qu'après qu'Xcode a parlé à l'appareil ; redémarrages) → faites confiance à l'ordinateur → après la première installation, faites confiance au certificat développeur (Réglages → Général → VPN et gestion des appareils). L'erreur « your team has no devices » d'Xcode signifie généralement que la tentative d'enregistrement a eu lieu pendant que le téléphone redémarrait ou était verrouillé — sélectionnez le téléphone comme destination d'exécution et réessayez.
- Invitations du trousseau à chaque build : cliquez sur Always Allow dans la boîte de dialogue du trousseau de codesign une fois ; les invites de mot de passe par build s'arrêtent.
- Limites du compte personnel gratuit : les installations expirent après 7 jours (redéployez pour re-signer), maximum 3 applications sideloadées, pas de notifications push, pas de groupes d'applications. Un compte payant lève les quatre — si l'application a besoin de push ou de groupes d'applications, c'est le déclencheur pour en obtenir un.
- CLI de déploiement/lancement :
xcrun devicectl list devices,xcrun devicectl device install app --device <id> <path>.app,… device process launch --device <id> <bundle-id>.
Bootstrap Mac frais (compatible sans tête)
- Xcode depuis l'App Store (ouvrez
macappstore://apps.apple.com/app/id497799835). sudo xcode-select -s /Applications/Xcode.app && sudo xcodebuild -license accept && sudo xcodebuild -runFirstLaunch— nécessite un administrateur ; sur un compte standard, corrigez d'abord l'appartenance admin (su admin -c 'sudo dseditgroup -o edit -a <user> -t user admin').xcodebuild -downloadPlatform iOS— le SDK iOS + l'environnement d'exécution du simulateur est un téléchargement séparé de plusieurs Go depuis Xcode 15 ; les builds échouent avec « iOS X.Y is not installed » sans cela.DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developerfonctionne comme une substitution par commande avant que xcode-select ne soit corrigé.- xcodegen depuis l'archive zip dans
~/.local(voir ci-dessus).
Clients compagnons pour un serveur existant — niveaux de synchronisation
Lorsque l'application iPhone rejoint une application qui a déjà un serveur + une application web (le serveur reste la source unique de vérité), choisissez le niveau le moins coûteux que le cas d'utilisation du téléphone permet. Demandez d'abord « le téléphone doit-il fonctionner hors du réseau du serveur ? » et « capture ou édition complète ? » :
- Client léger — pas de stockage local ; le téléphone appelle l'API existante. Approprié lorsque la connectivité est acquise.
- Miroir instantané — cache SwiftData reconstruit en bloc à partir d'une extraction JSON complète (les bases de données mono-utilisateur sont petites). Pas de fusion → pas de pierres tombales, pas d'horodatages par ligne, pas de moteur de conflit ; les suppressions/modifications côté serveur arrivent gratuitement. Les photos se chargent paresseusement via le cache d'URL.
- Boîte d'envoi limitée — création hors ligne limitée aux enfants d'enregistrements qui existent déjà (journaux, notes, photos contre des parents connus). Les parents portent de vrais IDs serveur, donc la relecture est de simples appels API — les schémas avec clé primaire entière n'ont besoin d'aucune migration UUID. Un événement de synchronisation = pousser la boîte d'envoi, puis tirer le miroir (les propres captures reviennent confirmées). Affichez les orphelins 404 (« N n'a pas pu se synchroniser »), ne les supprimez jamais silencieusement. Montrez un badge en attente + un horodatage de dernière synchro. Synchronisation automatique uniquement lorsque l'adresse serveur épinglée de l'utilisateur répond — ne sondez jamais des réseaux étrangers.
- Synchronisation bidirectionnelle complète (UUID, pierres tombales, points de terminaison delta,
LWW/CRDT) — seulement avec la preuve que les niveaux 1 à 3 ne suffisent pas ; le remaniement
commence côté serveur (UUID créés par le client, upserts idempotents,
changes?since=+ pierres tombales) avant qu'aucun Swift ne soit écrit.
Les niveaux 2 et 3 se combinent bien : un miroir instantané pour les lectures plus une boîte d'envoi limitée pour les captures couvre « fonctionne à la maison, capture n'importe où » sans aucun des mécanismes du niveau 4.
Hygiène de projet
- Les faits spécifiques à la machine et à l'appareil (ID d'équipe, UDID de l'appareil, nom du simulateur)
vont dans les variables
?=du Makefile ou dans les notes du projet — jamais codés en dur dans les scripts, et jamais dans cette compétence. - Conservez un document
plans/<sujet>.mdpour chaque tranche de travail en cours et unCHANGELOG.mdde haut niveau pour ce qui est livré ; déplacez un plan versplans/done/<sujet>.mdavec un en-tête « Livré dans — » une fois qu'il est terminé. - Dimensionnez chaque tranche de plan pour qu'elle se termine par quelque chose d'exécutable sur un téléphone ou un simulateur. Une tranche iOS qui ne peut pas être lancée ne peut pas être revue.