Chapitre 6
06 - Swift
06 - Swift
06 - Swift : Cours complet
Table des matières
- Swift moderne : fondamentaux
- Optionals et pattern matching
- Protocols et extensions
- Generics et enums with associated values
- async/await et Task
- Structured concurrency
- Actors et MainActor
- Result type
- Codable
- Swift 6 : strict concurrency et region-based isolation
- Développement de packages
- Résumé et checklist
1. Swift moderne : fondamentaux
Swift (Apple, 2014) est un langage compilé, sûr, expressif. Valeurs (struct) et références (class), immutabilité, protocols-first.
struct Product: Identifiable, Hashable {
let id: UUID
var name: String
var priceCents: Int
var tags: Set<String>
}
Principes :
- Value types par défaut (struct) : copie par valeur, pas de fuite de référence.
- Type inference, optionals, protocols first-class.
- Swift est le langage des Apple platforms et de plus en plus des serveurs (Vapor).
2. Optionals et pattern matching
2.1 Optionals
var email: String? = nil
// unwrap
if let email { print(email) } // short binding
guard let email else { return } // early return
// optional chaining
let length = email?.count
// default
let value = email ?? "inconnu"
2.2 Pattern matching et switch
let code = 200
switch code {
case 200...299: print("OK")
case 400, 401, 403: print("Auth")
case let c where c > 500: print("Serveur \(c)")
default: print("Autre")
}
2.3 Enumérations avec associated values
C'est LE pattern Swift pour modéliser des états.
enum LoadState<Value> {
case idle
case loading
case loaded(Value)
case failed(Error)
}
switch state {
case .idle: renderIdle()
case .loading: showSpinner()
case .loaded(let items): renderList(items)
case .failed(let error): showError(error)
}
- enum : type par valeur, cases exhaustives.
- associated values : transporter des données.
- Le compilateur vérifie l'exhaustivité du switch.
3. Protocols et extensions
3.1 Protocols
protocol Repositoriable {
associatedtype Item
func fetch() async throws -> [Item]
func save(_ items: [Item]) throws
}
struct ProductRepository: Repositoriable {
typealias Item = Product
func fetch() async throws -> [Product] { ... }
func save(_ items: [Product]) throws { ... }
}
associatedtype : protocols génériques (ex. Collection, View).
3.2 Extensions
extension Product {
var priceFormatted: String {
String(format: "%.2f €", Double(priceCents) / 100)
}
func isTagged(_ tag: String) -> Bool { tags.contains(tag) }
}
// conformance à un protocol via extension
extension Product: Comparable {
static func < (lhs: Product, rhs: Product) -> Bool {
lhs.priceCents < rhs.priceCents
}
}
3.3 Protocol extensions + default impl
protocol Loggable {
var name: String { get }
}
extension Loggable {
func log() { print("Log: \(name)") } // implémentation par défaut
}
4. Generics et enums
4.1 Generics
func first<T>(of array: [T], matching predicate: (T) -> Bool) -> T? {
array.first(where: predicate)
}
struct Cache<Key: Hashable, Value> {
private var storage: [Key: Value] = [:]
mutating func put(_ value: Value, for key: Key) { storage[key] = value }
func get(_ key: Key) -> Value? { storage[key] }
}
4.2 Contraintes génériques
func sortByPrice<T: Comparable>(_ array: [T]) -> [T] { array.sorted() }
func describe<T: CustomStringConvertible>(_ value: T) -> String { value.description }
4.3 Result et generics
enum Result<Success, Failure: Error> {
case success(Success)
case failure(Failure)
}
5. async/await et Task
5.1 async functions
func fetchProducts() async throws -> [Product] {
let url = URL(string: "https://api.example.com/products")!
let (data, response) = try await URLSession.shared.data(from: url)
guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode) else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode([Product].self, from: data)
}
async: fonction suspendable.await: point de suspension, ne bloque pas le thread.
5.2 Task
Task {
do {
let products = try await fetchProducts()
updateUI(products)
} catch {
showError(error)
}
}
Task { } hérite du contexte (MainActor si lancé depuis l'UI).
5.3 Task.detached
Pour du travail hors du contexte acteur courant (rare, préférer des tasks structurées).
5.4 Task.sleep vs Thread.sleep
try await Task.sleep(nanoseconds: 2_000_000_000) // suspend (annulable)
6. Structured concurrency
6.1 async let
func loadHome() async throws -> HomeData {
async let products = fetchProducts()
async let user = fetchUser()
return HomeData(
products: try await products,
user: try await user
)
}
async let : lance les tâches en parallèle, attend les deux.
6.2 TaskGroup
func fetchAll(_ ids: [UUID]) async throws -> [Product] {
try await withThrowingTaskGroup(of: Product.self) { group in
var products: [Product] = []
for id in ids {
group.addTask { try await fetch(id) }
}
for try await product in group {
products.append(product)
}
return products
}
}
6.3 Annulation
func download() async throws {
try Task.checkCancellation() // throw si annulé
// travail…
try await Task.sleep(...) // annulable
}
Task.isCancelled,Task.checkCancellation(),withTaskCancellationHandler.
7. Actors et MainActor
7.1 Actors
Un acteur protège son état mutable : l'accès est sérialisé, l'isolation d'acteur est vérifiée à la compilation.
actor Counter {
private var value = 0
func increment() { value += 1 }
func current() -> Int { value }
}
let counter = Counter()
Task {
await counter.increment()
}
7.2 MainActor
L'UI doit être isolée sur le main thread : @MainActor.
@MainActor
final class HomeViewModel {
private(set) var products: [Product] = []
func load() async {
let items = await repository.fetch() // travail hors UI
products = items // retour sur main
}
}
7.3 Isolation
@MainActor
func updateUI(_ items: [Product]) { ... }
await updateUI(items)
Règle : les types qui touchent l'UI sont @MainActor. Le modèle peut être nonisolated.
8. Result type
Result<Success, Failure> encapsule succès/échec explicitement.
func parse(_ data: Data) -> Result<Product, ParseError> {
do {
let product = try JSONDecoder().decode(Product.self, from: data)
return .success(product)
} catch {
return .failure(ParseError(message: error.localizedDescription))
}
}
switch parse(data) {
case .success(let p): print(p)
case .failure(let e): print("Erreur : \(e.message)")
}
// mapping
let value = result.map { $0.name } // Result<String, Error>
let either = result.flatMap { secondResult } // flatMap
let unwrapped = try result.get() // throw ou valeur
Utilisation typique : APIs à callback (legacy), opérations où on veut gérer l'erreur localement sans throw.
9. Codable
9.1 Décodage/Encodage
struct Product: Codable {
let id: Int
let name: String
let priceCents: Int
let tags: [String]
enum CodingKeys: String, CodingKey {
case id, name, tags
case priceCents = "price_cents" // mapping JSON
}
}
let decoder = JSONDecoder()
let product = try decoder.decode(Product.self, from: data)
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let data = try encoder.encode(product)
9.2 Custom decoding
struct Price: Codable {
let cents: Int
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
let raw = try c.decode(String.self, forKey: .value) // "45.00"
cents = Int((Double(raw) ?? 0) * 100)
}
}
9.3 Date et stratégies
decoder.dateDecodingStrategy = .iso8601
decoder.keyDecodingStrategy = .convertFromSnakeCase
10. Swift 6 : strict concurrency
10.1 Le contexte
Swift 5.10 marquait le début ; Swift 6 (2024) rend le modèle de concurrence strict par défaut : les erreurs d'accès aux données partagées deviennent des erreurs de compilation.
10.2 Region-based isolation
- Les valeurs sont isolées par région (region-based isolation) : une valeur et ses dérivées appartiennent à une région.
- Transférer une valeur d'une région à une autre = envoyer à travers un
Sendableou une frontière d'acteur. Sendable: type sûr à travers les frontières de concurrence.
struct Settings: Sendable {
let theme: String
let notifications: Bool
}
10.3 Ce qui change en Swift 6
- Concurrency checking par défaut (
-swift-version 6) : la plupart des warnings deviennent des erreurs. - Les captures de
non-Sendabledans les closures@Sendablesont refusées. nonisolated(unsafe)pour les cas documentés (ex. caches pré-initialisés).MainActordevient le défaut pour l'UI.
10.4 Migration
- Compiler en Swift 5 language mode pour commencer (
SWIFT_VERSION = 5), puis migrer par module. @preconcurrency importpour les libs non marquées.- Le Swift 6 Migration Guide d'Apple détaille chaque étape.
11. Développement de packages
11.1 Créer un package
swift package init --name MyKit --type library
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "MyKit",
platforms: [.iOS(.v17), .macOS(.v14)],
products: [
.library(name: "MyKit", targets: ["MyKit"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-log.git", from: "1.6.0")
],
targets: [
.target(name: "MyKit", dependencies: [
.product(name: "Logging", package: "swift-log")
]),
.testTarget(name: "MyKitTests", dependencies: ["MyKit"])
]
)
11.2 Tests
import Testing
@testable import MyKit
struct ProductTests {
@Test func priceFormatted() {
let p = Product(name: "Chaussures", priceCents: 4500)
#expect(p.priceFormatted == "45.00 €")
}
}
Depuis Swift 5.10/6, le framework Testing (Apple) remplace XCTest progressivement.
11.3 Publication
- Hébergement Git (GitHub/GitLab).
- Tags de version (semver :
1.2.3). - Ajout dans un app :
File → Add Package Dependencies.
12. Résumé et checklist
12.1 Points clés
- Value types (struct) et immutabilité.
- Optionals + guard/if-let + nil-coalescing.
- enum avec associated values pour les états.
- Protocols + extensions = composition de comportements.
- Generics avec contraintes.
- async/await, Task, async let, TaskGroup.
- Actors et MainActor : isolation vérifiée à la compilation.
- Result : succès/échec explicites.
- Codable : sérialisation typée.
- Swift 6 : strict concurrency, Sendable, region-based isolation.
- SPM : packages et tests.
12.2 Checklist
- J'écris du Swift idiomatique (no force unwrap, no force cast)
- Je modélise des états avec enum + associated values
- J'utilise protocols + extensions
- Je lance du code concurrent avec async/await et Task
- Je parallélise avec async let / TaskGroup
- J'isole les données avec actor/@MainActor
- J'utilise Result pour les opérations qui échouent
- Je code/décode avec Codable
- Je comprends Sendable et le strict concurrency de Swift 6
- Je crée et teste un package SPM
Prochain chapitre
→ 07-iOS-UI : UIKit avancé, SwiftUI, Auto Layout, diffable data sources.