MFormations
Modern Mobile Engineering

Chapitre 6

06 - Swift

06 - Swift

06 - Swift : Cours complet

Table des matières

  1. Swift moderne : fondamentaux
  2. Optionals et pattern matching
  3. Protocols et extensions
  4. Generics et enums with associated values
  5. async/await et Task
  6. Structured concurrency
  7. Actors et MainActor
  8. Result type
  9. Codable
  10. Swift 6 : strict concurrency et region-based isolation
  11. Développement de packages
  12. 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 Sendable ou 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

  1. Concurrency checking par défaut (-swift-version 6) : la plupart des warnings deviennent des erreurs.
  2. Les captures de non-Sendable dans les closures @Sendable sont refusées.
  3. nonisolated(unsafe) pour les cas documentés (ex. caches pré-initialisés).
  4. MainActor devient 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 import pour 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.