Maîtriser l'IDE Apple, la syntaxe Swift, ARC, async/await, actors et structured concurrency.
Result.Xcode 15 est l'IDE officiel d'Apple, gratuit sur le Mac App Store (poids ~12 Go, exclusivement macOS). Il intègre le compilateur Swift, les SDK iOS 17 / iPadOS 17 / watchOS 10 / visionOS 1 / macOS 14, les simulateurs, le débogueur LLDB, Instruments (profileur), Interface Builder (legacy UIKit) et SwiftUI Previews. Le passage à Xcode 15 a apporté un linker 30% plus rapide, des macros Swift, des previews SwiftUI plus stables et une intégration native du String Catalog pour la localisation.
Le projet typique comporte trois zones : la Navigator (gauche, arborescence fichiers + breakpoints + tests), l'Editor (centre, code + Canvas SwiftUI Preview) et l'Inspector (droite, attributes + file inspector). Les Schemes définissent les targets buildables (Debug / Release / TestFlight). Les simulateurs sont une émulation Mac (architecture native) — un bug visible sur device réel ne se reproduit pas toujours dans le simulateur, d'où l'importance du test sur appareil physique avant soumission App Store.
Côté signing, Apple impose un compte Apple Developer Program à 99 USD/an. Xcode 15 gère automatiquement les provisioning profiles via "Automatically manage signing", mais en production CI/CD (Fastlane, GitHub Actions), il faut maîtriser App ID, Provisioning Profile, Distribution Certificate et Push Notification Certificate APNs.
"Xcode includes everything developers need to create great applications for Mac, iPhone, iPad, Apple Watch, and Apple TV." — Apple Developer — Xcode
Swift est un langage compilé, statiquement typé, multi-paradigme, créé par Apple en 2014 et désormais open-source (swift.org). Sa philosophie repose sur la sûreté (safety), l'expressivité et la performance. Les optionals (String?) éliminent quasi totalement les crashes null pointer exception tristement célèbres en Objective-C. On les déballe avec if let, guard let, ?? (nil-coalescing) ou — avec prudence — le force unwrap !.
Les closures sont des fonctions anonymes capturant leur contexte (équivalent des lambdas Kotlin/Java). Les protocols (interfaces) supportent l'extension et l'associated type, base du paradigme Protocol-Oriented Programming promu par Apple à la WWDC 2015. Les generics permettent de typer paramétriquement (équivalent T en Java/Kotlin) avec contraintes (where T: Comparable).
L'error handling Swift utilise throw / try / catch et le type Result<Success, Failure: Error> pour les API callback-based legacy. Exemple :
enum NetworkError: Error { case timeout, unauthorized }
func fetchUser(id: Int) -> Result<User, NetworkError> {
guard id > 0 else { return .failure(.unauthorized) }
return .success(User(id: id, name: "Alice"))
}
Swift utilise ARC (Automatic Reference Counting) : le compilateur insère automatiquement les retain/release à la compilation. Plus de garbage collector (et plus de pauses GC), mais le développeur doit savoir éviter les retain cycles (cycles fortes références entre deux objets, fuite mémoire garantie).
La règle d'or : self capturé dans un closure asynchrone devient une référence forte. On utilise [weak self] ou [unowned self] dans la capture list pour casser le cycle. weak rend la référence optionnelle, unowned garantit non-nil (crash si l'objet a été désalloué).
Les structs et enums sont des value types (copie à l'assignation, immuables par défaut). Les classes sont des reference types (partage de référence). Apple recommande "use struct first, class only when needed" — règle suivie par tout SwiftUI moderne, où les Views sont des structs immutables redessinées à chaque changement d'état.
| Caractéristique | struct (value type) | class (reference type) |
|---|---|---|
| Assignation | Copie | Référence partagée |
| Héritage | Non (compose via protocols) | Oui (single inheritance) |
| Mutabilité | Méthodes mutating explicites | Mutable par défaut si var |
| ARC | Pas concerné (stack) | Reference counting (heap) |
| Usage SwiftUI | Views, modèles de données | ViewModel (ObservableObject), services réseau |
Depuis Swift 5.5 (2021), async/await remplace l'enfer du callback pyramid hérité de l'Objective-C et des completion handlers. Le compilateur transforme statiquement le code en machines à états, garantissant qu'on n'oublie pas d'appeler le completion handler et qu'on ne le rappelle pas deux fois.
Une fonction asynchrone se marque async et s'appelle avec try await :
func fetchUser(id: Int) async throws -> User {
let url = URL(string: "https://api.example.com/users/\(id)")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
// Appel depuis un contexte concurrent
Task {
do {
let user = try await fetchUser(id: 42)
print(user.name)
} catch {
print("Erreur : \(error)")
}
}
Les actors (Swift 5.5+) sont des types-référence garantissant l'isolation de leur état : un seul thread accède à leurs propriétés à la fois (mutual exclusion gérée par le runtime). Fini les DispatchQueue manuelles et les NSLock dans 90% des cas.
actor BankAccount {
private var balance: Double = 0
func deposit(_ amount: Double) {
balance += amount // thread-safe par construction
}
func currentBalance() -> Double { balance }
}
let account = BankAccount()
Task {
await account.deposit(100.0) // await obligatoire : appel cross-actor
let total = await account.currentBalance()
print("Solde : \(total)")
}
Le protocole Sendable certifie qu'un type peut traverser sans risque les frontières de concurrence. Swift 6 (strict concurrency) impose désormais des compile-time checks sur Sendable, rendant les data races impossibles à compiler.
Code legacy callback-based :
func loadImage(url: URL, completion: @escaping (UIImage?, Error?) -> Void) {
URLSession.shared.dataTask(with: url) { data, _, error in
if let error = error { completion(nil, error); return }
guard let data = data, let image = UIImage(data: data) else {
completion(nil, NSError(domain: "img", code: -1)); return
}
completion(image, nil)
}.resume()
}
Version async/await moderne :
enum ImageError: Error { case invalidData }
func loadImage(url: URL) async throws -> UIImage {
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else { throw ImageError.invalidData }
return image
}
Bénéfices : code linéaire, gestion d'erreur unifiée avec throws, annulation automatique via Task.cancel(), pas de capture [weak self] obligatoire car le task est porté par l'appelant.
Cmd+I dans Xcode pour profiler. Pour traquer un retain cycle, utilise le template "Leaks" + filtre sur ton bundle ID. Les flèches rouges dans le graphe Allocations pointent les objets jamais désalloués.self dans une closure d'URLSession ou Combine sans [weak self] crée un retain cycle si le ViewController détient le subscriber. Symptôme : deinit jamais appelé, mémoire qui grimpe à chaque navigation.Au-delà des bases d'async/await, la concurrence structurée Swift impose un modèle mental qui change profondément la façon dont on raisonne sur la durée de vie des tâches. Une tâche enfant ne survit jamais à son parent : c'est la garantie fondamentale que Apple a introduite avec SE-0304 pour éliminer les "fire-and-forget" qui plombaient GCD. Concrètement, lorsqu'on écrit async let a = fetch() ou un withTaskGroup, le compilateur insère des points d'annulation et de jonction automatiques. Si la fonction englobante est annulée, toutes les tâches filles reçoivent un CancellationError dans leur prochain point de suspension. Cela suppose que votre code coopère : un calcul CPU pur qui ne suspend jamais ignorera l'annulation. La discipline consiste à appeler try Task.checkCancellation() ou à vérifier Task.isCancelled dans les boucles longues.
La frontière isolation/non-isolation est le second pilier qu'un développeur senior doit internaliser. Un acteur protège son état mutable, mais ce qui sort de l'acteur doit être Sendable. Un type est Sendable s'il est Sendable par construction : value type avec uniquement des fields Sendable, class final avec uniquement des stored properties let de types Sendable, ou un acteur. Le compilateur en mode Swift 6 (strict concurrency checking) refusera de compiler tout franchissement de frontière qui ne respecte pas cette contrainte. Beaucoup d'équipes activent SWIFT_STRICT_CONCURRENCY=complete module par module pour préparer la migration Swift 6 sans bloquer la livraison.
Un piège méconnu des actors est leur réentrance. Contrairement à un mutex traditionnel, un acteur Swift peut suspendre une méthode à un await, traiter d'autres messages, puis reprendre. Cela signifie qu'une invariante observée avant un await peut être brisée après. Exemple : vous lisez self.users.count, faites un await network.fetch(), puis utilisez la valeur lue — entre temps, un autre appel a peut-être muté self.users. La solution n'est pas de désactiver la réentrance (impossible), mais de structurer le code pour que toute lecture-modification soit synchrone, ou de re-lire la propriété après chaque await. Pour les sections critiques vraiment atomiques, on isole sur un nonisolated(unsafe) avec un OSAllocatedUnfairLock (iOS 16+), bien plus performant qu'un acteur pour des opérations courtes.
Le MainActor est un acteur global préfourni dont l'exécuteur est la main queue. Toute fonction marquée @MainActor est garantie d'exécuter sur le main thread, ce qui remplace les anciens DispatchQueue.main.async. SwiftUI annote automatiquement View.body en @MainActor. Les pièges classiques : appeler une fonction @MainActor depuis un détaché demande un await, et marquer un type entier @MainActor sérialise tout son code sur le main thread, ce qui peut tuer les performances si on n'y prend garde.
Instruments reste l'outil de profiling de référence. La template "Swift Concurrency" (Xcode 14+) visualise les Tasks, les acteurs, les transitions de contexte et les temps passés en attente sur un await. Pour trouver une fuite de mémoire, l'instrument "Leaks" couplé à "Allocations" reste imbattable : on identifie d'abord les cycles de référence avec "Cycles & Roots", puis on affine. Les fuites les plus fréquentes en SwiftUI proviennent de captures fortes de self dans des closures stockées (par exemple un @StateObject qui retient un timer dont le handler capture l'object). La règle : toute closure stockée capture [weak self], toute closure transient (passée à une API qui l'exécute immédiatement) peut capturer self sans risque.
LLDB est plus qu'un débogueur ligne par ligne. Les commandes essentielles : po (print object via Swift), v (frame variable, plus rapide car n'invoque pas le runtime), bt (backtrace), thread step-in (s'), thread step-over (n), expression pour évaluer du Swift à la volée. Pour déboguer la concurrence : thread list affiche tous les threads, et avec Xcode 15 la vue "Swift Tasks" du débogueur montre la hiérarchie des tâches actives. La commande bugreport swift capture un snapshot complet pour Apple si vous rencontrez un crash compilateur.
Scénario : votre app doit télécharger 200 vignettes pour un feed, sans saturer le réseau cellulaire ni dépasser le budget de file descriptors. Un for await simple est trop lent (séquentiel) ; un TaskGroup non bridé peut lancer 200 connexions parallèles. La solution idiomatique Swift est un withThrowingTaskGroup avec un sémaphore logique : on n'ajoute une nouvelle tâche que quand une précédente termine, en gardant au maximum N tâches actives. C'est un pattern de "bounded concurrency" qu'on rencontre dans toute app sérieuse.
// Téléchargement parallèle borné à `maxConcurrent` requêtes simultanées.
func downloadAll(urls: [URL], maxConcurrent: Int = 6) async throws -> [Data] {
try await withThrowingTaskGroup(of: (Int, Data).self) { group in
var results = Array(repeating: nil, count: urls.count)
var nextIndex = 0
// Amorçage : on lance maxConcurrent premières tâches.
for _ in 0..
Un cache mémoire doit servir des lectures concurrentes sans corruption. Un acteur encapsule l'état mutable, et une politique LRU borne la taille. La subtilité : exposer une API async pour la lecture mais permettre une éviction lazy lors des insertions. On évite NSCache ici parce qu'il n'offre pas de garantie d'ordre LRU déterministe.
actor ImageCache {
private var storage: [URL: (data: Data, lastUsed: Date)] = [:]
private let capacity: Int
init(capacity: Int = 100) { self.capacity = capacity }
func get(_ url: URL) -> Data? {
guard var entry = storage[url] else { return nil }
entry.lastUsed = Date() // marque comme récemment utilisé
storage[url] = entry
return entry.data
}
func set(_ url: URL, _ data: Data) {
storage[url] = (data, Date())
if storage.count > capacity {
// évince le moins récemment utilisé
if let oldest = storage.min(by: { $0.value.lastUsed < $1.value.lastUsed }) {
storage.removeValue(forKey: oldest.key)
}
}
}
}
| Approche | Performance | Lisibilité | Cas d'usage |
|---|---|---|---|
GCD DispatchQueue.async | Excellente (overhead nul) | Faible (callbacks imbriqués) | Code legacy, callbacks ObjC bridgeés |
| Operation / OperationQueue | Bonne | Moyenne (verbose) | Dépendances entre tâches, cancel/priorité fine |
Combine Publisher | Bonne mais overhead | Moyenne (chaining) | Flux d'événements continus, UI reactive |
| async/await | Très bonne (cooperative scheduler) | Excellente (linéaire) | Code applicatif moderne, iOS 13+ |
| TaskGroup | Excellente avec borne | Bonne | Parallélisme dynamique N tâches |
| Actor | Bonne (faible contention) | Excellente | État mutable partagé thread-safe |
| OSAllocatedUnfairLock | Maximale (lock kernel) | Moyenne | Sections critiques très courtes < 100ns |
| AsyncSequence | Bonne | Excellente | Streams paresseux, file watchers, polls |
Task { await self.work() } stocké dans une propriété capture self fortement. Si l'objet doit être désalloué (ex : ViewController qui disparaît), la Task continue de tourner et empêche la désallocation. Solution : appeler task.cancel() dans deinit ou utiliser Task.detached avec [weak self].Sending non-Sendable value sont silencieux. Quand vous activez "complete", la dette technique explose. Migrez incrémentalement : un module à la fois, en commençant par les feuilles de l'arbre de dépendances.MainActor et revient, l'UI peut "respirer" entre les awaits mais paraître saccadée. Mesurer avec Instruments "Hangs" qui détecte les blocages > 250 ms du main thread.UIImage(data:) charge en RAM la taille décompressée (largeur × hauteur × 4 octets). Une JPEG 4 MB de 6000×4000 px occupe 96 MB en RAM. Utiliser ImageIO avec kCGImageSourceThumbnailMaxPixelSize pour un downsampling à la décompression.os_proc_available_memory() et libérer les caches à la réception de UIApplication.didReceiveMemoryWarningNotification.allowsCellularAccess = false sur une URLSessionConfiguration, les requêtes restent en attente indéfiniment sans erreur. Toujours configurer un timeoutIntervalForResource raisonnable (60-120 s).-Osize évite l'inlining et peut diviser les performances par 3. Préférer -O (optimize for speed) pour le release, sauf contrainte App Thinning extrême.print() — print() écrit sur stdout qui finit dans le log système Apple (visible via Console.app), exposant potentiellement des données sensibles. Utiliser os.Logger avec niveaux et catégories, et marquer les arguments sensibles .private..xcworkspace intrusif, résolution déterministe via Package.resolved. Documentation : swift.org/package-manager.xcodeproj depuis du Swift, évite les conflits de merge sur le project.pbxproj en équipe. tuist.ioxcodebuild pour la rendre lisible en CI. github.com/cpisciotta/xcbeautify"Swift concurrency provides a structured way to write asynchronous code that's easier to read and reason about. The compiler ensures data-race safety by checking that mutable state is accessed correctly across concurrent code." — developer.apple.com/documentation/swift/concurrency
"Actors allow only one task to access their mutable state at a time, which makes it safe for code in multiple tasks to interact with the same instance of an actor." — The Swift Programming Language — Concurrency
| Terme | Définition |
|---|---|
| Structured concurrency | Modèle où les tâches enfants sont liées au cycle de vie de leur parent ; toute tâche fille est annulée si le parent l'est, et le parent attend toutes ses filles avant de terminer. |
| Actor | Type de référence qui sérialise l'accès à son état mutable. Garanti thread-safe par le compilateur, accès depuis l'extérieur via await. |
| Sendable | Protocole marqueur indiquant qu'une valeur peut traverser sans risque les frontières de concurrence. Imposé par le compilateur en mode strict. |
| MainActor | Acteur global dont l'exécuteur est le main thread. Toute API UIKit/SwiftUI manipulant l'UI est annotée @MainActor. |
| Task | Unité de travail asynchrone. Une Task détachée n'a pas de parent ; une Task structurée hérite du contexte d'isolation et de priorité. |
| Reentrance | Propriété d'un acteur qui peut traiter d'autres messages pendant qu'une méthode est suspendue à un await. Source de bugs subtils si invariantes non maintenues. |
| ARC | Automatic Reference Counting : libération mémoire déterministe par comptage de références. Différent du GC : pas de pause, mais cycles à éviter manuellement. |
| Cooperative thread pool | Pool de threads géré par le runtime Swift. Toutes les tasks async s'exécutent dessus sauf les @MainActor. Taille = nb cœurs. |
| LLDB | Débogueur low-level du toolchain LLVM utilisé par Xcode. Permet inspection runtime, breakpoints conditionnels, scripting Python. |
| SPM | Swift Package Manager : système officiel de gestion de dépendances Swift, alternative moderne à CocoaPods et Carthage. |
La leçon suivante est également gratuite. Découvrez-la sans inscription.
Leçon 2 — Continuer →Choisis quels cookies tu acceptes — modifiable à tout moment.