Bases
Variables & constantes
Utilisez let par défaut pour les valeurs qui ne changent pas ; passez à var uniquement lorsque la mutation est nécessaire. Swift infère les types à la compilation, mais les annotations explicites améliorent la lisibilité pour les types complexes ou ambigus.
var name = "Alice" // mutable
let age = 30 // immutable constant
let pi: Double = 3.14159 // explicit type
var count: Int = 0
count = 10
print(type(of: age)) // IntOptionnels
Les optionnels représentent l'absence d'une valeur. Utilisez if-let pour le déballage sûr, ?? pour les valeurs par défaut, et ! uniquement lorsque vous êtes certain que la valeur existe (risqué — peut crasher).
var nickname: String? = nil
nickname = "Al"
if let n = nickname {
print("Hi, \(n)")
}
print(nickname ?? "Anonymous") // nil-coalescing
let force: String = nickname! // force unwrap (crashes if nil)Tuples
Les tuples regroupent plusieurs valeurs en une seule valeur composée. Utiles pour retourner plusieurs valeurs d'une fonction. Les éléments nommés améliorent la lisibilité.
let person = (name: "Alice", age: 30)
print(person.name) // Alice
print(person.0) // Alice
let (n, a) = person
print("\(n), \(a)") // Alice, 30Conversion de type
Swift ne convertit pas implicitement entre les types. Utilisez toujours la conversion explicite (par ex., Double(intVal)) pour éviter l'ambiguïté et prévenir les bugs subtils.
let intVal = 42
let doubleVal = Double(intVal)
let strVal = String(intVal)
let fromStr = Int("100")! // 100
let invalid = Int("abc") // nil
print(doubleVal, strVal)Assertions & préconditions
Utilisez assert pour les vérifications de débogage (supprimées dans les builds release) et precondition pour les invariants critiques vérifiés dans tous les builds. Les deux aident à attraper les erreurs de logique tôt.
let age = -5
assert(age >= 0, "Age cannot be negative")
precondition(age >= 0, "Age must be non-negative")
// In debug builds, assert crashes if false
// precondition checks in release builds tooChaînes & caractères
Interpolation de chaîne
L'interpolation de chaîne avec \(expr) intègre toute expression dans une chaîne. C'est de type sûr et évalué à la compilation, le rendant plus sûr que les chaînes de format.
let name = "Alice"
let age = 30
let msg = "Name: \(name), Age: \(age)"
let calc = "5 + 3 = \(5 + 3)"
print(msg) // Name: Alice, Age: 30
print(calc) // 5 + 3 = 8Méthodes de chaîne courantes
Les chaînes Swift sont correctes Unicode, ce qui signifie que count reflète les caractères réels (clusters de graphèmes), pas les octets. Utilisez ces méthodes au lieu de l'indexation manuelle pour la sécurité.
let s = "Hello, World"
print(s.count) // 12
print(s.uppercased()) // HELLO, WORLD
print(s.lowercased()) // hello, world
print(s.hasPrefix("Hello")) // true
print(s.contains("World")) // true
print(s.reversed()) // "dlroW ,olleH"Sous-chaîne & indexation
Les indices de chaîne ne sont pas des entiers car les caractères peuvent avoir des tailles d'octet différentes. Utilisez index(_:offsetBy:) pour la navigation. Les sous-chaînes partagent la mémoire avec l'originale — convertissez en String pour le stockage à long terme.
let s = "Hello, World"
let idx = s.index(s.startIndex, offsetBy: 7)
print(s[idx]) // W
let sub = s[idx...] // "World"
let newStr = String(sub) // convert to String
print(s.prefix(5)) // "Hello"Chaînes multilignes
Les chaînes à triples guillemets préservent les sauts de ligne et l'indentation. Les """ fermants déterminent l'indentation de base. Idéal pour HTML, JSON ou les longs blocs de texte.
let poem = """
Roses are red,
Violets are blue,
Swift is great,
And so are you.
"""
print(poem)
// Use \\(expr) for interpolation in multilineConstruction & découpage de chaîne
Utilisez joined(separator:) pour concaténer avec un délimiteur et split(separator:) pour tokeniser. Ce sont plus efficaces que les boucles manuelles avec la concaténation +.
let parts = ["apple", "banana", "cherry"]
let joined = parts.joined(separator: ", ")
print(joined) // apple, banana, cherry
let csv = "a,b,c"
let fields = csv.split(separator: ",")
print(fields) // ["a", "b", "c"]
let reversed = String(s.reversed())Structures de données
Array
Les tableaux sont des collections ordonnées indexées à partir de zéro. Utilisez append/insert pour les ajouts, filter/map/reduce pour les transformations. Préférez les types valeur (Array est une struct) pour la sécurité des threads.
var nums = [1, 2, 3]
nums.append(4)
nums.insert(0, at: 0)
nums[1] = 20
print(nums.count) // 5
print(nums.contains(20)) // true
let evens = nums.filter { $0 % 2 == 0 }
print(nums.first, nums.last)Dictionary
Les dictionnaires stockent des paires clé-valeur avec recherche O(1) en moyenne. Utilisez default: pour éviter nil lors de l'accès aux clés manquantes. Les clés doivent être Hashable.
var ages: [String: Int] = ["Alice": 30]
ages["Bob"] = 25
ages["Alice"] = 31
print(ages["Alice", default: 0]) // 31
for (name, age) in ages {
print("\(name): \(age)")
}
print(ages.keys.sorted())Set
Les sets stockent des valeurs uniques avec test d'appartenance O(1). Idéal pour la déduplication et les opérations ensemblistes (union, intersection, différence). Les éléments doivent être Hashable.
var a: Set<Int> = [1, 2, 3]
var b: Set<Int> = [3, 4, 5]
print(a.union(b)) // {1,2,3,4,5}
print(a.intersection(b)) // {3}
print(a.subtracting(b)) // {1,2}
print(a.isSubset(of: b)) // false
a.insert(6)Ranges
..< est une plage semi-ouverte (exclut la borne supérieure), ... est une plage fermée (inclut les deux). Les plages sont utiles dans les boucles, le découpage et le pattern matching avec l'opérateur ~=.
for i in 0..<5 { print(i) } // 0,1,2,3,4
for i in 0...5 { print(i) } // 0,1,2,3,4,5
let nums = Array(1...5) // [1,2,3,4,5]
let prefix = nums.prefix(3) // [1,2,3]
if 5...10 ~= 7 { print("in range") }Fonctions d'ordre supérieur
map transforme chaque élément, filter sélectionne les éléments correspondants, reduce combine tous en un. Ce sont le fondement de la programmation fonctionnelle en Swift et permettent des pipelines de données concis et lisibles.
let nums = [1, 2, 3, 4, 5]
let doubled = nums.map { $0 * 2 } // [2,4,6,8,10]
let evens = nums.filter { $0 % 2 == 0 } // [2,4]
let sum = nums.reduce(0, +) // 15
let strs = nums.map { String($0) } // ["1","2",...]
print(doubled, evens, sum)Flux de contrôle
If / else if / else
Branche conditionnelle standard. Les conditions doivent être booléennes (type Bool). Swift requiert des accolades même pour les instructions simples pour prévenir des bugs comme la vulnérabilité goto fail d'Apple.
let score = 85
if score >= 90 {
print("A")
} else if score >= 80 {
print("B")
} else if score >= 70 {
print("C")
} else {
print("F")
}Switch (pattern matching)
Le switch de Swift est puissant : supporte la liaison de valeur, les tuples, les gardes where et les plages. Il doit être exhaustif (default couvre les cas restants) et ne tombe pas par défaut.
let point = (2, 0)
switch point {
case (0, 0):
print("origin")
case (_, 0):
print("on x-axis")
case (0, _):
print("on y-axis")
case let (x, y) where x == y:
print("on diagonal")
default:
print("elsewhere: \(point.0),\(point.1)")
}Boucles for-in
For-in itère sur les plages, tableaux, dictionnaires et tout Sequence. Utilisez enumerated() lorsque vous avez besoin de l'index. Utilisez _ pour ignorer les variables de boucle que vous n'utilisez pas.
for i in 0..<5 { print(i) }
let fruits = ["apple", "banana"]
for (i, fruit) in fruits.enumerated() {
print("\(i): \(fruit)")
}
let dict = ["a": 1, "b": 2]
for (k, v) in dict { print("\(k)=\(v)") }
for _ in 0..<3 { print("tick") }While & repeat-while
while vérifie la condition avant chaque itération ; repeat-while vérifie après (comme do-while en C). Utilisez repeat-while lorsque le corps doit s'exécuter au moins une fois.
var n = 5
while n > 0 {
print(n)
n -= 1
}
var x = 0
repeat {
x += 1
} while x < 3
print(x) // 3Guard (sortie anticipée)
guard fournit une sortie anticipée lorsque les prérequis ne sont pas remplis. Les optionnels déballés restent disponibles dans le reste du périmètre, réduisant l'imbrication. Préférez guard à l'imbrication profonde if-let.
func greet(_ name: String?) {
guard let n = name, !n.isEmpty else {
print("No name provided")
return
}
print("Hello, \(n)")
// n is unwrapped and available here
}
greet("Alice")
greet(nil)Fonctions & fermetures
Définition de fonction
Utilisez _ pour omettre les étiquettes d'argument pour la lisibilité. Les valeurs de paramètre par défaut rendent les paramètres optionnels. Les fonctions avec une seule expression ont un retour implicite (Swift 5.9+).
func greet(_ name: String, greeting: String = "Hello") -> String {
return "\(greeting), \(name)!"
}
print(greet("Alice")) // Hello, Alice!
print(greet("Bob", greeting: "Hi")) // Hi, Bob!
func add(_ a: Int, _ b: Int) -> Int { a + b }
print(add(3, 4)) // 7Valeurs de retour multiples
Les tuples permettent de retourner plusieurs valeurs nommées. Retourner un tuple optionnel signale un échec possible. Accédez via r.min ou r.0 — les éléments nommés sont plus clairs.
func minMax(_ nums: [Int]) -> (min: Int, max: Int)? {
guard let first = nums.first else { return nil }
var mn = first, mx = first
for n in nums { mn = min(mn, n); mx = max(mx, n) }
return (mn, mx)
}
if let r = minMax([3, 1, 4, 1, 5]) {
print("min=\(r.min), max=\(r.max)")
}Fermetures
Les fermetures sont des blocs autonomes de fonctionnalité. Utilisez $0, $1 pour les noms d'argument raccourcis. La syntaxe de fermeture finale (omettant la dernière étiquette d'argument) est idiomatique pour map/filter/reduce.
let square: (Int) -> Int = { x in x * x }
print(square(5)) // 25
let add: (Int, Int) -> Int = { $0 + $1 }
print(add(3, 4)) // 7
let nums = [1, 2, 3]
let doubled = nums.map { $0 * 2 }
print(doubled) // [2, 4, 6]Escaping & auto-closures
@escaping marque les fermetures stockées ou appelées après le retour de la fonction (nécessaire pour les rappels async). @autoclosure enveloppe une expression dans une fermeture, retardant son évaluation (utilisé dans assert).
var handlers: [() -> Void] = []
func saveHandler(_ fn: @escaping () -> Void) {
handlers.append(fn)
}
saveHandler { print("done") }
handlers.first?()
// @autoclosure delays evaluation
func logIfTrue(_ cond: @autoclosure () -> Bool) {
if cond() { print("true") }
}
logIfTrue(2 > 1)Paramètres inout
Les paramètres inout permettent aux fonctions de modifier la variable de l'appelant (passage par référence). Utilisez-les avec parcimonie — préférez retourner de nouvelles valeurs pour la clarté. Le préfixe & marque le site de mutation.
func swap(_ a: inout Int, _ b: inout Int) {
let temp = a
a = b
b = temp
}
var x = 10, y = 20
swap(&x, &y)
print(x, y) // 20 10Classes & structs
Struct (type valeur)
Les structs sont des types valeur — copiés à l'affectation. Utilisez-les pour les conteneurs de données simples. Les propriétés calculées (distance) calculent à l'accès. Les méthodes mutating modifient self.
struct Point {
var x: Double
var y: Double
var distance: Double {
(x * x + y * y).squareRoot()
}
mutating func moveBy(dx: Double, dy: Double) {
x += dx; y += dy
}
}
var p = Point(x: 3, y: 4)
print(p.distance) // 5.0Class (type référence)
Les classes sont des types référence — partagées via des références, supportent l'héritage et deinit. Utilisez-les lorsque vous avez besoin d'identité, d'état muable partagé ou d'interopérabilité Objective-C. Sinon préférez les structs.
class Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
deinit { print("\(name) deallocated") }
}
let p = Person(name: "Alice", age: 30)
print(p.name) // AliceHéritage & override
Utilisez override pour redéfinir une méthode de superclasse. Swift utilise la distribution dynamique donc la version de la sous-classe est appelée. Marquez les méthodes final pour empêcher toute redéfinition ultérieure pour la performance.
class Animal {
func speak() { print("...") }
}
class Dog: Animal {
override func speak() { print("Woof!") }
}
class Cat: Animal {
override func speak() { print("Meow!") }
}
let pets: [Animal] = [Dog(), Cat()]
for pet in pets { pet.speak() }Propriétés (calculées & paresseuses)
Les propriétés calculées ont des blocs get/set ; set utilise newValue par défaut. lazy diffère l'initialisation jusqu'au premier accès — utile pour les propriétés coûteuses ou rarement nécessaires. Doit être var.
class Circle {
var radius: Double
var area: Double {
get { .pi * radius * radius }
set { radius = (newValue / .pi).squareRoot() }
}
lazy var heavy = loadExpensiveData()
init(radius: Double) { self.radius = radius }
}
func loadExpensiveData() -> String { "data" }Observateurs de propriété
willSet/didSet observent les changements de propriété. Utilisez didSet pour valider ou réagir aux changements (par ex., borner les valeurs, déclencher des mises à jour UI). Les observateurs ne se déclenchent pas pendant init.
class Counter {
var count: Int = 0 {
willSet { print("about to set \(newValue)") }
didSet {
print("was \(oldValue), now \(count)")
if count > 10 { count = 10 }
}
}
}
let c = Counter()
c.count = 5
c.count = 20 // clamped to 10Protocoles & extensions
Définition de protocole
Les protocoles définissent un plan de méthodes et propriétés. Les types se conforment en les implémentant. Utilisez-les pour l'abstraction, le polymorphisme et le découplage — similaire aux interfaces en Java/C#.
protocol Greetable {
var name: String { get }
func greet() -> String
}
struct User: Greetable {
let name: String
func greet() -> String { "Hi, \(name)" }
}
let u = User(name: "Alice")
print(u.greet()) // Hi, AliceExtensions de protocole (impl par défaut)
Les extensions de protocole fournissent des implémentations par défaut. Cela permet la modélisation rétroactive et la réutilisation de code sans classe de base. Une fonctionnalité puissante pour partager du comportement entre types non liés.
protocol Describable {
func describe() -> String
}
extension Describable {
func describe() -> String { "A \(type(of: self))" }
}
struct Box: Describable { }
let b = Box()
print(b.describe()) // A BoxExtensions
Les extensions ajoutent des fonctionnalités aux types existants (même ceux que vous ne possédez pas, comme Int). Utilisez-les pour organiser le code, ajouter des propriétés calculées ou se conformer à des protocoles. Ne peuvent pas ajouter de propriétés stockées.
extension Int {
var squared: Int { self * self }
func times(_ block: () -> Void) {
for _ in 0..<self { block() }
}
}
print(5.squared) // 25
3.times { print("hi") } // prints hi 3 timesGénériques
Les génériques écrivent du code flexible et réutilisable qui fonctionne avec tout type tout en préservant la sécurité de type. Utilisez <T> pour les paramètres de type. Les contraintes (where T: Equatable) restreignent les types autorisés.
func stackOf<T>(_ items: T...) -> [T] {
var arr: [T] = []
for item in items { arr.append(item) }
return arr
}
struct Stack<Element> {
private var items: [Element] = []
mutating func push(_ e: Element) { items.append(e) }
}
let s = stackOf(1, 2, 3) // [Int]Protocole avec type associé
Les types associés permettent aux protocoles d'utiliser des types d'espace réservé, comme les génériques pour les protocoles. Le type conforme spécifie le type réel. Utilisez typealias pour le rendre explicite, ou laissez Swift l'inférer.
protocol Container {
associatedtype Item
var count: Int { get }
mutating func append(_ item: Item)
subscript(i: Int) -> Item { get }
}
struct IntStack: Container {
typealias Item = Int
private var items: [Int] = []
var count: Int { items.count }
mutating func append(_ item: Int) { items.append(item) }
subscript(i: Int) -> Int { items[i] }
}Gestion des erreurs
Définir & lancer des erreurs
Les erreurs se conforment au protocole Error (généralement des enums). throw signale une erreur. Les valeurs associées (coinsNeeded) transportent le contexte. Utilisez throws pour marquer les fonctions qui peuvent échouer.
enum VendingError: Error {
case invalidSelection
case insufficientFunds(coinsNeeded: Int)
case outOfStock
}
func vend(item: String, coins: Int) throws -> String {
guard item == "Candy" else { throw VendingError.invalidSelection }
guard coins >= 2 else { throw VendingError.insufficientFunds(coinsNeeded: 2) }
return "Dispensing \(item)"
}Do-catch & try
do-catch gère les erreurs lancées. try marque les appels qui lancent. Pattern matchez sur des cas spécifiques pour une gestion ciblée. Un catch-all gère les erreurs inattendues. Les erreurs se propagent vers le haut de la pile d'appels.
do {
let result = try vend(item: "Candy", coins: 1)
print(result)
} catch VendingError.insufficientFunds(let needed) {
print("Need \(needed) more coins")
} catch VendingError.outOfStock {
print("Sold out")
} catch {
print("Other error: \(error)")
}Try? / Try!
try? convertit les erreurs en nil (renvoie Optional). try! suppose le succès et crashe en cas d'erreur — utilisez uniquement lorsque l'échec est véritablement impossible. Préférez try? avec liaison optionnelle pour une dégradation gracieuse.
let result1 = try? vend(item: "Candy", coins: 1)
print(result1) // nil (error converted to optional)
let result2 = try! vend(item: "Candy", coins: 5)
print(result2) // Dispensing Candy (crashes if error)
if let r = try? vend(item: "Candy", coins: 5) {
print(r)
}Type Result
Result encode le succès ou l'échec comme une valeur, permettant la gestion d'erreurs async sans throw. Utilisez-le pour les rappels, les APIs async, ou lorsque vous voulez stocker/enchaîner les résultats. .get() lance en cas d'échec.
enum FetchError: Error { case network, parse }
func fetch(_ url: String) -> Result<String, FetchError> {
url.isEmpty ? .failure(.network) : .success("data from \(url)")
}
switch fetch("https://api.com") {
case .success(let data): print(data)
case .failure(let err): print("Error: \(err)")
}
let value = try? fetch("").get()Defer (nettoyage)
defer planifie le code de nettoyage pour s'exécuter à la sortie du périmètre, quelle que soit la façon (retour normal, throw, erreur). Utilisez-le pour la libération de ressources (fichiers, verrous). Plusieurs defers s'exécutent en ordre LIFO.
func processFile(_ path: String) throws {
let handle = openFile(path)
defer { closeFile(handle) }
// ... work that might throw ...
let data = try read(handle)
// closeFile always runs, even if throw
print("processed \(data)")
}E/S fichiers & date/heure
Lire & écrire des fichiers
Foundation fournit des APIs de fichiers simples. write(toFile:atomically:) écrit en sécurité (fichier temporaire + renommage). Utilisez String(contentsOfFile:) pour le texte. Pour les grands fichiers, utilisez FileHandle pour le streaming.
import Foundation
let path = "test.txt"
let content = "Hello, File I/O!"
try content.write(toFile: path, atomically: true,
encoding: .utf8)
let read = try String(contentsOfFile: path, encoding: .utf8)
print(read) // Hello, File I/O!
let lines = read.split(separator: "\n")
print(lines.count)URL & FileManager
FileManager gère les opérations du système de fichiers (créer, supprimer, vérifier l'existence). Utilisez des URLs (pas des chemins) pour les APIs modernes. .documentDirectory est le stockage persistant de l'app sur iOS/macOS.
import Foundation
let fm = FileManager.default
let url = fm.urls(for: .documentDirectory, in: .userDomainMask)[0]
.appendingPathComponent("data.json")
try "data".write(to: url, atomically: true, encoding: .utf8)
let exists = fm.fileExists(atPath: url.path)
print("Exists: \(exists)")
try fm.removeItem(at: url) // deleteDate & DateFormatter
Date représente un point dans le temps (UTC en interne). DateFormatter convertit entre Date et String — définissez toujours la locale à en_US_POSIX pour l'analyse à format fixe afin d'éviter les bugs de locale.
import Foundation
let now = Date()
let fmt = DateFormatter()
fmt.dateFormat = "yyyy-MM-dd HH:mm:ss"
fmt.locale = Locale(identifier: "en_US_POSIX")
let str = fmt.string(from: now)
print(str) // e.g. 2024-01-15 14:30:00
let parsed = fmt.date(from: "2024-01-01 00:00:00")
print(parsed ?? "invalid")Encodage/décodage JSON
Codable automatise la sérialisation JSON. JSONEncoder/JSONDecoder gèrent la conversion. Conformez vos types à Codable — le compilateur synthétise la logique. Utilisez CodingKeys pour personnaliser les noms de clés.
struct User: Codable {
let name: String
let age: Int
}
let user = User(name: "Alice", age: 30)
let json = try JSONEncoder().encode(user)
let str = String(data: json, encoding: .utf8)!
print(str) // {"name":"Alice","age":30}
let decoded = try JSONDecoder().decode(User.self, from: json)
print(decoded.name) // AliceCalculs de date
Calendar gère l'arithmétique de date en respectant les fuseaux horaires et l'heure d'été. Utilisez dateComponents pour extraire les champs ou calculer les différences. N'utilisez jamais les secondes brutes pour les calculs de date — utilisez les APIs Calendar.
import Foundation
let cal = Calendar.current
let now = Date()
let tomorrow = cal.date(byAdding: .day, value: 1, to: now)!
let components = cal.dateComponents([.year, .month, .day], from: now)
print(components.year!, components.month!)
let diff = cal.dateComponents([.hour], from: now, to: tomorrow)
print(diff.hour!) // 24Concurrence & async
Async / await
async/await (Swift 5.5+) fait que le code async se lit comme du code sync. Task crée un nouveau contexte async. async let tourne concurremment et attend tout. Élimine l'enfer des rappels — préférez-le aux completion handlers.
func fetchUser(_ id: Int) async -> String {
try? await Task.sleep(nanoseconds: 1_000_000_000)
return "User \(id)"
}
Task {
let user = await fetchUser(42)
print(user) // User 42
async let u1 = fetchUser(1)
async let u2 = fetchUser(2)
let users = await [u1, u2] // concurrent
print(users)
}Task & annulation
Task représente une unité de travail async. L'annulation coopérative via Task.isCancelled — les tâches longues devraient vérifier périodiquement. task.value attend le résultat. L'annulation est coopérative, pas forcée.
let task = Task {
for i in 1...100 {
if Task.isCancelled { return }
try? await Task.sleep(nanoseconds: 100_000_000)
print(i)
}
}
Task {
try? await Task.sleep(nanoseconds: 500_000_000)
task.cancel() // request cancellation
}
await task.value // wait for completionActors (sécurité des threads)
Les actors (Swift 5.5+) protègent l'état muable des data races en sérialisant l'accès. Tout l'accès passe par await. Utilisez-les au lieu des verrous/files pour l'état partagé. Le compilateur vérifie la sécurité.
actor Counter {
private var count = 0
func increment() { count += 1 }
func value() -> Int { count }
}
let counter = Counter()
Task {
await counter.increment()
print(await counter.value())
}
// Actors serialize access—no data racesGCD (Grand Central Dispatch)
GCD est l'API de concurrence traditionnelle. DispatchQueue.global() pour le travail en arrière-plan, .main pour les mises à jour UI. QoS (.userInitiated, .background) priorise les tâches. Encore utile pour le code non-async.
import Dispatch
DispatchQueue.global(qos: .userInitiated).async {
let result = heavyComputation()
DispatchQueue.main.async {
print("UI update: \(result)")
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
print("delayed by 2s")
}Async Sequence
AsyncSequence permet d'itérer sur des valeurs async (comme un flux). Utilisez for await pour consommer. Idéal pour les APIs paginées, les server-sent events, ou toute source produisant des valeurs au fil du temps.
let urls = ["url1", "url2", "url3"]
func fetchAll() async {
for url in urls {
let data = await fetchData(url)
print(data)
}
}
func fetchData(_ url: String) async -> String {
"data from \(url)"
}
// AsyncSequence: for await item in stream { ... }Protocoles approfondis
Protocole avec types associés
Les types associés (associatedtype) permettent aux protocoles de déclarer un type d'espace réservé que les types conformes spécifient. C'est l'équivalent Swift des paramètres de type générique pour les protocoles. Le type est inféré depuis les méthodes du type conforme. Utilisez les clauses 'where' pour contraindre les types associés. Les PATs (Protocols with Associated Types) ne peuvent pas être utilisés comme types existentiels directement sans type erasure ou (Swift 5.7+) 'any Container'.
protocol Container {
associatedtype Item
var count: Int { get }
mutating func append(_ item: Item)
subscript(i: Int) -> Item { get }
}
struct IntStack: Container {
// associatedtype Item inferred as Int
private var items: [Int] = []
var count: Int { items.count }
mutating func append(_ item: Int) { items.append(item) }
subscript(i: Int) -> Int { items[i] }
}
// Generic constraint with associated type
func sum<C: Container>(_ c: C) -> Int where C.Item == Int {
var total = 0
for i in 0..<c.count { total += c[i] }
return total
}Extensions de protocole (implémentations par défaut)
Les extensions de protocole fournissent des implémentations par défaut — les types conformes obtiennent la méthode gratuitement mais peuvent la redéfinir. C'est la façon de Swift d'ajouter des fonctionnalités aux types rétroactivement. Les extensions contraintes (where Element: Numeric) ajoutent des méthodes uniquement aux types répondant à la contrainte. C'est ainsi que la bibliothèque standard ajoute map/filter/reduce à toutes les Collections.
protocol Describable {
var description: String { get }
}
// Default implementation via extension
extension Describable {
var description: String { "A \(type(of: self))" }
}
struct Point: Describable {
let x, y: Int
// Uses default description: "A Point"
}
struct Person: Describable {
let name: String
var description: String { "Person named (name)" } // override
}
// Extension with constraints
extension Collection where Element: Numeric {
var sum: Element { reduce(0, +) }
}
[1, 2, 3].sum // 6Composition de protocoles et existentiels
La composition de protocoles (A & B) requiert qu'une valeur se conforme à plusieurs protocoles. Les types existentiels (any Protocol) peuvent contenir tout type conforme mais ont un surcoût de distribution à l'exécution — une recherche de witness table par appel. Utilisez 'some Protocol' (type de retour opaque) lorsque vous renvoyez un type spécifique mais voulez le cacher. Préférez les génériques aux existentiels pour la performance ; utilisez les existentiels lorsque vous avez besoin de collections hétérogènes.
protocol Named { var name: String { get } }
protocol Aged { var age: Int { get } }
struct Person: Named, Aged {
let name: String
let age: Int
}
// Protocol composition: requires both protocols
func wishHappyBirthday(to celebrant: Named & Aged) {
print("Happy birthday, (celebrant.name), you're (celebrant.age)!")
}
let p = Person(name: "Alice", age: 30)
wishHappyBirthday(to: p)
// Existential types (any) — type erasure overhead
let items: [any Named] = [p, Pet(name: "Rex")]
for item in items { print(item.name) }
// Swift 5.7: 'any' keyword required for existentials
// 'some' for opaque types (returns one concrete type)Programmation orientée protocole
La programmation orientée protocole est le changement de paradigme de Swift par rapport à la POO. Au lieu des hiérarchies de classes, concevez autour des protocoles avec des implémentations par défaut. Avantages : fonctionne avec les types valeur (structs/enums), supporte la conformité rétroactive (étendre des types que vous ne possédez pas), permet l''héritage' multiple (un type peut se conformer à plusieurs protocoles). Les classes sont encore utiles pour la sémantique de référence et l'interop Objective-C, mais structs+protocoles sont préférés pour la plupart des modèles.
// POP: design around protocols, not inheritance
protocol Drawable {
func draw(on canvas: Canvas)
}
extension Drawable {
// Default implementation + polymorphism
func drawTwice(on canvas: Canvas) {
draw(on: canvas)
draw(on: canvas)
}
}
struct Circle: Drawable { let radius: Double
func draw(on canvas: Canvas) { /* ... */ }
}
struct Square: Drawable { let side: Double
func draw(on canvas: Canvas) { /* ... */ }
}
// Value types + protocols = no inheritance needed
let shapes: [any Drawable] = [Circle(radius: 5), Square(side: 3)]
shapes.forEach { $0.draw(on: canvas) }
// Retroactive modeling (extend types you don't own)
extension Int: Drawable {
func draw(on canvas: Canvas) { /* draw the number */ }
}Témoins de protocole personnalisés
Les protocoles comme dépendances (Repository) permettent d'échanger les implémentations (réelle vs mock) pour les tests et la flexibilité. Les types associés rendent les protocoles génériques. C'est l'injection de dépendances de Swift — injectez un type conforme au protocole. Le 'protocol witness' est le type conforme qui fournit le comportement concret. Ce pattern (repository, data source) est courant dans l'architecture Swift (VIPER, Clean Architecture).
// Protocol with requirements
protocol Repository {
associatedtype Entity
func get(_ id: Int) -> Entity?
func save(_ entity: Entity)
}
// Concrete implementation
struct UserRepo: Repository {
typealias Entity = User
func get(_ id: Int) -> User? { /* fetch */ nil }
func save(_ user: User) { /* persist */ }
}
// Generic function using protocol
func display<R: Repository>(_ repo: R, id: Int) where R.Entity: CustomStringConvertible {
if let entity = repo.get(id) {
print(entity.description)
}
}
// Test double for testing
struct MockUserRepo: Repository {
typealias Entity = User
var stored: [Int: User] = [:]
func get(_ id: Int) -> User? { stored[id] }
func save(_ user: User) { /* no-op for tests */ }
}Génériques approfondis
Fonctions et types génériques
Les génériques écrivent du code flexible et réutilisable qui fonctionne avec tout type tout en préservant la sécurité de type. T est un paramètre de type (espace réservé). Le compilateur génère des versions spécialisées pour la performance (pas de boxing). Les types génériques (Stack<T>) maintiennent leur paramètre de type. Element est le type générique de Array. Les génériques sont résolus à la compilation — pas de surcoût à l'exécution, contrairement aux existentiels.
// Generic function
func swap<T>(_ a: inout T, _ b: inout T) {
let temp = a; a = b; b = temp
}
var x = 1, y = 2
swap(&x, &y) // x=2, y=1
// Generic type
struct Stack<T> {
private var items: [T] = []
mutating func push(_ item: T) { items.append(item) }
mutating func pop() -> T? { items.popLast() }
}
var intStack = Stack<Int>()
intStack.push(42)
var strStack = Stack<String>()
strStack.push("hello")
// Generic method on non-generic type
extension Array {
func chunked(into size: Int) -> [[Element]] {
stride(from: 0, to: count, by: size).map {
Array(self[$0..<Swift.min($0 + size, count)])
}
}
}Contraintes de type
Les contraintes de type limitent les types utilisables : T: Equatable requiert T conforme à un protocole, T: SomeClass requiert une hiérarchie de classe. La clause where ajoute des contraintes plus complexes (par ex., correspondance des types associés). Les contraintes vous permettent d'utiliser les méthodes du protocole (== pour Equatable, < pour Comparable). Sans contraintes, vous pouvez seulement assigner et passer T — pas d'opérations.
// Constraint: T must conform to Equatable
func indexOf<T: Equatable>(_ item: T, in array: [T]) -> Int? {
for (i, elem) in array.enumerated() {
if elem == item { return i } // needs Equatable
}
return nil
}
// Multiple constraints
func max<T: Comparable>(_ a: T, _ b: T) -> T {
return a > b ? a : b
}
// Constraint on associated type
protocol Repository {
associatedtype Entity: Identifiable
func find(_ id: Entity.ID) -> Entity?
}
// where clause for complex constraints
func merge<C1: Collection, C2: Collection>(
_ c1: C1, _ c2: C2
) -> [C1.Element] where C1.Element == C2.Element {
Array(c1) + Array(c2)
}Types opaques (some)
Les types opaques (some Protocol, Swift 5.1) renvoient un type concret spécifique caché à l'appelant. Contrairement à 'any' (existentiel), le type est fixe et connu du compilateur — pas de boxing, distribution à la compilation. C'est le fondement du 'some View' de SwiftUI. Utilisez 'some' lorsque vous voulez cacher le type concret mais maintenir la performance. L'appelant peut utiliser les méthodes du protocole mais ne peut pas se fier au type spécifique.
// Opaque return type: returns ONE concrete type
// (hidden from caller, but fixed per call site)
func makeStack() -> some Collection<Int> {
return [1, 2, 3] // concrete type is [Int]
}
let s = makeStack() // type is 'some Collection<Int>'
print(s.count) // 3
// Useful for hiding implementation details
protocol Shape { func draw() }
struct Circle: Shape { func draw() { print("circle") } }
func makeShape() -> some Shape {
return Circle() // caller doesn't know it's Circle
}
// Difference from existential (any):
// - some: one concrete type, no boxing, compile-time dispatch
// - any: can be different types, runtime dispatch, boxing
// Opaque types in properties
struct View {
var body: some View { Text("Hello") } // SwiftUI
}Conformité conditionnelle
La conformité conditionnelle fait qu'un type se conforme à un protocole uniquement lorsque ses paramètres de type remplissent certaines conditions. Array<Int> est Equatable car Int l'est ; Array<MyStruct> ne l'est pas sauf si MyStruct l'est. Cela se propage : [[Int]] est Equatable car [Int] l'est. La bibliothèque standard utilise cela largement — Array, Optional, Dictionary se conforment tous conditionnellement à Equatable, Hashable, Codable.
// Array conforms to Equatable only if Element is Equatable
extension Array: Equatable where Element: Equatable {
static func == (lhs: [Element], rhs: [Element]) -> Bool {
return lhs.count == rhs.count && lhs.elementsEqual(rhs)
}
}
[1, 2, 3] == [1, 2, 3] // true (Int: Equatable)
// [SomeStruct] == [SomeStruct] // error if SomeStruct isn't Equatable
// Conditional conformance for Hashable
extension Stack: Equatable where T: Equatable {
static func == (lhs: Stack, rhs: Stack) -> Bool {
return lhs.items == rhs.items
}
}
// Enables: [Stack<Int>] == [Stack<Int>] (transitively)Result et gestion d'erreurs générique
Result<Success, Failure> est un enum générique pour les opérations qui peuvent échouer, transportant soit une valeur soit une erreur. Utile lorsque vous voulez différer la gestion d'erreurs ou stocker des résultats. map transforme la valeur de succès ; flatMap enchaîne les opérations qui peuvent aussi échouer. Result { try ... } convertit les fonctions qui lancent. .get() reconvertit vers throwing. Avec async/await, Result est moins nécessaire mais reste utile pour les erreurs stockées/passées.
// Result: built-in generic for success/failure
enum Result<Success, Failure: Error> {
case success(Success)
case failure(Failure)
}
func fetchUser(_ id: Int) -> Result<User, FetchError> {
if id < 0 { return .failure(.invalidId) }
return .success(User(id: id))
}
switch fetchUser(42) {
case .success(let user): print(user)
case .failure(let err): print(err)
}
// map/flatMap for chaining
let result = fetchUser(1)
.map { $0.name }
.flatMap { name in fetchAvatar(name) }
// Throwing function → Result
let result = Result { try throwingFunction() }
// Result → throwing
let value = try result.get()ARC & gestion de la mémoire
ARC (Automatic Reference Counting)
ARC (Automatic Reference Counting) gère la mémoire pour les instances de classe (types référence). Chaque référence forte incrémente le compteur ; en retirer une le décrémente. Quand le compteur atteint 0, l'objet est libéré immédiatement (déterministe, contrairement au garbage collection). Les structs/enums (types valeur) n'utilisent pas ARC — ils sont copiés. ARC ne peut pas gérer les cycles de référence — vous devez utiliser weak/unowned pour les casser.
class Person {
let name: String
init(name: String) { self.name = name; print("\(name) initialized") }
deinit { print("\(name) deinitialized") }
}
var p1: Person? = Person(name: "Alice") // "Alice initialized"
var p2 = p1 // strong ref count = 2
p1 = nil // ref count = 1, not deinitialized
p2 = nil // ref count = 0, "Alice deinitialized"
// ARC tracks strong references:
// - +1 on assignment/copy
// - -1 when variable goes out of scope
// - Object freed when count hits 0
// Unlike GC, deterministic (freed immediately when count=0)Références strong, weak et unowned
Les références strong (par défaut) maintiennent les objets en vie. Les références weak ne maintiennent pas l'objet en vie et deviennent nil lorsque l'objet est libéré (doit être optional var). Les références unowned ne maintiennent pas l'objet en vie mais sont non-optionnelles — utilisez-les uniquement lorsque vous êtes certain que l'objet référencé survit à la référence (crash si accédé après dealloc). Utilisez weak pour les patterns de délégué ; unowned pour parent-enfant où le parent survit toujours à l'enfant.
class Person {
var name: String
var apartment: Apartment? // strong
init(name: String) { self.name = name }
deinit { print("\(name) deinit") }
}
class Apartment {
var unit: String
weak var tenant: Person? // weak breaks cycle
init(unit: String) { self.unit = unit }
deinit { print("Apartment \(unit) deinit") }
}
var alice: Person? = Person(name: "Alice")
var apt: Apartment? = Apartment(unit: "4A")
alice?.apartment = apt
apt?.tenant = alice // weak, doesn't keep alice alive
alice = nil // "Alice deinit" (no strong cycle)
apt = nil // "Apartment 4A deinit"
// unowned: non-optional, assumed to always have a value
// Use when the referenced object outlives the reference
class Customer {
var card: CreditCard? // strong
}
class CreditCard {
unowned let owner: Customer // non-nil, non-optional
init(owner: Customer) { self.owner = owner }
}Cycles de rétention et fermetures
Les fermetures capturent les références fortement par défaut. Si une classe stocke une fermeture qui capture self, vous avez un cycle de rétention (les deux se maintiennent en vie → fuite de mémoire). Corrigez avec des listes de capture : [weak self] (self devient optionnel, utilisez guard let) ou [unowned self] (non-optionnel, crash si nil). Utilisez toujours weak self dans les fermetures stockées comme propriétés ou passées à des objets longs (observateurs, tâches async).
class ViewModel {
var name = "VM"
var onUpdate: (() -> Void)?
func setup() {
// BAD: closure captures self strongly → cycle
onUpdate = {
print(self.name) // strong capture
}
// ViewModel holds closure, closure holds ViewModel → leak!
// GOOD: capture list with weak self
onUpdate = { [weak self] in
guard let self = self else { return }
print(self.name)
}
// GOOD: unowned if self outlives closure
onUpdate = { [unowned self] in
print(self.name)
}
}
deinit { print("VM deinit") }
}Fermetures escaping et non-escaping
Les fermetures non-escaping (par défaut) s'exécutent dans la fonction et sont jetées — pas de risque de cycle de rétention, pas besoin de self. dans la fermeture. Les fermetures @escaping survivent à la fonction (stockées dans une propriété, dispatchées async, etc.) — elles peuvent causer des cycles de rétention, donc vous devez utiliser self. explicitement et envisager [weak self]. Le compilateur applique cela. La plupart des completion handlers sont @escaping ; map/filter/reduce sont non-escaping.
class DataLoader {
var storedHandler: (() -> Void)?
// @escaping: closure outlives the function (stored or async)
func load(with handler: @escaping (Data) -> Void) {
storedHandler = { handler(cachedData) } // stored
DispatchQueue.global().async {
handler(self.fetch()) // escapes via async
}
}
// Non-escaping (default): closure runs during the function
func process(_ transform: (Int) -> Int) -> [Int] {
return [1, 2, 3].map(transform) // runs now, no storage
}
// No need for [weak self]—self is alive during the call
}
// @escaping requires explicit self. in closures
class View {
func load() {
DataLoader().load { [self] data in // self. required
print(data)
}
}
}Sécurité mémoire et exclusivité
Swift applique l'exclusivité — une variable ne peut pas être accédée (lecture+écriture ou écriture+écriture) simultanément depuis des périmètres qui se chevauchent. Cela prévient les data races et le comportement indéfini. Les paramètres inout obtiennent un accès exclusif pour la durée de l'appel. Les méthodes mutating de struct détiennent un accès exclusif à self. Les actors (Swift 5.5+) appliquent l'exclusivité au niveau du langage pour l'état muable partagé, éliminant les data races par conception.
// Exclusivity: Swift prevents overlapping access to a variable
// This is a compile-time or runtime error:
var step = 1
func increment(_ n: inout Int) {
step += 1 // accessing global while n is inout to step
}
// increment(&step) // ERROR: overlapping access
// Fix with explicit copy
func safeIncrement(_ n: inout Int) {
var copy = step
copy += 1
step = copy
}
// Struct mutating methods have exclusive access
struct Counter {
var count = 0
mutating func increment(_ other: inout Counter) {
count += other.count // ok, different instances
}
}
// 'inout' parameters: exclusive access for duration
// 'isolation' (Swift 5.5+): actors enforce exclusivityBases de SwiftUI
Vues et modificateurs
SwiftUI est le framework UI déclaratif d'Apple. Les vues sont des structs conformes à View avec une propriété 'body'. Les modificateurs (.font, .padding) renvoient de nouvelles vues enveloppées — ils ne mutent pas. Les vues sont des types valeur légers ; SwiftUI les diff pour mettre à jour l'UI réelle. Le style déclaratif décrit à quoi l'UI devrait ressembler pour un état donné, et SwiftUI gère les transitions.
import SwiftUI
struct ContentView: View {
var body: some View {
VStack(spacing: 20) {
Text("Hello, World!")
.font(.title)
.fontWeight(.bold)
.foregroundColor(.blue)
Button(action: { print("tapped") }) {
HStack {
Image(systemName: "star.fill")
Text("Tap me")
}
}
.padding()
.background(Color.orange)
.cornerRadius(10)
}
.padding()
}
}
// Views are value types (structs), declarative
// Modifiers return new views (immutability)State et Binding
@State est pour l'état de vue local et muable — lorsqu'il change, SwiftUI re-rend la vue. @Binding laisse une vue enfant lire/écrire le @State d'un parent via le préfixe $ (crée une liaison). L'état devrait être la source unique de vérité ; passez les liaisons vers le bas, pas les valeurs à modifier. Pour l'état partagé/complexe à travers les vues, utilisez @StateObject/@ObservedObject/@EnvironmentObject avec ObservableObject.
struct CounterView: View {
@State private var count = 0 // local, mutable state
var body: some View {
VStack {
Text("Count: \(count)")
Button("Increment") { count += 1 }
}
}
}
// @Binding: child receives a reference to parent's state
struct StepperView: View {
@Binding var value: Int
var body: some View {
HStack {
Button("-") { value -= 1 }
Text("\(value)")
Button("+") { value += 1 }
}
}
}
// Parent passes binding with $
struct ParentView: View {
@State var score = 0
var body: some View {
StepperView(value: $score) // $ creates a Binding
}
}ObservableObject et EnvironmentObject
ObservableObject (classe) est pour l'état partagé à travers plusieurs vues. Les propriétés @Published déclenchent des mises à jour de vue lorsqu'elles changent. @StateObject crée et possède l'objet (utilisez en haut de la hiérarchie). @EnvironmentObject injecte l'objet dans l'arbre de vue — tout descendant peut y accéder sans passer par des liaisons. @ObservedObject reçoit un objet existant (ne le possède pas). Utilisez @StateObject pour la propriété, @ObservedObject/@EnvironmentObject pour la réception.
// Model: ObservableObject for shared state
class AppModel: ObservableObject {
@Published var username: String = "" // @Published triggers UI update
@Published var isLoggedIn: Bool = false
func login() {
// ... auth logic ...
isLoggedIn = true
}
}
// @StateObject: owns the model (created once)
struct RootView: View {
@StateObject var model = AppModel()
var body: some View {
// Inject into environment
ContentView().environmentObject(model)
}
}
// @EnvironmentObject: receives from environment
struct ProfileView: View {
@EnvironmentObject var model: AppModel
var body: some View {
Text("Hello, \(model.username)")
}
}
// @ObservedObject: receives from parent (doesn't own)
struct LoginView: View {
@ObservedObject var model: AppModel
}Listes et navigation
List rend des lignes défilables (comme UITableView). Les éléments doivent se conformer à Identifiable (ou vous fournissez id:). NavigationStack (iOS 16+) gère la navigation ; NavigationLink pousse les destinations. LazyVStack/LazyHStack chargent le contenu paresseusement pour la performance. ForEach est pour répéter des vues à l'intérieur d'autres conteneurs. Utilisez List pour les données tabulaires, ScrollView+LazyVStack pour les dispositions personnalisées.
struct Item: Identifiable {
let id = UUID()
let name: String
}
struct ListView: View {
let items = [Item(name: "Apple"), Item(name: "Banana")]
var body: some View {
NavigationStack {
List(items) { item in
NavigationLink(item.name) {
DetailView(item: item)
}
}
.navigationTitle("Fruits")
}
}
}
struct DetailView: View {
let item: Item
var body: some View {
Text("Detail: \(item.name)")
.navigationTitle(item.name)
}
}
// ForEach for custom layouts
ScrollView {
LazyVStack {
ForEach(items) { item in
Text(item.name)
}
}
}Formulaires et feuilles
Form stylise automatiquement les contrôles pour les écrans de paramètres (groupé, approprié à la plateforme). Contrôles courants : TextField, Toggle, Slider, Picker, Stepper. .sheet présente un modal ; .fullScreenCover pour plein écran. Les liaisons ($) connectent les contrôles à l'état. Form s'adapte à la plateforme (liste groupée iOS, disposition de formulaire macOS). Utilisez Form pour la saisie de données et les paramètres ; utilisez VStack pour les dispositions personnalisées.
struct SettingsView: View {
@State private var name = ""
@State private var notifications = true
@State private var volume: Double = 0.5
@State private var theme = "Light"
@State private var showSheet = false
var body: some View {
Form {
Section("Profile") {
TextField("Name", text: $name)
Toggle("Notifications", isOn: $notifications)
}
Section("Audio") {
Slider(value: $volume, in: 0...1)
}
Section("Appearance") {
Picker("Theme", selection: $theme) {
Text("Light").tag("Light")
Text("Dark").tag("Dark")
}
}
Button("Show Modal") { showSheet = true }
}
.sheet(isPresented: $showSheet) {
Text("Modal content")
}
}
}Fermetures approfondies
Variations de syntaxe de fermeture
Les fermetures ont de nombreuses formes syntaxiques. Forme complète : { (params) -> Type in body }. L'inférence de type vous permet d'omettre les types. Les arguments raccourcis ($0, $1) remplacent les params nommés. La syntaxe de fermeture finale déplace la fermeture hors de () lorsque c'est le dernier arg. Les fermetures finales multiples (Swift 5.3+) nomment les fermetures supplémentaires. Utilisez la forme la plus concise encore lisible — les arguments raccourcis sont géniaux pour les courtes fermetures comme map.
// Full closure
let add: (Int, Int) -> Int = { (a: Int, b: Int) -> Int in
return a + b
}
// Type inference
let add2: (Int, Int) -> Int = { a, b in a + b }
// Shorthand argument names ($0, $1...)
let add3: (Int, Int) -> Int = { $0 + $1 }
// Trailing closure syntax
let result = [1, 2, 3].map { $0 * 2 } // [2, 4, 6]
// Multiple trailing closures (Swift 5.3+)
Button {
print("action") // action
} label: {
Text("Click") // label
}
// No return for Void closures
let log: (String) -> Void = { msg in print(msg) }Capture de valeurs
Les fermetures capturent les variables de leur périmètre englobant. Par défaut, elles capturent par référence (les changements à la variable capturée sont visibles). Les listes de capture [foo] capturent par valeur (un instantané à la création de la fermeture). Pour les types référence (classes), [weak self] ou [unowned self] cassent les cycles de rétention. Les types valeur (structs) capturés par référence voient toujours les mises à jour car la fermeture détient une box. Les listes de capture vont avant la liste de paramètres.
// Closures capture surrounding state
func makeIncrementer(amount: Int) -> () -> Int {
var total = 0
return {
total += amount // captures total and amount
return total
}
}
let inc = makeIncrementer(amount: 5)
inc() // 5
inc() // 10
// total persists between calls (captured by reference)
// Capture list controls how
class Foo { var x = 0 }
let foo = Foo()
let closure = { [foo] in // captures foo by value (snapshot)
print(foo.x)
}
foo.x = 100
closure() // prints 0 (captured the old value)
// Without capture list, foo is captured by reference
let refClosure = { print(foo.x) }
foo.x = 200
refClosure() // prints 200Escaping et autoclosure
@autoclosure enveloppe une expression dans une fermeture sans argument automatiquement, donc les appelants écrivent l'expression directement (pas d'accolades). Cela permet l'évaluation paresseuse — assert() l'utilise pour sauter la condition dans les builds release. Combiné avec @escaping, vous pouvez différer l'évaluation. Les opérateurs && et || utilisent @autoclosure pour l'évaluation en court-circuit. Utilisez @autoclosure avec parcimonie — il cache que le code est différé, ce qui peut confondre les lecteurs.
// @autoclosure: wraps an expression in a closure automatically
func log(_ condition: @autoclosure () -> Bool, _ message: String) {
if condition() { // evaluated here, not at call
print(message)
}
}
log(2 > 1, "two is greater") // no { } needed
// assert uses @autoclosure to avoid evaluating in release
assert(isValid, "Invalid state") // isValid not checked in release
// @autoclosure + @escaping
func deferred(_ block: @autoclosure @escaping () -> Int) {
DispatchQueue.main.async { print(block()) }
}
deferred(expensiveComputation()) // runs later, on main
// Short-circuit evaluation with @autoclosure
func &&(lhs: @autoclosure () -> Bool, rhs: @autoclosure () -> Bool) -> Bool {
return lhs() ? rhs() : false // rhs not evaluated if lhs is false
}Fonctions d'ordre supérieur
Les fonctions d'ordre supérieur de Swift (map, filter, reduce, flatMap, compactMap) permettent la programmation fonctionnelle sur les collections. map transforme, filter sélectionne, reduce agrège, flatMap aplatit, compactMap retire les nil. Elles renvoient de nouvelles collections (immuabilité). Le chaînage compose les opérations. Pour les grands jeux de données, utilisez .lazy pour éviter les tableaux intermédiaires. Ce sont le fondement du Swift fonctionnel — préférez-les aux boucles for impératives lors de la transformation de données.
let nums = [1, 2, 3, 4, 5, 6]
// map: transform each element
let doubled = nums.map { $0 * 2 } // [2, 4, 6, 8, 10, 12]
// filter: keep elements matching predicate
let evens = nums.filter { $0 % 2 == 0 } // [2, 4, 6]
// reduce: combine all into one
let sum = nums.reduce(0, +) // 21
let product = nums.reduce(1, *) // 720
// flatMap: transform + flatten
let nested = [[1, 2], [3, 4]]
let flat = nested.flatMap { $0 } // [1, 2, 3, 4]
// compactMap: transform + filter nils
let strings = ["1", "abc", "3"]
let ints = strings.compactMap { Int($0) } // [1, 3]
// Chaining (lazy evaluation)
let result = nums
.filter { $0 % 2 == 0 }
.map { $0 * $0 }
.reduce(0, +) // 4 + 16 + 36 = 56Fermetures comme completion handlers
Les completion handlers étaient le pattern async standard avant async/await (Swift 5.5). Ils sont @escaping (s'exécutent plus tard) et utilisent souvent Result pour la gestion d'erreurs. L'inconvénient est les rappels imbriqués ('pyramide de doom'). async/await rend cela linéaire et lisible. Le nouveau code devrait utiliser async/await ; les completion handlers restent pour les APIs déléguées et l'interop Objective-C. Vous pouvez envelopper les completion handlers avec withCheckedContinuation pour les utiliser avec async/await.
// Completion handler pattern (pre-async/await)
func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
URLSession.shared.dataTask(with: url) { data, _, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data,
let user = try? JSONDecoder().decode(User.self, from: data) else {
completion(.failure(DecodeError()))
return
}
completion(.success(user))
}.resume()
}
// Usage (pyramid of doom)
fetchUser(id: 42) { result in
switch result {
case .success(let user):
fetchAvatar(user) { avatarResult in
// nested...
}
case .failure(let error):
print(error)
}
}
// Modern: async/await replaces this
func fetchUserAsync(id: Int) async throws -> User {
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}Property wrappers
Définir un property wrapper
Les property wrappers (@propertyWrapper) encapsulent un comportement de propriété réutilisable. La propriété wrappedValue est ce avec quoi les utilisateurs interagissent. L'init reçoit la valeur initiale et tout argument personnalisé. SwiftUI utilise largement les property wrappers : @State, @Binding, @Published, @AppStorage. Définissez les vôtres pour la validation, le cache, la journalisation ou les valeurs par défaut. Le wrapper est une struct/class qui gère le stockage.
@propertyWrapper
struct Clamped<Value: Comparable> {
var value: Value
let range: ClosedRange<Value>
var wrappedValue: Value {
get { value }
set { value = min(max(newValue, range.lowerBound), range.upperBound) }
}
init(wrappedValue: Value, _ range: ClosedRange<Value>) {
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
self.range = range
}
}
// Usage
struct Slider {
@Clamped(0...100) var progress: Int = 50
}
var s = Slider()
s.progress = 150 // clamped to 100
s.progress = -10 // clamped to 0
print(s.progress) // 0Valeur projetée
La projectedValue (accessible via $) est une interface secondaire à la propriété. SwiftUI l'utilise largement : le $ de @State donne une Binding pour les vues enfants, le $ de @Published donne un Publisher. Définissez projectedValue lorsque le wrapper devrait exposer quelque chose au-delà de la valeur enveloppée (une liaison, un publisher, le wrapper lui-même). La syntaxe de préfixe $ rend cela ergonomique. Tous les wrappers n'ont pas besoin d'une projectedValue — c'est optionnel.
@propertyWrapper
struct Observable<T> {
var value: T
var wrappedValue: T {
get { value }
set { value = newValue; onChange() }
}
var onChange: () -> Void = {}
// projectedValue: accessed via $ (like @State's binding)
var projectedValue: Observable<T> { self }
}
struct View {
@Observable var count = 0
// count: the wrapped value (Int)
// $count: the projected value (Observable<Int>)
}
// SwiftUI's @State projects a Binding
struct Counter: View {
@State var value = 0
var body: some View {
Stepper("Value: \(value)", value: $value) // Binding<Int>
}
}Property wrappers intégrés
SwiftUI fournit de nombreux property wrappers : @State (état local), @Binding (état parent), @ObservedObject/@StateObject (modèle externe), @EnvironmentObject (modèle injecté), @AppStorage (UserDefaults), @SceneStorage (restauration de scène), @FocusState (focus clavier), @ScaledMetric (Dynamic Type), @Namespace (animations). Chacun gère le stockage et le cycle de vie. Le @Published de Combine déclenche les mises à jour UI. Savoir lequel utiliser quand est la clé de SwiftUI.
import SwiftUI
import Combine
struct ContentView: View {
@State var count = 0 // local view state
@Binding var externalValue: Int // parent-owned
@EnvironmentObject var model: AppModel // injected
@AppStorage("username") var username = "" // UserDefaults
@SceneStorage("draft") var draft = "" // per-scene storage
@FocusState var isFocused: Bool // focus management
@ScaledMetric var size = 16 // Dynamic Type scaling
@Namespace var animationNamespace // matched geometry
// Combine
@Published var data = [Item]() // in ObservableObject
}
// @AppStorage auto-syncs with UserDefaults
// @SceneStorage restores state per scene (iPad multitasking)Wrappers personnalisés pour validation
Les property wrappers brillent pour les préoccupations transversales. NonEmpty valide à la définition. Cached calcule paresseusement et mémoïse. Autres idées : Logged (journalise les changements), Trimmed (trim les espaces), Formatted (formate au get), Clamped (limites de plage), UserDefaults-backed. Les wrappers réduisent le boilerplate — vous écrivez la logique une fois et l'appliquez avec @. Ils se composent : @Logged @Clamped(0...100) var value. Gardez les wrappers concentrés sur une préoccupation.
@propertyWrapper
struct NonEmpty {
private var value: String = ""
var wrappedValue: String {
get { value }
set {
guard !newValue.isEmpty else {
print("Warning: cannot set to empty")
return
}
value = newValue
}
}
init(wrappedValue: String) {
self.wrappedValue = wrappedValue
}
}
@propertyWrapper
struct Cached<T> {
private var cached: T?
private var generator: () -> T
var wrappedValue: T {
mutating get {
if cached == nil { cached = generator() }
return cached!
}
}
init(wrappedValue: @autoclosure @escaping () -> T) {
self.generator = wrappedValue
}
}
struct Config {
@NonEmpty var name: String = "default"
@Cached var expensiveValue: Int = computeExpensive()
}Property wrappers dans les protocoles et génériques
Les property wrappers peuvent être génériques et utilisés dans les protocoles. Cependant, ils ont des limitations : une struct avec des propriétés enveloppées n'obtient pas d'initialiseurs memberwise (vous devez écrire init manuellement), et la copie peut être délicate (le wrapper est copié, y compris son état). Pour les classes, c'est moins un problème. Malgré les limitations, les wrappers sont puissants pour un comportement de propriété déclaratif et réutilisable. La conception de SwiftUI s'appuie largement dessus.
// Apply to protocol requirements
protocol Configurable {
@NonEmpty var name: String { get set }
}
// Generic property wrapper
@propertyWrapper
struct Validated<T> {
private var value: T?
let validator: (T) -> Bool
var wrappedValue: T? {
get { value }
set {
if let v = newValue, validator(v) { value = v }
}
}
init(wrappedValue: T?, validator: @escaping (T) -> Bool) {
self.validator = validator
self.wrappedValue = wrappedValue
}
}
struct Form {
@Validated(validator: { $0.count >= 8 }) var password: String? = nil
}
// Limitation: property wrappers in structs can't be
// initialized from another instance easily (copy issues).
// They work best as stored properties, not computed.Framework Combine
Publishers et subscribers
Combine est le framework réactif d'Apple (comme RxSwift). Les Publishers émettent des valeurs ; les Subscribers les reçoivent. sink crée un subscriber avec des fermetures. assign lie la sortie à une propriété. Les opérateurs (map, filter, reduce) transforment les publishers déclarativement. Combine est déclaratif — décrivez le pipeline, et les valeurs y circulent. Utilisé pour les opérations async, les liaisons UI et la gestion d'événements. Le Swift moderne préfère async/await, mais Combine reste pour les pipelines complexes.
import Combine
// Publisher: emits values over time
let publisher = [1, 2, 3].publisher
// Subscriber: receives values
publisher.sink { value in
print(value) // 1, 2, 3
}
// With completion
publisher.sink(receiveCompletion: { completion in
print("done: \(completion)")
}, receiveValue: { value in
print(value)
})
// Assign to a property
class ViewModel {
var data: String = ""
}
let vm = ViewModel()
Just("hello").assign(to: &vm.data)
// Operators transform publishers
[1, 2, 3, 4].publisher
.map { $0 * 2 }
.filter { $0 > 4 }
.sink { print($0) } // 6, 8@Published et ObservableObject
@Published enveloppe une propriété et émet sa nouvelle valeur aux subscribers à chaque changement. Le préfixe $ accède au publisher. Cela fait le pont entre Combine et SwiftUI — @Published dans un ObservableObject déclenche les mises à jour de vue. Les subscriptions sont annulées lorsque le AnyCancellable est désalloué, donc stockez-les (par ex., dans un Set). Utilisez Combine pour le debouncing, le throttling, la combinaison de sources async multiples — des choses qu'async/await gère moins élégamment.
class UserStore: ObservableObject {
@Published var name: String = "" // publishes on change
@Published var age: Int = 0
// $name is a Publisher<String, Never>
func demo() {
$name.sink { print("name changed to \($0)") }
name = "Alice" // prints "name changed to Alice"
}
}
// Combine with SwiftUI
struct ProfileView: View {
@ObservedObject var store: UserStore
var body: some View {
Text(store.name) // updates when name changes
}
}
// Cancellable: store subscriptions to keep them alive
class Service {
var cancellables = Set<AnyCancellable>()
let store = UserStore()
init {
store.$name
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.sink { name in saveToServer(name) }
.store(in: &cancellables) // keep alive
}
}Opérateurs
Les opérateurs de Combine transforment, filtrent, combinent et contrôlent temporellement les publishers. map/scan transforment ; filter/selectors choisissent ; merge/zip/combineLatest combinent plusieurs flux ; debounce/throttle contrôlent le timing. Les opérateurs renvoient de nouveaux publishers (immuabilité, chaînage). debounce attend une période calme (search-as-you-type) ; throttle limite le taux (taps de bouton). collect rassemble toutes les valeurs dans un tableau. Ils permettent des pipelines réactifs déclaratifs.
let publisher = [1, 2, 3, 4, 5].publisher
// Transforming
publisher.map { $0 * 2 } // [2, 4, 6, 8, 10]
publisher.scan(0, +) // [1, 3, 6, 10, 15] (running sum)
// Filtering
publisher.filter { $0 % 2 == 0 } // [2, 4]
publisher.removeDuplicates() // consecutive dupes
publisher.first() // just 1
// Combining
let p1 = [1, 2].publisher
let p2 = [3, 4].publisher
p1.merge(with: p2) // interleaved: 1, 2, 3, 4 (or any order)
p1.zip(p2) // pairs: (1,3), (2,4)
p1.combineLatest(p2) // latest of each
// Timing
publisher.delay(for: 1, scheduler: RunLoop.main)
publisher.throttle(for: 1, scheduler: .main, latest: true)
publisher.debounce(for: 0.5, scheduler: .main)
publisher.collect() // gather all into [1,2,3,4,5]Subjects (publication manuelle)
Les Subjects sont des publishers muables auxquels vous pouvez envoyer des valeurs manuellement — faisant le pont entre le code impératif et réactif. PassthroughSubject diffuse sans stocker (flux d'événements). CurrentValueSubject stocke la dernière valeur (état). Les nouveaux subscribers à CurrentValueSubject obtiennent immédiatement la valeur courante. Utilisez les Subjects pour envelopper les délégués, notifications ou événements UI en pipelines Combine. @Published est essentiellement un CurrentValueSubject intégré à ObservableObject.
import Combine
// PassthroughSubject: broadcasts to subscribers, no current value
let subject = PassthroughSubject<Int, Never>()
subject.sink { print($0) } // subscriber 1
subject.send(1) // prints 1
subject.send(2) // prints 2
// CurrentValueSubject: holds current value, new subs get it
let current = CurrentValueSubject<Int, Never>(0)
current.sink { print($0) } // prints 0 (current)
current.send(1) // prints 1
current.sink { print($0) } // prints 1 (current)
current.value // 1
// Use cases:
// - PassthroughSubject: events (button taps, notifications)
// - CurrentValueSubject: state (like @Published but more control)
// - Bridge imperative code to Combine
// Finish a subject
subject.send(completion: .finished)Gestion des erreurs
Les publishers de Combine ont un type Failure (Never pour ne peut pas échouer). catch remplace une erreur par un publisher de repli. retry se ré-abonne en cas d'échec (bon pour les réseaux instables). mapError transforme le type d'erreur. assertNoFailure crash si une erreur se produit (utilisez lorsque vous êtes certain). Les publishers avec Failure == Never peuvent être utilisés partout ; ceux avec erreurs nécessitent une gestion. Cela rend la gestion d'erreurs explicite et composable dans les pipelines réactifs.
enum APIError: Error { case network, parse }
// Publishers can fail with an error
let publisher = Fail<Int, APIError>(error: .network)
// catch: replace error with fallback publisher
publisher
.catch { _ in Just(0) } // fallback to 0
.sink { print($0) } // 0
// retry: retry on failure
URLSession.shared.dataTaskPublisher(for: url)
.retry(3) // retry up to 3 times
.sink(receiveCompletion: { _ in }, receiveValue: { _ in })
// mapError: transform error type
publisher
.mapError { _ in NSError(domain: "x", code: 1) }
// assertNoFailure: crash if error (when you're sure it won't)
Just(1).assertNoFailure().sink { print($0) }
// Result publisher
let resultPublisher = [1, 2, 3].publisher.setFailureType(to: Error.self)Contrôle d'accès & organisation du code
Niveaux d'accès
Swift a cinq niveaux d'accès. private (périmètre le plus petit, dans la déclaration), fileprivate (dans le fichier source), internal (dans le module, par défaut), public (tout importateur), open (public + sous-classable/redéfinissable, classes uniquement). Swift 5.9 ajoute package pour les modules SPM. Commencez restrictif et élargissez au besoin. Utilisez public pour les APIs de bibliothèque, internal pour le code d'app, private pour les détails d'implémentation. open est pour les classes de framework conçues pour l'héritage.
// public: accessible from any module that imports this
public class PublicAPI {
public func connect() {}
}
// internal (default): accessible within the module
class InternalClass {
func helper() {} // internal by default
}
// fileprivate: accessible within the source file
fileprivate func fileHelper() {}
// private: accessible within the enclosing declaration
class Counter {
private var count = 0 // only Counter can access
fileprivate func reset() { count = 0 } // same file
}
// Swift 5.9: package (for Swift Package Manager)
package struct PackageType {
package var value: Int
}
// Guidelines:
// - Start restrictive (private), open up as needed
// - public for framework API, internal for implementation
// - private for encapsulation within a typeExtensions et organisation
Les extensions vous permettent de diviser la fonctionnalité d'un type à travers des fichiers (String+Validation.swift, String+Parsing.swift) et d'ajouter la conformité rétroactivement. Cela garde les fichiers concentrés et gérables. La conformité de protocole peut être dans une extension/fichier séparé de la définition de type principale. La conformité conditionnelle (where Element: ...) ajoute la conformité conditionnellement. Utilisez les extensions pour organiser : gardez le type principal minimal, ajoutez la fonctionnalité dans des fichiers d'extension focalisés.
// Split a type across files with extensions
// String+Validation.swift
extension String {
var isValidEmail: Bool {
contains("@") && contains(".")
}
}
// String+Parsing.swift
extension String {
func toURL() -> URL? { URL(string: self) }
}
// Conform to protocols in extensions
extension String: Comparable { // already conforms, just showing
// protocol methods
}
// Conditional conformance
extension Array: MyProtocol where Element: MyProtocol {}
// Protocol conformance in a separate file
// (keeps the main type definition clean)
struct User { let id: Int; let name: String }
extension User: Codable {} // synthesized
extension User: Equatable {} // synthesized
extension User: CustomStringConvertible {
var description: String { "User(\(id), \(name))" }
}Modules et packages
Swift Package Manager (SPM) définit les modules via Package.swift. Chaque cible est un module — l'accès internal est dans une cible, public traverse les modules. Les dépendances sont d'autres packages (URLs git). Les produits sont ce que votre package expose. SPM est la façon moderne de gérer le code Swift (remplaçant CocoaPods/Carthage). Xcode intègre SPM nativement. Structure : Sources/MyLibrary/ pour le code, Tests/MyLibraryTests/ pour les tests. Utilisez les modules pour séparer les préoccupations et contrôler l'accès.
// Package.swift (Swift Package Manager)
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "MyLibrary",
products: [
.library(name: "MyLibrary", targets: ["MyLibrary"]),
],
dependencies: [
.package(url: "https://github.com/.../Logging", from: "1.0.0"),
],
targets: [
.target(name: "MyLibrary", dependencies: ["Logging"]),
.testTarget(name: "MyLibraryTests", dependencies: ["MyLibrary"]),
]
)
// Importing modules
import Foundation
import MyLibrary // your module
import Logging // dependency
// Each target is a module
// internal access is within a target/module
// public access crosses module boundariesInitialiseurs et designated/convenience
Les classes ont des initialiseurs designated (init complet, doit appeler le designated du super) et des initialiseurs convenience (délèguent à un autre init dans la même classe). Ce système à deux niveaux garantit une initialisation complète. Les structs obtiennent un initialiseur memberwise gratuitement. Les initialiseurs faillibles (init?) renvoient nil en cas d'échec. Les initialiseurs requis (required init) doivent être implémentés par les sous-classes. Pour la plupart du code, préférez les structs (initialisation plus simple) aux classes.
class Person {
let name: String
let age: Int
// Designated initializer: fully initializes
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
class Employee: Person {
let employeeId: String
// Designated init must call super's designated init
init(name: String, age: Int, employeeId: String) {
self.employeeId = employeeId
super.init(name: name, age: age) // must call super
}
// Convenience init must call another init (same class)
convenience init(name: String) {
self.init(name: name, age: 0, employeeId: "TEMP")
}
}
// Structs: memberwise initializer (free)
struct Point { let x, y: Int }
let p = Point(x: 1, y: 2) // synthesized
// Failable initializers
struct Config {
let port: Int
init?(port: Int) {
guard port > 0 else { return nil }
self.port = port
}
}Optionnels approfondis
Les optionnels sont le mécanisme de sécurité null de Swift — un enum qui est soit .some(value) soit .none (nil). Vous devez déballer pour accéder à la valeur. L'enchaînement optionnel (?.) navigue en sécurité ; le nil-coalescing (??) fournit une valeur par défaut ; if let/guard let déballent en sécurité. Évitez ! (force unwrap) sauf si vous êtes certain — il crash sur nil. Les optionnels vous forcent à gérer l'absence explicitement, éliminant les exceptions de référence null courantes dans d'autres langages.
// Optional: enum with .some(Value) and .none
let maybeInt: Int? = 42
let nilInt: Int? = nil
// Optional chaining (short-circuits on nil)
let name: String? = user?.profile?.name // nil if any link is nil
// Nil-coalescing (default value)
let value = maybeInt ?? 0 // 42
let value2 = nilInt ?? 0 // 0
// Forced unwrapping (crashes if nil)
let forced = maybeInt! // 42
// let crash = nilInt! // RUNTIME CRASH
// Optional binding
if let actual = maybeInt {
print(actual) // 42, actual is Int (not optional)
}
// Guard for early exit
func process(_ data: Data?) {
guard let data = data else { return }
// data is non-optional here
}
// Optional try
let result = try? parse(json) // nil on error
let result2 = try! parse(json) // crashes on error
// Multiple binding
if let a = optA, let b = optB, a < b { /* ... */ }Gestion de la mémoire (ARC/weak)
Bases d'ARC
Automatic Reference Counting (ARC) suit les références fortes. Quand le compteur atteint zéro, l'objet est désalloué. ARC est déterministe (contrairement au garbage collection) : deinit s'exécute immédiatement lorsque la dernière référence est libérée. La plupart du temps, ARC fonctionne simplement. Les problèmes surviennent avec les cycles de référence : deux objets se référencient fortement l'un l'autre, empêchant la désallocation. Utilisez weak ou unowned pour casser les cycles.
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
var reference1: Person?
var reference2: Person?
reference1 = Person(name: "Alice")
// Prints: Alice is being initialized
reference2 = reference1 // Strong reference count: 2
reference1 = nil // Count: 1
reference2 = nil // Count: 0, deinit runs
// Prints: Alice is being deinitializedRéférences weak
Les références weak ne maintiennent pas l'objet référencé en vie. Quand l'objet est désalloué, les références weak deviennent automatiquement nil. weak doit être optional (var tenant: Person?). Utilisez weak lorsque l'objet référencé a une durée de vie plus courte (le locataire peut quitter l'appartement). Le cas d'usage classique est le pattern délégué : le délégué est weak pour éviter les cycles. weak a un léger surcoût (enregistré avec le runtime pour le nil-setting).
class Person {
var name: String
var apartment: Apartment? // Strong (person owns apartment)
init(name: String) { self.name = name }
deinit { print("\(name) deinit") }
}
class Apartment {
var unit: String
weak var tenant: Person? // Weak (breaks cycle)
init(unit: String) { self.unit = unit }
deinit { print("Apartment \(unit) deinit") }
}
var alice: Person? = Person(name: "Alice")
var apt: Apartment? = Apartment(unit: "4A")
alice!.apartment = apt
apt!.tenant = alice // Weak reference, no cycle
alice = nil // Alice deinit (apartment's tenant becomes nil)
apt = nil // Apartment deinit
// weak must be optional (becomes nil when deallocated)Références unowned
Les références unowned, comme weak, ne maintiennent pas l'objet en vie. Contrairement à weak, unowned est non-optionnel et ne devient pas nil. Accéder à une référence unowned après désallocation crash. Utilisez unowned lorsque vous pouvez garantir que l'objet référencé survit à la référence (par ex., une carte de crédit ne peut pas exister sans son client). unowned a moins de surcoût que weak. Choisissez : weak pour les durées de vie incertaines, unowned pour les durées de vie garanties.
class Customer {
let name: String
var card: CreditCard? // Strong
init(name: String) { self.name = name }
deinit { print("\(name) deinit") }
}
class CreditCard {
let number: UInt
unowned let customer: Customer // Non-optional, no cycle
init(number: UInt, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card \(number) deinit") }
}
var john: Customer? = Customer(name: "John")
john!.card = CreditCard(number: 1234, customer: john!)
john = nil
// Customer deinit, then CreditCard deinit
// unowned: non-optional, assumed to always have a value
// CRASH if accessed after deallocation
// Use when the referenced object has same or longer lifetimeFermetures & listes de capture
Les fermetures capturent les références fortement par défaut, causant des cycles lorsque la fermeture est stockée sur self. Les listes de capture ([weak self] ou [unowned self]) cassent le cycle. weak self requiert une gestion optionnelle (guard let self = self). Capturez des valeurs spécifiques (let name = self.name) pour éviter de capturer self entièrement. Les fermetures passées aux fonctions (non stockées) ne causent pas de cycles. Utilisez toujours des listes de capture pour les fermetures stockées qui référencent self.
class ViewController {
var name = "MyVC"
var onComplete: (() -> Void)?
// BAD: Strong reference cycle
func setupBad() {
onComplete = {
print("\(self.name) completed") // Captures self strongly
}
}
// GOOD: Capture list breaks cycle
func setupGood() {
onComplete = { [weak self] in
guard let self = self else { return }
print("\(self.name) completed")
}
}
// unowned for guaranteed lifetime
func setupUnowned() {
onComplete = { [unowned self] in
print("\(self.name) completed")
}
}
// Capture specific values
func setupCapture() {
let name = self.name // Capture value, not self
onComplete = {
print("\(name) completed")
}
}
deinit { print("VC deinit") }
}Détection & correction des fuites
Sources de fuites courantes : délégués stockés strong (utilisez weak), Timer retenant la cible (invalidez dans deinit), observateurs NotificationCenter (retirez dans deinit). Le Memory Graph Debugger de Xcode visualise les graphes d'objets et surligne les fuites. Lancez avec MallocStackLogging pour obtenir les traces d'allocation. Testez deinit en mettant les références à nil et en vérifiant que deinit s'exécute. Instruments (outil Leaks) détecte les fuites à l'exécution. Associez toujours le setup au teardown.
// Xcode memory graph debugger: visualize reference cycles
// Product > Debug > View Memory Graph Hierarchy
// Common leak patterns:
// 1. Delegate stored as strong
class View {
weak var delegate: ViewDelegate? // MUST be weak
}
// 2. Timer retains target
class TimerHolder {
var timer: Timer?
func start() {
// BAD: timer retains self via #selector
timer = Timer.scheduledTimer(timeInterval: 1,
target: self, selector: #selector(fire),
userInfo: nil, repeats: true)
}
@objc func fire() {}
deinit { timer?.invalidate() } // MUST invalidate
}
// 3. NotificationCenter observer
class Observer {
init() {
NotificationCenter.default.addObserver(
self, selector: #selector(handle),
name: .someNotification, object: nil)
}
@objc func handle() {}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
// Use Xcode's Debug Memory Graph to find leaksActors & async/await
Bases d'async/await
async/await (Swift 5.5+) fait que le code asynchrone ressemble à du code synchrone. async marque les fonctions qui peuvent suspendre ; await marque les points de suspension. Les Tasks font le pont entre contexte sync et async. async let démarre une tâche concurrence ; await collecte le résultat. Utilisez async let pour l'exécution parallèle, await régulier pour séquentiel. Les erreurs se propagent avec throws/try. Le compilateur applique await aux points de suspension. async/await remplace les completion handlers et Combine pour de nombreux cas d'usage.
// Async function
func fetchUser(id: Int) async throws -> User {
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
// Calling async functions
func loadProfile() async {
do {
let user = try await fetchUser(id: 1)
print("Loaded: \(user.name)")
} catch {
print("Error: \(error)")
}
}
// Task: bridge async to sync context
Task {
await loadProfile()
}
// Concurrent execution
func loadMultiple() async {
async let user1 = fetchUser(id: 1)
async let user2 = fetchUser(id: 2)
async let user3 = fetchUser(id: 3)
let users = try await [user1, user2, user3]
print("Loaded \(users.count) users")
}
// Sequenced vs concurrent
let sequential = try await fetchUser(id: 1) // Waits
let concurrent = try await fetchUser(id: 2) // Then waits againActors
Les actors (Swift 5.5+) sont des types référence avec accès sérialisé, empêchant les data races. Une seule méthode s'exécute sur un actor à la fois — pas de verrouillage manuel nécessaire. Les méthodes d'actor doivent être appelées avec await (elles peuvent suspendre). Les propriétés sont isolées et ne peuvent pas être accédées directement de l'extérieur. Les actors sont le défaut sûr pour l'état muable partagé dans le code concurrent. Utilisez les actors au lieu des classes avec verrous pour la plupart de l'état concurrent.
// Actor: reference type with serialized access
actor BankAccount {
private var balance: Decimal
init(balance: Decimal) {
self.balance = balance
}
func deposit(_ amount: Decimal) {
balance += amount
}
func withdraw(_ amount: Decimal) -> Bool {
guard balance >= amount else { return false }
balance -= amount
return true
}
func getBalance() -> Decimal { balance }
}
// Usage (must be async)
let account = BankAccount(balance: 100)
Task {
await account.deposit(50)
let balance = await account.getBalance()
print("Balance: \(balance)") // 150
}
// Actors prevent data races automatically
// Only one method runs at a time per actor instance
// No need for locks or queuesIsolation d'actor
L'isolation d'actor protège l'état. Les membres nonisolated peuvent être accédés sans await (pour les valeurs pures ou les méthodes qui ne touchent pas l'état isolé). @MainActor est un actor global pour l'isolation du thread principal (mises à jour UI). @globalActor crée des actors globaux personnalisés. Traverser les frontières d'actor requiert await. Le compilateur vérifie l'isolation à la compilation, empêchant les data races. Utilisez @MainActor pour les ViewModels et le code UI ; les actors personnalisés pour l'isolation spécifique au domaine.
actor Counter {
private var count = 0
// Isolated method (default)
func increment() { count += 1 }
// Nonisolated: can be called without await
nonisolated var description: String { "Counter" }
// nonisolated function
nonisolated func id() -> String { "counter-\(ObjectIdentifier(self))" }
}
// Global actor: @MainActor runs on main thread
@MainActor
class ViewModel: ObservableObject {
@Published var data: [Item] = []
func loadData() async {
// Always runs on main thread
data = try await fetchItems()
}
}
// Mark specific functions
class Service {
@MainActor
func updateUI(_ result: Result) {
// UI updates on main thread
}
}
// globalActor lets you apply to types, functions, properties
@globalActor
actor MyActor {
static let shared = MyActor()
}Concurrence structurée
Les groupes de tâches permettent le calcul parallèle dynamique. addTask génère des tâches enfants ; for await group collecte les résultats au fur et à mesure qu'ils se terminent. Les groupes de tâches appliquent la concurrence structurée : tous les enfants se terminent avant que le parent continue. L'annulation se propage du parent aux enfants automatiquement. Vérifiez Task.isCancelled dans les tâches longues. withThrowingTaskGroup propage les erreurs, annulant les siblings en cas de throw. Utilisez les groupes de tâches pour les patterns fan-out/fan-in.
// TaskGroup: dynamic parallel computation
func fetchAllUsers(ids: [Int]) async throws -> [User] {
try await withThrowingTaskGroup(of: User.self) { group in
for id in ids {
group.addTask { try await fetchUser(id: id) }
}
var users: [User] = []
for try await user in group {
users.append(user)
}
return users
}
}
// Task tree: parent-child relationship
func process() async {
await withTaskGroup(of: Void.self) { group in
for i in 1...5 {
group.addTask {
await processItem(i)
}
}
// All tasks complete before group returns
}
print("All done")
}
// Cancellation
func longRunning() async {
await withTaskGroup(of: Void.self) { group in
group.addTask {
for i in 1...100 {
if Task.isCancelled { return }
await doWork(i)
}
}
}
}
// Cancel from outside
let task = Task { await longRunning() }
task.cancel()AsyncSequence & flux
AsyncSequence est la version async de Sequence, permettant les boucles for await. AsyncStream fait le pont entre les APIs basés sur rappels ou délégués vers async/await. yield émet des valeurs, finish termine. onTermination nettoie les ressources. AsyncThrowingStream supporte les erreurs. Utilisez AsyncSequence pour les APIs paginées, les données en temps réel, ou tout flux de valeurs au fil du temps. La bibliothèque standard fournit .lines sur URL pour la lecture de fichier ligne par ligne. AsyncSequence s'intègre à l'annulation de tâche.
// AsyncSequence: iterate asynchronously
struct Counter: AsyncSequence {
struct AsyncIterator: AsyncIteratorProtocol {
var current: Int
let max: Int
mutating func next() async -> Int? {
guard current <= max else { return nil }
defer { current += 1 }
try? await Task.sleep(nanoseconds: 100_000_000)
return current
}
}
let start: Int
let max: Int
func makeAsyncIterator() -> AsyncIterator {
AsyncIterator(current: start, max: max)
}
}
// Usage
for await num in Counter(start: 1, max: 5) {
print(num) // 1 2 3 4 5 (with delays)
}
// AsyncStream: bridge callbacks to async
func urlLines(_ url: URL) -> AsyncThrowingStream<String, Error> {
AsyncThrowingStream { continuation in
let task = Task {
do {
for try await line in url.lines {
continuation.yield(line)
}
continuation.finish()
} catch {
continuation.finish(throwing: error)
}
}
continuation.onTermination = { _ in task.cancel() }
}
}Tests
Bases de XCTest
XCTest est le framework de test standard. XCTestCase regroupe les tests associés. setUp/tearDown s'exécutent avant/après chaque test. Assertions : XCTAssertEqual, XCTAssertNil, XCTAssertThrowsError, XCTAssertTrue. @testable import accède aux symboles internes. Les tests s'exécutent sur le thread principal par défaut. Utilisez XCTAssertNotNil pour les optionnels. Lancez les tests avec Cmd+U. Xcode affiche les résultats des tests en ligne. Écrivez les tests à côté du code pour des retours rapides.
import XCTest
@testable import MyApp
class UserTests: XCTestCase {
var user: User!
override func setUp() {
super.setUp()
user = User(name: "Alice", age: 30)
}
override func tearDown() {
user = nil
super.tearDown()
}
func testName() {
XCTAssertEqual(user.name, "Alice")
}
func testAgeAfterBirthday() {
user.haveBirthday()
XCTAssertEqual(user.age, 31)
}
func testValidation() {
XCTAssertThrowsError(try User(name: "", age: -1)) { error in
guard let error = error as? UserError else {
XCTFail("Wrong error type")
return
}
XCTAssertEqual(error, .invalidName)
}
}
func testOptional() {
XCTAssertNotNil(user.email)
XCTAssertNil(user.address)
}
}
// Run: Cmd+U in XcodeTests asynchrones
Swift 5.5+ prend en charge les méthodes de test asynchrones directement (async throws func test...). Utilisez try await dans les tests. Pour le code plus ancien, utilisez XCTestExpectation : créez une expectation, fulfill() dans le callback, wait(for:timeout:). Définissez des délais raisonnables pour détecter les blocages. Pour tester les séquences asynchrones, utilisez les boucles for await. Simulez les appels réseau avec URLProtocol ou l'injection de dépendances. Testez les chemins de succès et d'échec. Les tests asynchrones s'exécutent en parallèle par défaut—utilisez le trait .serialized si l'ordre compte.
class APITests: XCTestCase {
func testFetchUser() async throws {
let user = try await fetchUser(id: 1)
XCTAssertEqual(user.name, "Alice")
}
func testFetchFailure() async {
do {
_ = try await fetchUser(id: -1)
XCTFail("Should have thrown")
} catch {
// Expected
}
}
// Old style with expectations (still works)
func testAsyncWithExpectation() {
let expectation = XCTestExpectation(description: "Fetch completes")
fetchUserAsync { result in
switch result {
case .success(let user):
XCTAssertEqual(user.name, "Alice")
case .failure:
XCTFail("Should succeed")
}
expectation.fulfill()
}
wait(for: [expectation], timeout: 5)
}
}Simulation et bouchons
L'injection de dépendances permet les tests : définissez des protocoles pour les dépendances, injectez des mocks dans les tests. MockNetworkService renvoie des résultats prédéterminés. Testez les cas de succès et d'échec en définissant mock.fetchUserResult. Ce pattern découple les tests du réseau, les rendant rapides et déterministes. Pour les mocks complexes, utilisez des bibliothèques comme Cuckoo ou Mockingbird. Testez l'API publique de vos types, pas les détails d'implémentation. Les mocks doivent être simples et ciblés.
// Protocol for dependency injection
protocol NetworkService {
func fetchUser(id: Int) async throws -> User
}
// Production implementation
class APIService: NetworkService {
func fetchUser(id: Int) async throws -> User {
// Real network call
}
}
// Mock for testing
class MockNetworkService: NetworkService {
var fetchUserResult: Result<User, Error>?
func fetchUser(id: Int) async throws -> User {
switch fetchUserResult {
case .success(let user): return user
case .failure(let error): throw error
case .none: throw URLError(.badURL)
}
}
}
class ViewModelTests: XCTestCase {
func testLoadUser() async throws {
let mock = MockNetworkService()
mock.fetchUserResult = .success(User(name: "Alice", age: 30))
let vm = ViewModel(service: mock)
await vm.load()
XCTAssertEqual(vm.userName, "Alice")
}
}Tests d'interface utilisateur
Les tests d'interface utilisateur automatisent les interactions utilisateur via les identifiants d'accessibilité. XCUIApplication lance l'application. Trouvez les éléments par identifiant d'accessibilité (défini dans le code avec .accessibilityIdentifier). Actions : tap, typeText, swipeLeft/Right. Assertions : waitForExistence, exists, hittable. Les tests d'UI sont plus lents que les tests unitaires mais détectent les bugs d'intégration. Définissez les identifiants d'accessibilité dans SwiftUI avec .accessibilityIdentifier("Email"). Lancez les tests d'UI sur plusieurs appareils/simulateurs pour la couverture.
import XCTest
class AppUITests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
let app = XCUIApplication()
app.launch()
}
func testLoginFlow() {
let app = XCUIApplication()
let emailField = app.textFields["Email"]
let passwordField = app.secureTextFields["Password"]
let loginButton = app.buttons["Log In"]
emailField.tap()
emailField.typeText("[email protected]")
passwordField.tap()
passwordField.typeText("password123")
loginButton.tap()
// Verify navigation
XCTAssertTrue(app.staticTexts["Welcome, Alice!"].waitForExistence(timeout: 5))
}
func testSwipeAndTap() {
let cell = app.cells["Item 5"]
cell.swipeLeft()
app.buttons["Delete"].tap()
XCTAssertFalse(cell.exists)
}
}Performance et couverture de code
measure évalue les performances du code, en comparant avec les baselines. Définissez les baselines dans Xcode ; les tests échouent si la régression dépasse le seuil. measureMetrics suit des métriques spécifiques (mémoire de pointe, CPU). Activez la couverture de code dans le schéma pour voir les chemins de code non testés. Visez 80%+ de couverture sur la logique métier. Les tests de snapshot (swift-snapshot-testing) capturent l'UI pour la régression visuelle. Profilez avec Instruments (Time Profiler, Allocations) pour une analyse approfondie. Les tests de performance détectent les régressions tôt.
class PerformanceTests: XCTestCase {
func testSortingPerformance() {
let data = (1...10000).shuffled()
measure {
_ = data.sorted()
}
}
func testMemoryUsage() {
measureMetrics([XCTPerformanceMetric.peakMemory]) {
_ = (0..<10000).map { _ in [Int](repeating: 0, count: 100) }
}
}
// Baseline comparison
func testWithBaselines() {
let options = XCTMeasureOptions()
options.defaultOptions.measurementUnits = .seconds
measure(options) {
// Code to benchmark
}
}
}
// Code coverage: enable in Edit Scheme > Test > Options
// View coverage: Cmd+Shift+Y (Report Navigator) > Coverage
// Snapshot testing (third-party)
// func testAppearance() {
// assertSnapshot(of: view, as: .image)
// }Concurrence Swift
async/await
async/await (Swift 5.5+) simplifie le code asynchrone. async marque les fonctions qui peuvent se suspendre. await marque les points de suspension. Task crée un contexte asynchrone. Beaucoup plus propre que les completion handlers. Les erreurs se propagent avec try.
func fetchData() async throws -> String {
let (data, _) = try await URLSession.shared.data(from: url)
return String(data: data, encoding: .utf8) ?? ""
}
// Call from async context
Task {
let result = try await fetchData()
print(result)
}Acteurs
actor (Swift 5.5+) est un type référence avec exclusion mutuelle automatique. Une seule tâche accède à son état à la fois. Les méthodes sont implicitement async lorsqu'elles sont appelées de l'extérieur. Remplace les verrous et les dispatch queues pour l'état mutable partagé. Thread-safe par conception.
actor Counter {
private var count = 0
func increment() {
count += 1
}
func getValue() -> Int {
count
}
}
// Usage
Task {
let counter = Counter()
await counter.increment()
let value = await counter.getValue()
}Séquence asynchrone
AsyncSequence est l'équivalent asynchrone de Sequence. for await itère de manière asynchrone. Utile pour le streaming de données (réseau, lignes de fichier). Implémentez AsyncIteratorProtocol pour créer des séquences asynchrones personnalisées. Le compilateur gère la suspension et l'annulation.
for try await item in asyncSequence {
print(item)
}
// Custom AsyncSequence
struct Counter: AsyncSequence {
struct AsyncIterator: AsyncIteratorProtocol {
var current = 0
mutating func next() async -> Int? {
current += 1
return current <= 5 ? current : nil
}
}
func makeAsyncIterator() -> AsyncIterator { AsyncIterator() }
}Groupes de tâches
Les groupes de tâches exécutent plusieurs tâches en parallèle et collectent les résultats. addTask ajoute une tâche enfant. for await itère les résultats au fur et à mesure qu'ils se complètent. Toutes les tâches doivent se terminer avant que le groupe ne retourne. Concurrence structurée : l'annulation se propage aux enfants. Les résultats sont collectés en toute sécurité.
let results = await withTaskGroup(of: Int.self) { group in
for i in 1...5 {
group.addTask { i * i }
}
var sum = 0
for await result in group {
sum += result
}
return sum
}Continuation
withCheckedContinuation fait le pont entre les APIs à completion-handler et async/await. resume(returning:) reprend la fonction asynchrone. Doit être appelé exactement une fois. withCheckedThrowingContinuation prend en charge les erreurs. Utile pour adopter async/await avec les APIs existantes.
func fetchWithCompletion(_ completion: @escaping (String) -> Void) {
// Legacy completion handler API
completion("data")
}
// Wrap in async
func fetchAsync() async -> String {
await withCheckedContinuation { continuation in
fetchWithCompletion { result in
continuation.resume(returning: result)
}
}
}SwiftUI en profondeur
Modificateurs de vue
Les modificateurs enveloppent les vues pour changer l'apparence ou le comportement. L'ordre compte : les modificateurs ultérieurs enveloppent les précédents. .padding avant .background met le padding à l'intérieur du background. Les modificateurs retournent de nouvelles instances de View. Chaînez-les pour un style complexe.
Text("Hello")
.font(.title)
.foregroundColor(.blue)
.padding()
.background(Color.gray)
.cornerRadius(8)
.shadow(radius: 4)Listes
List affiche des lignes défilantes. ForEach génère des lignes à partir des données. onDelete active le balayage pour supprimer. Autres modificateurs : onMove, onInsert. Les données Identifiable (propriété id) sont requises pour ForEach. Style de List : .plain, .insetGrouped, .sidebar.
List {
ForEach(items) { item in
HStack {
Text(item.name)
Spacer()
Text("\(item.price)")
}
}
.onDelete { indexSet in
items.remove(atOffsets: indexSet)
}
}
.listStyle(.insetGrouped)Navigation
NavigationStack gère une pile de vues. NavigationLink pousse une destination. navigationTitle définit le titre. toolbar ajoute des boutons à la barre de navigation. Pour iOS 16+. Utilisez NavigationView pour les versions plus anciennes. Les sheets et alerts utilisent les modificateurs .sheet et .alert.
NavigationStack {
List(items) { item in
NavigationLink(item.name) {
DetailView(item: item)
}
}
.navigationTitle("Items")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Add") { addItem() }
}
}
}Gestion de l'état
@State pour l'état local de la vue (types valeur). @Binding pour l'état passé. @StateObject pour les objets observables possédés (créés une fois). @ObservedObject pour les objets observables externes. @EnvironmentObject pour l'état global de l'application. Les changements de @State déclenchent des mises à jour de la vue.
struct CounterView: View {
@State private var count = 0
@Binding var selected: Int
@StateObject var viewModel = ViewModel()
var body: some View {
Button("\(count)") { count += 1 }
}
}Animations
withAnimation anime les changements d'état. .spring, .easeInOut sont des courbes d'animation. .animation(value:) anime lorsque la valeur change. .scaleEffect, .opacity, .offset sont animables. Les animations implicites utilisent le modificateur .animation. Les explicites utilisent withAnimation.
struct AnimatedView: View {
@State private var scale: CGFloat = 1.0
var body: some View {
Button("Tap") {
withAnimation(.spring(response: 0.3)) {
scale = scale == 1 ? 1.5 : 1
}
}
.scaleEffect(scale)
.animation(.easeInOut(duration: 0.3), value: scale)
}
}Framework Combine
Éditeurs et abonnés
Combine est le framework réactif d'Apple. Les Publishers émettent des valeurs au fil du temps. Les Subscribers les reçoivent. sink crée un abonné. Just émet une valeur unique. PassthroughSubject est un publisher manuel. Les Cancellables doivent être conservés ou l'abonnement s'annule. Similaire à RxSwift.
import Combine
let publisher = Just(42)
let cancellable = publisher.sink { value in
print("Received: \(value)")
}
// Just emits one value then finishes
// PassthroughSubject: manual values
let subject = PassthroughSubject<Int, Never>()
subject.send(1)
subject.send(2)Opérateurs
Les opérateurs transforment la sortie du publisher. map transforme les valeurs. filter sélectionne les valeurs. debounce retarde jusqu'au calme. throttle limite le débit. combineLatest fusionne les flux. flatMap chaîne les publishers. Les opérateurs sont paresseux : rien ne se passe tant que sink ne s'abonne pas.
let publisher = (1...10).publisher
.map { $0 * 2 }
.filter { $0 > 10 }
.sink { print($0) } // 12, 14, 16, 18, 20
// Debounce
let debounced = subject
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.sink { print($0) }@Published
@Published expose une propriété comme un publisher Combine. Le préfixe $ accède au publisher. Les changements émettent de nouvelles valeurs. Fonctionne avec ObservableObject pour SwiftUI. Le publisher émet la valeur actuelle lors de l'abonnement. Utile pour les mises à jour réactives de l'UI sans notifications manuelles.
class ViewModel: ObservableObject {
@Published var count = 0
}
let vm = ViewModel()
let cancellable = vm.$count.sink { newCount in
print("Count changed: \(newCount)")
}
vm.count = 1 // Prints "Count changed: 1"
vm.count = 2 // Prints "Count changed: 2"Future
Future enveloppe une API à completion-handler en tant que publisher. promise(.success) émet une valeur. promise(.failure) émet une erreur. Future émet exactement une fois. Utile pour faire le pont entre les APIs à callback et Combine. La closure s'exécute avidement sauf si enveloppée avec .delay ou deferred.
func fetchUser() -> Future<User, Error> {
Future { promise in
URLSession.shared.dataTask(with: url) { data, _, error in
if let error = error { promise(.failure(error)) }
else { promise(.success(parse(data!))) }
}.resume()
}
}
// Usage
fetchUser().sink(receiveCompletion: { _ in }, receiveValue: { user in print(user) })Gestion des erreurs
Fail émet immédiatement une erreur. catch remplace un publisher échoué par un autre. retry se réabonne en cas d'échec. assertNoFailure plante en cas d'erreur (pour le débogage). L'événement de completion signale qu'il n'y a plus de valeurs. Les erreurs se propagent en aval sauf si elles sont attrapées.
let publisher = Fail<Int, MyError>(error: .notFound)
.catch { error in
Just(0) // Fallback value
}
.sink { print($0) } // 0
// retry on failure
let retried = requestPublisher
.retry(3)
.sink(receiveCompletion: { _ in }, receiveValue: { _ in })Gestion de la mémoire
Bases de l'ARC
ARC (Automatic Reference Counting) suit les références. Quand le compte atteint zéro, deinit s'exécute et la mémoire se libère. Les références fortes incrémentent le compte. Les cycles de références empêchent la désallocation. L'ARC est déterministe (contrairement au GC). deinit s'exécute de manière synchrone lorsque la dernière référence disparaît.
class Person {
let name: String
init(name: String) { self.name = name }
deinit { print("\(name) is being deinitialized") }
}
var p1: Person? = Person(name: "Alice")
p1 = nil // Prints "Alice is being deinitialized"Références faibles
Les références weak n'incrémentent pas le compte de retenue. Elles se mettent automatiquement à nil lorsque l'objet se désalloue. Doivent être optional var. Utilisez-les pour les delegates et les observers afin de briser les cycles. Les références weak sont zeroing : sûres à accéder après que l'objet a disparu. La correction la plus courante pour les cycles de retenue.
class View {
weak var delegate: Delegate?
}
class Delegate {
var view: View? // strong
}
// weak: does not keep object alive, auto-set to nil
// Use for delegate patterns to avoid cyclesRéférences unowned
Les références unowned n'incrémentent pas le compte de retenue. Contrairement à weak, elles sont non-optionnelles et non zeroing. L'accès après désallocation plante. Utilisez-les lorsque l'objet référencé survit ou meurt avec le référençant. Plus rapide que weak (pas de vérification nil). Courant dans les closures capturant self lorsque self survit à la closure.
class Customer {
var card: CreditCard?
}
class CreditCard {
unowned let customer: Customer
init(customer: Customer) { self.customer = customer }
}
// unowned: does not keep alive, but not optional
// CRASHES if accessed after dealloc
// Use when the other object has same or shorter lifetimeCycles de retenue
Les cycles de retenue se produisent lorsque les objets se référencent fortement mutuellement. Aucun ne se désalloue, fuyant la mémoire. Corrigez avec weak ou unowned. Courant dans les delegates, observers et closures. Les closures capturent self fortement par défaut. [weak self] ou [unowned self] brise le cycle. Utilisez Memory Graph Debugger pour trouver les cycles.
// BAD: retain cycle
class Node {
var next: Node?
var prev: Node? // strong -> cycle
}
// GOOD: break with weak
class Node {
var next: Node?
weak var prev: Node?
}
// Closures: use [weak self]
{ [weak self] in self?.update() }Closures et capture
Les closures capturent les variables par référence. Les closures stockées (escaping) créent des cycles de retenue si elles capturent self. [weak self] rend la capture optionnelle et faible. @escaping marque les closures qui survivent à l'appel de fonction. Les closures non-escaping (par défaut) ne peuvent pas causer de cycles. Le compilateur avertit des cycles potentiels.
class Timer {
var handler: (() -> Void)?
func start() {
handler = { [weak self] in
self?.tick() // weak to avoid cycle
}
}
func tick() { /* ... */ }
}
// Escaping closure: stored for later
// @escaping marks escaping closuresGestion des erreurs en profondeur
Erreurs personnalisées
Les erreurs personnalisées se conforment à Error. LocalizedError fournit errorDescription. Les valeurs associées transportent le contexte. Les enums sont la manière idiomatique de définir les erreurs. Chaque cas représente un échec distinct. L'exhaustivité du switch garantit que toutes les erreurs sont gérées. Conformez-vous à CustomStringConvertible pour la sortie de débogage.
enum APIError: Error, LocalizedError {
case invalidURL
case unauthorized
case serverError(Int)
case decodingFailed(Error)
var errorDescription: String? {
switch self {
case .invalidURL: return "Invalid URL"
case .unauthorized: return "Unauthorized"
case .serverError(let code): return "Server error: \(code)"
case .decodingFailed(let err): return "Decode failed: \(err)"
}
}
}Type Result
Result est un enum typé succès/échec. Utile pour les opérations synchrones qui peuvent échouer. map transforme la valeur de succès. flatMap chaîne les opérations. get() lance pour convertir en try/catch. Result est préféré aux fonctions lançant des exceptions pour les résultats stockés ou passés. Combine la gestion des erreurs avec la sémantique de valeur.
func fetchUser(id: Int) -> Result<User, APIError> {
guard id > 0 else { return .failure(.invalidURL) }
return .success(User(name: "Alice"))
}
// Usage
switch fetchUser(id: 1) {
case .success(let user): print(user)
case .failure(let error): print(error)
}
// map and flatMap
let result = fetchUser(id: 1).map { $0.name }Rethrows
rethrows propage les erreurs d'un paramètre de closure. Si la closure ne lance pas d'erreur, la fonction n'en lance pas non plus. Cela évite de forcer try sur les appelants qui passent des closures non lançantes. Utilisé par map, filter et autres fonctions d'ordre supérieur. La fonction elle-même ne peut pas lancer d'erreur indépendamment de la closure.
func withLock<T>(_ lock: NSLock, _ body: () throws -> T) rethrows -> T {
lock.lock()
defer { lock.unlock() }
return try body()
}
// Only throws if body throws
let x = withLock(lock) { 5 } // no try needed
let y = try withLock(lock) { try throwingFunc() }Propagation des erreurs
try propage les erreurs à l'appelant. try? convertit les erreurs en nil (retourne un Optional). try! plante en cas d'erreur (à utiliser lorsque certain). Les erreurs remontent la pile d'appels jusqu'à être attrapées. Les fonctions qui lancent des erreurs doivent être marquées throws. Le compilateur impose les marqueurs try, empêchant les erreurs non gérées. defer s'exécute quelles que soient les erreurs lancées.
func process() throws {
let data = try fetchData() // may throw
let user = try parse(data) // may throw
save(user) // does not throw
}
// try?: converts to optional, nil on error
let user = try? fetchUser()
// try!: asserts no error, crashes if thrown
let user = try! fetchUser()Nettoyage avec defer
defer planifie le nettoyage pour s'exécuter à la sortie de la portée. S'exécute quelle que soit la manière dont la portée se termine (return, throw, ou fall-through). Plusieurs defer s'exécutent dans l'ordre LIFO. Utile pour fermer les fichiers, libérer les verrous, libérer les ressources. defer ne peut pas lancer d'erreur ou break/continue. La closure capture les variables par référence.
func processFile() throws {
let file = open("file.txt")
defer { close(file) } // runs on scope exit
let data = try read(file)
// ... process ...
// close() runs here even if read throws
}
// Multiple defers run in reverse order (LIFO)
defer { print("1") }
defer { print("2") } // prints 2 then 1Snippets Swift associés
Copy-paste ready code for common tasks.
Optionals
Use optional binding, nil-coalescing, chaining, and guard for safe unwrapping.
Closures
Define closure expressions, capture state, and pass escaping callbacks.
Protocols
Define contracts, conform with structs, and add default behavior via extensions.
Generics
Write type-parameterized functions and types with protocol constraints.
Structs and Classes
Compare value-type structs with reference-type classes and inheritance.
Error Handling
Throw and catch typed errors with do-catch, try?, try!, and rethrows.
Concurrency (async/await)
Run async functions, parallelize with async let, and fan out with task groups.
String Manipulation
Trim, split, join, replace, and index strings using Swift's Unicode API.
Was this helpful?