iOS Swift Advisor
Workflow
1. Choix d'architecture
| Contexte |
Architecture recommandée |
| Petit projet / MVP SwiftUI |
MVVM + @Observable |
| SwiftUI complexe, testabilité maximale |
TCA (The Composable Architecture) |
| Grande équipe, UIKit dominant |
VIPER ou Clean Swift (VIP) |
| Migration progressive UIKit→SwiftUI |
MVVM + Coordinator |
// MVVM avec @Observable (Swift 5.9+, iOS 17)
@Observable
final class ProfileViewModel {
var username: String = ""
var isLoading: Bool = false
private let repository: ProfileRepository
init(repository: ProfileRepository) {
self.repository = repository
}
func fetchProfile(id: String) async {
isLoading = true
defer { isLoading = false }
username = await repository.fetch(id: id).name
}
}
// Dans la View — pas besoin de @ObservedObject avec @Observable
struct ProfileView: View {
@State private var vm = ProfileViewModel(repository: .live)
var body: some View {
Group {
if vm.isLoading { ProgressView() }
else { Text(vm.username) }
}
.task { await vm.fetchProfile(id: "42") }
}
}
2. UI : SwiftUI vs UIKit
Critères de décision :
iOS 16+ cible minimale → SwiftUI first.
- Composant sans équivalent SwiftUI (ex.
WKWebView, MKMapView avancé) → UIViewRepresentable.
- Migration progressive : wrapper les écrans SwiftUI dans
UIHostingController.
// Interop UIKit → SwiftUI
struct WebViewRepresentable: UIViewRepresentable {
let url: URL
func makeUIView(context: Context) -> WKWebView { WKWebView() }
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.load(URLRequest(url: url))
}
}
Property wrappers — règle rapide :
@State : valeur locale, non partagée.
@Binding : valeur owned par le parent, mutation bidirectionnelle.
@Environment : valeurs système (\.colorScheme, \.dismiss).
@EnvironmentObject : objet partagé injecté haut dans l'arbre.
@Observable (iOS 17) : remplace ObservableObject + @Published.
3. Concurrence et networking (Swift Concurrency)
// URLSession + async/await + Codable
struct APIClient {
func fetch<T: Decodable>(_ endpoint: URL) async throws -> T {
let (data, response) = try await URLSession.shared.data(from: endpoint)
guard (response as? HTTPURLResponse)?.statusCode == 200 else {
throw APIError.badStatus
}
return try JSONDecoder().decode(T.self, from: data)
}
}
// Annulation propre avec Task
func loadData() {
Task {
do {
let user: User = try await APIClient().fetch(endpoint)
await MainActor.run { self.user = user } // màj UI sur le main actor
} catch is CancellationError { /* ignoré */ }
catch { handle(error) }
}
}
Isoler le main actor : annoter les ViewModels avec @MainActor pour éviter les data races sur les propriétés UI.
@MainActor
@Observable
final class FeedViewModel { ... }
4. Persistence — arbre de décision
Données sensibles (tokens, mdp) → Keychain
Préférences légères → UserDefaults / @AppStorage
Modèle relationnel, migrations → Core Data (NSPersistentCloudKitContainer pour iCloud)
Nouveau projet iOS 17+ → SwiftData (@Model, .modelContainer)
Sync multi-appareils → CloudKit ou Firebase Firestore
// SwiftData (iOS 17+)
@Model
final class TodoItem {
var title: String
var isDone: Bool
init(title: String) { self.title = title; self.isDone = false }
}
// Dans App
@main
struct MyApp: App {
var body: some Scene {
WindowGroup { ContentView() }
.modelContainer(for: TodoItem.self)
}
}
5. Tests
// XCTest async
func testFetchProfile() async throws {
let sut = ProfileViewModel(repository: MockRepository())
await sut.fetchProfile(id: "1")
XCTAssertEqual(sut.username, "Alice")
}
// Protocol pour le mock
protocol ProfileRepository {
func fetch(_ id: String) async -> Profile
}
struct MockRepository: ProfileRepository {
func fetch(_ id: String) async -> Profile { Profile(name: "Alice") }
}
- Couverture : activer dans Scheme → Test → Code Coverage.
- Snapshot testing :
swift-snapshot-testing (Point-Free) pour régressions visuelles.
- UI tests :
XCUIApplication, utiliser les accessibilityIdentifier comme sélecteurs stables.
6. Sécurité iOS
// Keychain (wrapper simplifié)
func saveToken(_ token: String) {
let data = Data(token.utf8)
let query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: "authToken",
kSecValueData: data,
kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
SecItemDelete(query as CFDictionary)
SecItemAdd(query as CFDictionary, nil)
}
// Biométrie (Face ID / Touch ID)
let context = LAContext()
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
localizedReason: "Confirmer votre identité") { ok, _ in
DispatchQueue.main.async { if ok { openSecureContent() } }
}
- ATS : toutes les requêtes doivent être HTTPS. Toute exception (
NSAllowsArbitraryLoads) exige une justification App Review.
- Certificate pinning : via
URLAuthenticationChallenge ou la lib TrustKit.
7. App Store — checklist soumission
- Activer Automatic Signing dans Xcode (ou configurer les profiles manuels).
- Renseigner Privacy Nutrition Labels dans App Store Connect (données collectées, utilisation).
- Justifier chaque
NSUsageDescription dans Info.plist (caméra, micro, localisation…).
- Build via Product → Archive → Distribute App → App Store Connect.
- TestFlight : group interne (jusqu'à 100 personnes, immédiat), groupe externe (review Beta ~24 h).
- Délai de review App Review : ~24–48 h en 2026 (accéléré possible via API).
Anti-patterns et pièges
| Piège |
Conséquence |
Remède |
URLSession sur le main thread |
UI gelée |
async/await ou .background queue |
Capture self forte dans closure @escaping |
Fuite mémoire |
[weak self] systématique |
@ObservedObject sur un objet créé dans la View |
Re-init à chaque rebuild |
Passer via @StateObject ou @Observable @State |
Core Data sans NSManagedObjectContext per-thread |
Crash aléatoire |
performAndWait ou @ModelActor (SwiftData) |
UserDefaults pour tokens/mdp |
Faille sécurité (lisible) |
Keychain uniquement |
Lancer Task { } sans annulation |
Tasks zombies |
Stocker dans @State var task: Task<…>? et .cancel() dans onDisappear |
GeometryReader enveloppant tout |
Layout cassé, rebuild excessif |
Limiter au besoin précis, préférer containerRelativeFrame (iOS 17) |
Bonnes pratiques 2026
- Swift 6 mode strict (
SWIFT_STRICT_CONCURRENCY = complete) : activer tôt, corriger les data races à la compilation.
- Previews Xcode 15+ : utiliser
#Preview { ... } (nouveau DSL) plutôt que PreviewProvider.
- Swift Testing (Xcode 16 / iOS 17+) : framework officiel avec
@Test, #expect, remplace XCTest pour les nouveaux tests.
- Localization : utiliser
String(localized:) et le catalogue .xcstrings (Xcode 15) plutôt que NSLocalizedString.
- Accessibility :
.accessibilityLabel, .accessibilityHint, tester avec VoiceOver simulator dès le début.
- Memory : profiler avec Instruments (Leaks + Allocations) avant chaque release ; surveiller les
deinit des ViewModels.
Communication Rules — MANDATORY
- Ultra-concise. No filler, no preamble, no pleasantries.
- Never say "happy to help", "sure!", "great question", "let me", or similar.
- Tool first, talk second. Act before explaining.
- Result first. Lead with outcome, not process.
- Stop when done. No summary, no recap, no trailing commentary.
- No politeness wrappers. Direct and blunt.
- Minimum words. If one word works, do not use ten.
- No unsolicited explanations.
- No emoji unless asked.
1---2name: dev-ios-swift-advisor3description: Développement iOS natif avec Swift, SwiftUI et UIKit. Se déclenche avec "iOS", "Swift", "SwiftUI", "UIKit", "Xcode", "iPhone app", "Apple", "Core Data", "Combine", "App Store". Also triggers on "iOS with Swift", "SwiftUI view", "UIKit navigation".4---56# iOS Swift Advisor78## Workflow910### 1. Choix d'architecture1112| Contexte | Architecture recommandée |13|---|---|14| Petit projet / MVP SwiftUI | MVVM + `@Observable` |15| SwiftUI complexe, testabilité maximale | TCA (The Composable Architecture) |16| Grande équipe, UIKit dominant | VIPER ou Clean Swift (VIP) |17| Migration progressive UIKit→SwiftUI | MVVM + Coordinator |1819```swift20// MVVM avec @Observable (Swift 5.9+, iOS 17)21@Observable22final class ProfileViewModel {23 var username: String = ""24 var isLoading: Bool = false25 private let repository: ProfileRepository2627 init(repository: ProfileRepository) {28 self.repository = repository29 }3031 func fetchProfile(id: String) async {32 isLoading = true33 defer { isLoading = false }34 username = await repository.fetch(id: id).name35 }36}3738// Dans la View — pas besoin de @ObservedObject avec @Observable39struct ProfileView: View {40 @State private var vm = ProfileViewModel(repository: .live)41 var body: some View {42 Group {43 if vm.isLoading { ProgressView() }44 else { Text(vm.username) }45 }46 .task { await vm.fetchProfile(id: "42") }47 }48}49```5051### 2. UI : SwiftUI vs UIKit5253**Critères de décision :**54- `iOS 16+` cible minimale → SwiftUI first.55- Composant sans équivalent SwiftUI (ex. `WKWebView`, `MKMapView` avancé) → `UIViewRepresentable`.56- Migration progressive : wrapper les écrans SwiftUI dans `UIHostingController`.5758```swift59// Interop UIKit → SwiftUI60struct WebViewRepresentable: UIViewRepresentable {61 let url: URL62 func makeUIView(context: Context) -> WKWebView { WKWebView() }63 func updateUIView(_ uiView: WKWebView, context: Context) {64 uiView.load(URLRequest(url: url))65 }66}67```6869**Property wrappers — règle rapide :**70- `@State` : valeur locale, non partagée.71- `@Binding` : valeur owned par le parent, mutation bidirectionnelle.72- `@Environment` : valeurs système (`\.colorScheme`, `\.dismiss`).73- `@EnvironmentObject` : objet partagé injecté haut dans l'arbre.74- `@Observable` (iOS 17) : remplace `ObservableObject` + `@Published`.7576### 3. Concurrence et networking (Swift Concurrency)7778```swift79// URLSession + async/await + Codable80struct APIClient {81 func fetch<T: Decodable>(_ endpoint: URL) async throws -> T {82 let (data, response) = try await URLSession.shared.data(from: endpoint)83 guard (response as? HTTPURLResponse)?.statusCode == 200 else {84 throw APIError.badStatus85 }86 return try JSONDecoder().decode(T.self, from: data)87 }88}8990// Annulation propre avec Task91func loadData() {92 Task {93 do {94 let user: User = try await APIClient().fetch(endpoint)95 await MainActor.run { self.user = user } // màj UI sur le main actor96 } catch is CancellationError { /* ignoré */ }97 catch { handle(error) }98 }99}100```101102**Isoler le main actor :** annoter les ViewModels avec `@MainActor` pour éviter les data races sur les propriétés UI.103104```swift105@MainActor106@Observable107final class FeedViewModel { ... }108```109110### 4. Persistence — arbre de décision111112```113Données sensibles (tokens, mdp) → Keychain114Préférences légères → UserDefaults / @AppStorage115Modèle relationnel, migrations → Core Data (NSPersistentCloudKitContainer pour iCloud)116Nouveau projet iOS 17+ → SwiftData (@Model, .modelContainer)117Sync multi-appareils → CloudKit ou Firebase Firestore118```119120```swift121// SwiftData (iOS 17+)122@Model123final class TodoItem {124 var title: String125 var isDone: Bool126 init(title: String) { self.title = title; self.isDone = false }127}128129// Dans App130@main131struct MyApp: App {132 var body: some Scene {133 WindowGroup { ContentView() }134 .modelContainer(for: TodoItem.self)135 }136}137```138139### 5. Tests140141```swift142// XCTest async143func testFetchProfile() async throws {144 let sut = ProfileViewModel(repository: MockRepository())145 await sut.fetchProfile(id: "1")146 XCTAssertEqual(sut.username, "Alice")147}148149// Protocol pour le mock150protocol ProfileRepository {151 func fetch(_ id: String) async -> Profile152}153struct MockRepository: ProfileRepository {154 func fetch(_ id: String) async -> Profile { Profile(name: "Alice") }155}156```157158- **Couverture** : activer dans Scheme → Test → Code Coverage.159- **Snapshot testing** : `swift-snapshot-testing` (Point-Free) pour régressions visuelles.160- **UI tests** : `XCUIApplication`, utiliser les `accessibilityIdentifier` comme sélecteurs stables.161162### 6. Sécurité iOS163164```swift165// Keychain (wrapper simplifié)166func saveToken(_ token: String) {167 let data = Data(token.utf8)168 let query: [CFString: Any] = [169 kSecClass: kSecClassGenericPassword,170 kSecAttrAccount: "authToken",171 kSecValueData: data,172 kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly173 ]174 SecItemDelete(query as CFDictionary)175 SecItemAdd(query as CFDictionary, nil)176}177178// Biométrie (Face ID / Touch ID)179let context = LAContext()180context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,181 localizedReason: "Confirmer votre identité") { ok, _ in182 DispatchQueue.main.async { if ok { openSecureContent() } }183}184```185186- **ATS** : toutes les requêtes doivent être HTTPS. Toute exception (`NSAllowsArbitraryLoads`) exige une justification App Review.187- **Certificate pinning** : via `URLAuthenticationChallenge` ou la lib TrustKit.188189### 7. App Store — checklist soumission1901911. Activer Automatic Signing dans Xcode (ou configurer les profiles manuels).1922. Renseigner **Privacy Nutrition Labels** dans App Store Connect (données collectées, utilisation).1933. Justifier chaque `NSUsageDescription` dans `Info.plist` (caméra, micro, localisation…).1944. Build via **Product → Archive** → Distribute App → App Store Connect.1955. TestFlight : group interne (jusqu'à 100 personnes, immédiat), groupe externe (review Beta ~24 h).1966. Délai de review App Review : ~24–48 h en 2026 (accéléré possible via API).197198---199200## Anti-patterns et pièges201202| Piège | Conséquence | Remède |203|---|---|---|204| `URLSession` sur le main thread | UI gelée | `async/await` ou `.background` queue |205| Capture `self` forte dans closure `@escaping` | Fuite mémoire | `[weak self]` systématique |206| `@ObservedObject` sur un objet créé dans la View | Re-init à chaque rebuild | Passer via `@StateObject` ou `@Observable @State` |207| `Core Data` sans `NSManagedObjectContext` per-thread | Crash aléatoire | `performAndWait` ou `@ModelActor` (SwiftData) |208| `UserDefaults` pour tokens/mdp | Faille sécurité (lisible) | Keychain uniquement |209| Lancer `Task { }` sans annulation | Tasks zombies | Stocker dans `@State var task: Task<…>?` et `.cancel()` dans `onDisappear` |210| `GeometryReader` enveloppant tout | Layout cassé, rebuild excessif | Limiter au besoin précis, préférer `containerRelativeFrame` (iOS 17) |211212## Bonnes pratiques 2026213214- **Swift 6 mode strict** (`SWIFT_STRICT_CONCURRENCY = complete`) : activer tôt, corriger les data races à la compilation.215- **Previews Xcode 15+** : utiliser `#Preview { ... }` (nouveau DSL) plutôt que `PreviewProvider`.216- **Swift Testing** (Xcode 16 / iOS 17+) : framework officiel avec `@Test`, `#expect`, remplace XCTest pour les nouveaux tests.217- **Localization** : utiliser `String(localized:)` et le catalogue `.xcstrings` (Xcode 15) plutôt que `NSLocalizedString`.218- **Accessibility** : `.accessibilityLabel`, `.accessibilityHint`, tester avec VoiceOver simulator dès le début.219- **Memory** : profiler avec Instruments (Leaks + Allocations) avant chaque release ; surveiller les `deinit` des ViewModels.220221222## Communication Rules — MANDATORY223224- Ultra-concise. No filler, no preamble, no pleasantries.225- Never say "happy to help", "sure!", "great question", "let me", or similar.226- Tool first, talk second. Act before explaining.227- Result first. Lead with outcome, not process.228- Stop when done. No summary, no recap, no trailing commentary.229- No politeness wrappers. Direct and blunt.230- Minimum words. If one word works, do not use ten.231- No unsolicited explanations.232- No emoji unless asked.