← Retour au cours
▶ Aperçu gratuit · Leçon offerte

Xcode 15 et Swift 5 moderne : async/await, actors, structured concurrency, ARC

⏱ 840 min · 🎬 Lecon · 🏆 20 XP
🎬
Vidéo en production
Notre équipe pédagogique tourne actuellement cette leçon avec un·e formateur·rice expert·e. Le contenu textuel ci-dessous est complet et utilisable dès maintenant.

Leçon 1 — Xcode 15 et Swift 5 moderne

Maîtriser l'IDE Apple, la syntaxe Swift, ARC, async/await, actors et structured concurrency.

🎯 Objectifs pédagogiques

  • Installer et configurer Xcode 15 (simulateurs iOS 17, Instruments, signing certificates Apple Developer).
  • Maîtriser la syntaxe Swift 5 : optionals, closures, protocols, generics, error handling avec Result.
  • Comprendre le modèle mémoire ARC, la différence value type vs reference type, struct vs class.
  • Écrire du code asynchrone moderne avec async/await, actors, structured concurrency et Sendable.
  • Profiler une app avec Instruments (Time Profiler, Allocations, Leaks) pour traquer fuites et retain cycles.

1. L'écosystème Xcode 15

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

2. Swift 5 : fondamentaux modernes

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"))
}

3. ARC, value types et reference types

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éristiquestruct (value type)class (reference type)
AssignationCopieRéférence partagée
HéritageNon (compose via protocols)Oui (single inheritance)
MutabilitéMéthodes mutating explicitesMutable par défaut si var
ARCPas concerné (stack)Reference counting (heap)
Usage SwiftUIViews, modèles de donnéesViewModel (ObservableObject), services réseau

4. Structured concurrency, async/await et actors

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.

✏️ Cas pratique : convertir un completion handler en async/await

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.

💡 Pro tip Instruments : lance 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.
⚠️ Piège classique : capturer 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.

5. Pour aller plus loin

Approfondissement technique

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.

Sous-section 1 — Actors réentrance et atomicity hazards

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.

Sous-section 2 — Instruments, LLDB et profiling avancé

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.

Cas pratiques détaillés

✏️ Cas 1 — Sérialiser N téléchargements avec budget de concurrence

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..

✏️ Cas 2 — Cache thread-safe avec actor + LRU eviction

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)
            }
        }
    }
}

Comparaison et benchmark

ApprochePerformanceLisibilitéCas d'usage
GCD DispatchQueue.asyncExcellente (overhead nul)Faible (callbacks imbriqués)Code legacy, callbacks ObjC bridgeés
Operation / OperationQueueBonneMoyenne (verbose)Dépendances entre tâches, cancel/priorité fine
Combine PublisherBonne mais overheadMoyenne (chaining)Flux d'événements continus, UI reactive
async/awaitTrès bonne (cooperative scheduler)Excellente (linéaire)Code applicatif moderne, iOS 13+
TaskGroupExcellente avec borneBonneParallélisme dynamique N tâches
ActorBonne (faible contention)ExcellenteÉtat mutable partagé thread-safe
OSAllocatedUnfairLockMaximale (lock kernel)MoyenneSections critiques très courtes < 100ns
AsyncSequenceBonneExcellenteStreams paresseux, file watchers, polls

Pièges fréquents en production

  • Retain cycle via Task non annulée — Un 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].
  • Sendable warnings ignorés — En mode "minimal" de strict concurrency, beaucoup de warnings 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.
  • Main actor reentrancy starvation — Si une longue chaîne d'awaits saute du 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.
  • Memory ballooning sur imagesUIImage(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.
  • Crash en background : OOM jetsam — iOS tue agressivement les apps en background dépassant ~50 MB. Surveiller avec os_proc_available_memory() et libérer les caches à la réception de UIApplication.didReceiveMemoryWarningNotification.
  • URLSession bloquée en cellular — Si allowsCellularAccess = false sur une URLSessionConfiguration, les requêtes restent en attente indéfiniment sans erreur. Toujours configurer un timeoutIntervalForResource raisonnable (60-120 s).
  • Build "Optimize for size" -Osize casse les performances — Sur les types génériques lourds (Combine, SwiftUI), -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.
  • Logs en production via 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.

Outils et écosystème

  • Swift Package Manager (SPM) — Outil officiel de gestion des dépendances Swift, intégré à Xcode. Préférable à CocoaPods pour les nouveaux projets : pas de fichier .xcworkspace intrusif, résolution déterministe via Package.resolved. Documentation : swift.org/package-manager
  • SwiftLint — Linter standard de la communauté Swift, plus de 200 règles configurables. À intégrer comme build phase pour bloquer les commits non conformes. Voir github.com/realm/SwiftLint
  • swift-format — Formateur officiel d'Apple, intégré à Xcode 16 (menu Editor → Structure → Format). Compétiteur direct de SwiftLint sur le pan formatage. github.com/apple/swift-format
  • Periphery — Détecte le code mort (classes, méthodes, propriétés non référencées). Indispensable sur des codebases > 50 kLOC pour réduire la surface d'attaque et le temps de compilation. github.com/peripheryapp/periphery
  • Tuist — Gestionnaire de projet Xcode déclaratif. Génère les .xcodeproj depuis du Swift, évite les conflits de merge sur le project.pbxproj en équipe. tuist.io
  • Fastlane — Automatise build, sign, upload TestFlight, captures d'écran. Standard de facto pour CI/CD iOS. fastlane.tools
  • XCBeautify / XCPretty — Reformate la sortie de xcodebuild pour la rendre lisible en CI. github.com/cpisciotta/xcbeautify

Citations sources officielles

"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

Glossaire (10 termes clés)

TermeDéfinition
Structured concurrencyModè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.
ActorType 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.
SendableProtocole marqueur indiquant qu'une valeur peut traverser sans risque les frontières de concurrence. Imposé par le compilateur en mode strict.
MainActorActeur global dont l'exécuteur est le main thread. Toute API UIKit/SwiftUI manipulant l'UI est annotée @MainActor.
TaskUnité 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é.
ReentranceProprié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.
ARCAutomatic 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 poolPool de threads géré par le runtime Swift. Toutes les tasks async s'exécutent dessus sauf les @MainActor. Taille = nb cœurs.
LLDBDébogueur low-level du toolchain LLVM utilisé par Xcode. Permet inspection runtime, breakpoints conditionnels, scripting Python.
SPMSwift Package Manager : système officiel de gestion de dépendances Swift, alternative moderne à CocoaPods et Carthage.

Ressources d'approfondissement

Continuez le parcours 🚀

La leçon suivante est également gratuite. Découvrez-la sans inscription.

Leçon 2 — Continuer →
🍪 Nous utilisons des cookies essentiels et, avec ton accord, des cookies analytiques. En savoir plus

⚙️ Préférences cookies

Choisis quels cookies tu acceptes — modifiable à tout moment.

🔐 Essentiels (obligatoires)Authentification, session, sécurité. Toujours actifs.
📊 Analytics anonymesMesure d'audience anonymisée — aucune donnée personnelle.
📣 MarketingPublicités ITAG pertinentes sur d'autres sites.