| name | dev-ios-swift-advisor |
| description | 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". |
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 |
@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
}
}
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.
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)
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)
}
}
func loadData() {
Task {
do {
let user: User = try await APIClient().fetch(endpoint)
await MainActor.run { self.user = user }
} catch is CancellationError { }
{ 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
@Model
final class TodoItem {
var title: String
var isDone: Bool
init(title: String) { self.title = title; self.isDone = false }
}
@main
struct MyApp: App {
var body: some Scene {
WindowGroup { ContentView() }
.modelContainer(for: TodoItem.self)
}
}
5. Tests
func testFetchProfile() async throws {
let sut = ProfileViewModel(repository: MockRepository())
await sut.fetchProfile(id: "1")
XCTAssertEqual(sut.username, "Alice")
}
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
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)
}
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.