Bezpečnost dat ve Swift 6: Kompletní průvodce Sendable, aktory a Mutex

Kompletní průvodce bezpečností dat ve Swift 6 — od Sendable protokolu a aktorů po Mutex ze Synchronization frameworku a novinky Swift 6.2 Approachable Concurrency.

Proč záleží na bezpečnosti dat při souběžném zpracování

Datové závody (data races) patří mezi nejzákeřnější chyby v programování. A upřímně? Jsou to ty nejhorší. Představte si situaci: dvě vlákna současně čtou a zapisují do stejné proměnné. Výsledek? Nedeterministické chování, padání aplikace, poškozená data — a hlavně chyby, které se prakticky nedají reprodukovat při ladění. I zkušení vývojáři nad nimi stráví hodiny (a nemalé množství kávy).

Swift 6 na to přišel s docela revoluční odpovědí: kompletní kontrola souběžnosti v době kompilace. Místo toho, aby se datové závody odhalovaly až za běhu (pokud vůbec), kompilátor je teď dokáže odhalit ještě předtím, než aplikaci spustíte.

A s příchodem Swift 6.2 se celý systém ještě výrazně zjednodušil díky konceptu Approachable Concurrency.

Tak pojďme na to. V tomto průvodci si projdeme všechno, co potřebujete vědět — od základních konceptů jako Sendable a aktory, přes praktické použití Mutex z nového frameworku Synchronization, až po migrační strategii a nejnovější změny ve Swift 6.2. Připravte si Xcode, budeme psát hodně kódu.

Co jsou datové závody a proč je Swift řeší

Datový závod nastane, když dvě nebo více vláken přistupuje ke stejným datům současně a alespoň jedno z nich provádí zápis. Tady je klasický příklad:

// NEBEZPEČNÉ: Datový závod!
var counter = 0

DispatchQueue.concurrentPerform(iterations: 1000) { _ in
    counter += 1 // Více vláken čte a zapisuje současně
}

print(counter) // Výsledek: Kdo ví? 987? 993? 1000? Pokaždé jinak.

Tento kód vypadá nevinně, ale je fundamentálně rozbitý. Operace counter += 1 není atomická — skládá se z přečtení hodnoty, zvýšení o jedničku a zápisu zpět. Když dvě vlákna přečtou stejnou hodnotu současně, jedno přepsání se prostě ztratí.

Před Swift 6 jste měli k dispozici nástroje jako Thread Sanitizer (TSan), který datové závody odhaloval za běhu. Jenže problém je v tom, že TSan zachytí závod jen tehdy, když k němu skutečně dojde během testování. A datové závody jsou ze své podstaty nedeterministické — v testech se nemusí projevit vůbec, ale v produkci vám spadnou na 0,1 % uživatelů. Přesně ten typ bugu, ze kterého se vám dělá špatně.

Swift 6 tento problém řeší zásadně jinak: přesouvá detekci datových závodů do doby kompilace. Kompilátor analyzuje tok dat a izolační hranice a odmítne zkompilovat kód, který by mohl vést k datovému závodu. Je to jako mít neúnavného code reviewera, který nikdy nic nepřehlédne.

Sendable: Základ bezpečného přenosu dat

Protokol Sendable je základním kamenem celého systému bezpečnosti dat ve Swift 6. Říká kompilátoru: „Tento typ je bezpečný pro přenos mezi izolačními doménami (vlákny, aktory)."

Které typy jsou automaticky Sendable

Hodnotové typy jsou přirozeně bezpečné pro souběžný přístup, protože se při předání kopírují. Swift proto automaticky označuje jako Sendable tyto typy:

  • Základní typyInt, String, Bool, Double a další
  • Struktury — pokud všechny jejich vlastnosti jsou také Sendable
  • Výčty — pokud všechny asociované hodnoty jsou Sendable
  • Tuple — pokud všechny prvky jsou Sendable
  • Aktory — ty jsou navrženy přímo pro bezpečný souběžný přístup
// Automaticky Sendable - všechny vlastnosti jsou Sendable
struct UserProfile {
    let name: String
    let age: Int
    let email: String
}

// Automaticky Sendable - asociované hodnoty jsou Sendable
enum PaymentStatus {
    case pending
    case completed(amount: Double)
    case failed(error: String)
}

Kdy Sendable nefunguje automaticky

Třídy (reference types) nejsou automaticky Sendable, protože se předávají odkazem — více vláken tak může přistupovat ke stejné instanci. Kompilátor vyžaduje explicitní konformanci a vynucuje přísná pravidla:

// Třída může být Sendable pouze pokud:
// 1. Je final
// 2. Všechny uložené vlastnosti jsou let a Sendable
final class Configuration: Sendable {
    let apiUrl: String
    let timeout: Int
    let maxRetries: Int

    init(apiUrl: String, timeout: Int, maxRetries: Int) {
        self.apiUrl = apiUrl
        self.timeout = timeout
        self.maxRetries = maxRetries
    }
}

// CHYBA: var vlastnost nemůže být v Sendable třídě
final class MutableConfig: Sendable {
    var apiUrl: String // Kompilátor odmítne
    init(apiUrl: String) { self.apiUrl = apiUrl }
}

@unchecked Sendable — únikový ventil

V praxi narazíte na situace, kdy víte, že váš typ je bezpečný, ale kompilátor to prostě nedokáže ověřit. Pro tyto případy existuje @unchecked Sendable:

// @unchecked Sendable - říkáme kompilátoru "věř mi"
// POZOR: Odpovědnost za bezpečnost je plně na nás!
final class ThreadSafeCache: @unchecked Sendable {
    private var cache: [String: Data] = [:]
    private let lock = NSLock()

    func get(_ key: String) -> Data? {
        lock.lock()
        defer { lock.unlock() }
        return cache[key]
    }

    func set(_ key: String, value: Data) {
        lock.lock()
        defer { lock.unlock() }
        cache[key] = value
    }
}

Tohle funguje, ale je to dost křehké. Pokud uděláte chybu v implementaci zámku, kompilátor vám vůbec nepomůže. Naštěstí Swift 6 přinesl mnohem lepší řešení — Mutex. Ale k tomu se dostaneme za chvilku.

Aktory: Izolace stavu na úrovni jazyka

Aktory jsou referenční typy (jako třídy), které automaticky serializují přístup ke svému vnitřnímu stavu. To je ta klíčová věc. Kdykoli chcete přistoupit k vlastnosti nebo metodě aktoru zvenčí, musíte použít await — kompilátor vás donutí počkat, až je přístup bezpečný.

actor BankAccount {
    let accountNumber: String
    private(set) var balance: Double

    init(accountNumber: String, initialBalance: Double) {
        self.accountNumber = accountNumber
        self.balance = initialBalance
    }

    func deposit(_ amount: Double) {
        balance += amount
    }

    func withdraw(_ amount: Double) -> Bool {
        guard balance >= amount else { return false }
        balance -= amount
        return true
    }

    func transfer(to other: BankAccount, amount: Double) async -> Bool {
        guard balance >= amount else { return false }
        balance -= amount
        await other.deposit(amount) // await při přístupu k jinému aktoru
        return true
    }
}

Použití aktoru zvenčí vždy vyžaduje await:

let account = BankAccount(accountNumber: "CZ123", initialBalance: 10000)

// Z asynchronního kontextu
Task {
    await account.deposit(5000)
    let balance = await account.balance
    print("Zůstatek: \(balance) Kč")

    let success = await account.withdraw(3000)
    if success {
        print("Výběr úspěšný")
    }
}

Izolace aktoru a nonisolated

Uvnitř aktoru jsou všechny metody a vlastnosti izolované — mají exkluzivní přístup ke stavu. Někdy ale chcete metodu, která nepotřebuje přístup k měnitelnému stavu. Pro to slouží klíčové slovo nonisolated:

actor UserService {
    private var users: [String: User] = [:]

    // Izolovaná metoda - přistupuje k users
    func addUser(_ user: User) {
        users[user.id] = user
    }

    func getUser(id: String) -> User? {
        return users[id]
    }

    // nonisolated - nepotřebuje přístup ke stavu aktoru
    // Lze volat bez await
    nonisolated func formatUserName(_ name: String) -> String {
        return name.trimmingCharacters(in: .whitespaces).capitalized
    }

    // nonisolated computed property
    nonisolated var serviceName: String {
        "UserService v2.0"
    }
}

MainActor: Bezpečné aktualizace UI

@MainActor je globální aktor, který garantuje spuštění kódu na hlavním vlákně. Pokud děláte cokoli s UI ve SwiftUI nebo UIKit, je tohle váš nejlepší kamarád:

@MainActor
class ProfileViewModel: ObservableObject {
    @Published var userName: String = ""
    @Published var isLoading: Bool = false
    @Published var errorMessage: String?

    private let userService = UserService()

    func loadProfile(userId: String) async {
        isLoading = true
        errorMessage = nil

        do {
            // Síťový požadavek běží na pozadí
            let profile = try await APIClient.shared.fetchProfile(userId: userId)
            // Zpátky na MainActor - aktualizace UI je bezpečná
            userName = profile.name
        } catch {
            errorMessage = "Nepodařilo se načíst profil: \(error.localizedDescription)"
        }

        isLoading = false
    }
}

S anotací @MainActor na celé třídě máte jistotu, že všechny vlastnosti a metody se vykonají na hlavním vlákně. Pokud voláte kód označený @MainActor z jiného kontextu, musíte použít await.

Mutex: Moderní synchronizace ze Synchronization frameworku

Tak, a tady to začíná být opravdu zajímavé. Swift 6 a iOS 18 představily zcela nový framework Synchronization, který obsahuje dvě nízkoúrovňové synchronizační primitiva: Mutex a Atomic. Pro většinu z nás je nejdůležitější právě Mutex.

Mutex je zámek navržený přímo pro Swift Concurrency. Na rozdíl od NSLock nebo os_unfair_lock je nepodmíněně Sendable — to znamená, že kompilátor rozumí jeho sémantice a dokáže ověřit bezpečnost v době kompilace. To je obrovský rozdíl.

Základní použití Mutex

import Synchronization

final class SafeCounter: Sendable {
    let counter = Mutex(0) // Mutex chrání hodnotu Int

    func increment() {
        counter.withLock { value in
            value += 1
        }
    }

    func decrement() {
        counter.withLock { value in
            value -= 1
        }
    }

    func currentValue() -> Int {
        counter.withLock { value in
            value
        }
    }
}

// Použití - plně bezpečné pro souběžný přístup
let counter = SafeCounter()

await withTaskGroup(of: Void.self) { group in
    for _ in 0..<1000 {
        group.addTask {
            counter.increment()
        }
    }
}

print(counter.currentValue()) // Vždy 1000 ✓

Všimněte si klíčového rozdílu oproti dřívějšímu přístupu s @unchecked Sendable:

  • Třída je Sendable (ne @unchecked Sendable) — kompilátor to skutečně ověřuje
  • Žádný manuální lock()/unlock()withLock automaticky zamkne a odemkne
  • Není možné zapomenout odemknout zámek — closure se o to postará za vás
  • Vlastnost counter je letMutex sám je neměnný, mění se jen chráněná hodnota uvnitř

Mutex s komplexními typy

Mutex dokáže chránit libovolnou hodnotu, nejen primitivní typy. Tady je příklad se složitějším stavem aplikace:

import Synchronization

struct AppState {
    var currentUser: String?
    var isAuthenticated: Bool = false
    var preferences: [String: String] = [:]
    var notificationCount: Int = 0
}

final class StateManager: Sendable {
    let state = Mutex(AppState())

    func login(userName: String) {
        state.withLock { state in
            state.currentUser = userName
            state.isAuthenticated = true
        }
    }

    func logout() {
        state.withLock { state in
            state.currentUser = nil
            state.isAuthenticated = false
            state.preferences = [:]
        }
    }

    func updatePreference(key: String, value: String) {
        state.withLock { state in
            state.preferences[key] = value
        }
    }

    func incrementNotifications() {
        state.withLock { state in
            state.notificationCount += 1
        }
    }

    func snapshot() -> AppState {
        state.withLock { $0 }
    }
}

Kdy použít Mutex vs. aktor

Tohle je otázka, na kterou se mě ptají dost často. Tady je jednoduché rozhodovací pravidlo:

  • Mutex použijte, když potřebujete synchronní přístup (uvnitř computed properties, synchronních metod), ultra nízký overhead, nebo integraci s legacy synchronním kódem
  • Aktor použijte, když pracujete s asynchronním kódem, potřebujete izolovat složitější logiku, nebo chcete naplno využít systém izolace Swift Concurrency
// Mutex: Skvělé pro synchronní přístup
final class Settings: Sendable {
    private let _fontSize = Mutex(14.0)

    // Computed property - synchronní, žádné await
    var fontSize: Double {
        get { _fontSize.withLock { $0 } }
    }

    func setFontSize(_ size: Double) {
        _fontSize.withLock { $0 = size }
    }
}

// Aktor: Skvělé pro asynchronní operace
actor NetworkCache {
    private var cache: [URL: Data] = [:]

    func fetch(url: URL) async throws -> Data {
        if let cached = cache[url] {
            return cached
        }
        let (data, _) = try await URLSession.shared.data(from: url)
        cache[url] = data
        return data
    }
}

Region-Based Isolation: Chytřejší kompilátor

Jedním z největších zlepšení ve Swift 6 oproti Swift 5.10 je izolace založená na regionech (SE-0414). Pokud jste zkoušeli strict concurrency ve Swift 5.10, určitě si pamatujete tu záplavu falešných varování. Kompilátor odmítal kód, který byl ve skutečnosti naprosto bezpečný, protože nedokázal sledovat, jak hodnoty proudí programem.

SE-0414 zavádí koncept izolačních regionů. Kompilátor teď rozdělí hodnoty do regionů a sleduje, zda hodnoty z jednoho regionu mohou uniknout do jiného. Pokud dokáže prokázat, že ne-Sendable hodnota je předána bezpečně (žádné jiné vlákno k ní nemá přístup), přenos povolí:

// Ve Swift 5.10 by tohle generovalo varování
// Ve Swift 6 kompilátor ví, že je to bezpečné
func processData() async {
    // Vytvoříme ne-Sendable hodnotu
    let formatter = DateFormatter()
    formatter.dateFormat = "dd.MM.yyyy"

    // Předáme ji do Task - kompilátor ví, že po tomto
    // bodě formatter už nepoužíváme
    await Task {
        let result = formatter.string(from: Date())
        print(result)
    }.value
}

Klíčové slovo sending

SE-0430 přidává klíčové slovo sending, které explicitně říká, že parametr bude přenesen mezi izolačními regiony:

// sending parametr - hodnota přechází do jiného regionu
func processOnBackground(sending data: sending [String]) async {
    await Task.detached {
        for item in data {
            print("Zpracovávám: \(item)")
        }
    }.value
}

// Použití
let items = ["jablko", "hruška", "banán"]
await processOnBackground(sending: items)
// Po zavolání funkce už items nemůžeme použít
// v tomto izolačním regionu

Globální proměnné a Swift 6

Jednou z nejčastějších bolístek při migraci na Swift 6 jsou globální proměnné. SE-0412 vyžaduje, aby každá globální proměnná byla bezpečná pro souběžný přístup. A věřte mi, v každém větším projektu jich najdete víc, než byste čekali.

// CHYBA ve Swift 6: Globální var není bezpečná
var globalConfig = AppConfiguration()

// Řešení 1: Udělat ji konstantní
let globalConfig = AppConfiguration()

// Řešení 2: Izolovat na MainActor
@MainActor
var globalConfig = AppConfiguration()

// Řešení 3: Použít nonisolated(unsafe) - poslední možnost
nonisolated(unsafe) var globalConfig = AppConfiguration()

// Řešení 4: Mutex pro skutečně sdílený stav
let globalConfig = Mutex(AppConfiguration())

Osobně doporučuji přehodnotit, zda globální proměnnou vůbec potřebujete. Většinou je lepší ji přesunout do dependency injection kontejneru nebo do aktoru. Globální stav je skoro vždycky code smell.

Praktická migrace na Swift 6: Krok za krokem

Migrace na striktní souběžnost nemusí být bolestivá, pokud ji provedete postupně. Tady je postup, který se mi osvědčil:

Krok 1: Zapněte varování v Swift 5 režimu

Nejprve zapněte kontrolu souběžnosti jako varování, aniž byste přepínali na Swift 6 jazykový režim:

// V Package.swift
.target(
    name: "MyApp",
    swiftSettings: [
        .enableUpcomingFeature("StrictConcurrency")
    ]
)

// Nebo v Xcode:
// Build Settings → Swift Compiler - Upcoming Features
// → Strict Concurrency Checking = Complete

Tímto získáte všechna varování, ale projekt se stále zkompiluje. Můžete řešit problémy postupně, bez tlaku.

Krok 2: Začněte od listových modulů

Pokud máte projekt rozdělen do modulů, začněte od těch, které nemají žádné závislosti na jiných vašich modulech. Ty jsou nejjednodušší na migraci, protože nemusíte řešit interakci s nemigrovaným kódem.

Krok 3: Označte typy jako Sendable

Projděte své typy a rozhodněte, které potřebují být Sendable:

// Modely dat - typicky jednoduché hodnotové typy
struct Article: Sendable, Codable {
    let id: UUID
    let title: String
    let content: String
    let publishedAt: Date
}

// Konfigurační objekty - neměnné třídy
final class APIConfiguration: Sendable {
    let baseURL: URL
    let apiKey: String
    let timeout: TimeInterval

    init(baseURL: URL, apiKey: String, timeout: TimeInterval = 30) {
        self.baseURL = baseURL
        self.apiKey = apiKey
        self.timeout = timeout
    }
}

// Služby s měnitelným stavem - použijte Mutex
import Synchronization

final class AnalyticsService: Sendable {
    private let events = Mutex<[AnalyticsEvent]>([])

    func track(_ event: AnalyticsEvent) {
        events.withLock { $0.append(event) }
    }

    func flush() -> [AnalyticsEvent] {
        events.withLock { events in
            let copy = events
            events.removeAll()
            return copy
        }
    }
}

Krok 4: Převeďte @unchecked Sendable na Mutex

Pokud máte existující kód s @unchecked Sendable, teď je ideální čas ho nahradit. Podívejte se na ten rozdíl:

// PŘED: Křehké a nekontrolované
final class ImageCache: @unchecked Sendable {
    private var cache: [String: UIImage] = [:]
    private let lock = NSLock()

    func image(for key: String) -> UIImage? {
        lock.lock()
        defer { lock.unlock() }
        return cache[key]
    }

    func store(_ image: UIImage, for key: String) {
        lock.lock()
        defer { lock.unlock() }
        cache[key] = image
    }
}

// PO: Bezpečné a ověřené kompilátorem
import Synchronization

final class ImageCache: Sendable {
    private let cache = Mutex<[String: UIImage]>([:])

    func image(for key: String) -> UIImage? {
        cache.withLock { $0[key] }
    }

    func store(_ image: UIImage, for key: String) {
        cache.withLock { $0[key] = image }
    }
}

Mnohem čistší, že?

Krok 5: Přepněte na Swift 6 jazykový režim

Když jsou všechna varování vyřešena, přepněte na Swift 6:

// V Package.swift
// swift-tools-version: 6.0
import PackageDescription

let package = Package(
    name: "MyApp",
    platforms: [.iOS(.v18)],
    targets: [
        .target(name: "MyApp")
    ]
)

Swift 6.2: Approachable Concurrency

Pokud vám přechod na Swift 6 přišel náročný (a já vám to vůbec nemám za zlé), máme dobrou zprávu. Swift 6.2 přináší koncept Approachable Concurrency, který zásadně zjednodušuje práci se souběžností pro většinu vývojářů.

Výchozí izolace na MainActor

Tohle je asi největší změna. Nové projekty v Xcode 26 budou mít zapnutou výchozí izolaci na MainActor automaticky. Celý váš kód je ve výchozím stavu na hlavním vlákně, jako by byl jednovláknový:

// S DefaultIsolation = MainActor (Swift 6.2)
// Tento kód je automaticky @MainActor
class ProfileViewController: UIViewController {
    var userName: String = "" // Bezpečné - jsme na MainActor

    func updateUI() {
        // Toto je na hlavním vlákně automaticky
        nameLabel.text = userName
    }
}

Pro zapnutí v existujícím projektu přidejte:

// V Package.swift
.target(
    name: "MyApp",
    swiftSettings: [
        .enableExperimentalFeature("DefaultIsolation", value: "MainActor")
    ]
)

nonisolated(nonsending): Konzistentní chování

Swift 6.2 opravuje jednu nekonzistenci, která trápila vývojáře od začátku. Dříve se nonisolated async funkce chovaly dost neočekávaně — mohly „utéct" z aktoru volajícího a spustit se na libovolném vlákně. Nové klíčové slovo nonisolated(nonsending) zajišťuje, že funkce zůstane na vlákně volajícího:

// nonisolated(nonsending) - zůstane na vlákně volajícího
nonisolated(nonsending) func formatDate(_ date: Date) async -> String {
    // Běží na stejném vlákně jako volající
    let formatter = DateFormatter()
    formatter.dateFormat = "dd.MM.yyyy HH:mm"
    return formatter.string(from: date)
}

// S příznakem NonIsolatedNonSendingByDefault je toto výchozí chování
// pro všechny nonisolated funkce

@concurrent: Explicitní offloading

Když chcete explicitně spustit práci na pozadí, použijte nový atribut @concurrent:

// @concurrent - explicitně běží mimo aktor volajícího
@concurrent
func processImage(_ data: Data) async -> UIImage? {
    // Toto běží na background vlákně
    // Vhodné pro výpočetně náročné operace
    guard let image = UIImage(data: data) else { return nil }

    let renderer = UIGraphicsImageRenderer(size: CGSize(width: 200, height: 200))
    return renderer.image { context in
        image.draw(in: CGRect(origin: .zero, size: CGSize(width: 200, height: 200)))
    }
}

// Volání z MainActor kontextu
@MainActor
func handleImageUpload(_ data: Data) async {
    // processImage se automaticky přesune na pozadí
    if let thumbnail = await processImage(data) {
        imageView.image = thumbnail // Zpět na MainActor
    }
}

Tento model je mnohem intuitivnější: vše je ve výchozím stavu na hlavním vlákně, a pouze to, co explicitně označíte @concurrent, se přesune na pozadí. Žádné nechtěné přepínání vláken, žádná překvapení. Konečně to dává smysl.

Časté chyby a jak je řešit

Při práci se Swift 6 souběžností narazíte na několik typických problémů. Pojďme si projít ty nejčastější — a hlavně jak se jich zbavit.

Chyba: Closure předaná do funkce se Sendable parametrem

// CHYBA: Capture of 'self' with non-sendable type
class DataProcessor {
    var results: [String] = []

    func process() {
        Task {
            // self není Sendable - kompilátor hlásí chybu
            results.append("zpracováno")
        }
    }
}

// ŘEŠENÍ: Použijte aktor
actor DataProcessor {
    var results: [String] = []

    func process() {
        Task {
            // Uvnitř aktoru je to bezpečné
            await self.appendResult("zpracováno")
        }
    }

    func appendResult(_ result: String) {
        results.append(result)
    }
}

Chyba: Předání delegáta mezi izolačními doménami

// PROBLÉM: Delegate pattern s ne-Sendable protokolem
protocol DataDelegate: AnyObject {
    func didReceiveData(_ data: Data)
}

// ŘEŠENÍ: Označte protokol jako MainActor
@MainActor
protocol DataDelegate: AnyObject {
    func didReceiveData(_ data: Data)
}

// Nebo použijte Sendable closure místo delegáta
actor DataLoader {
    func load(url: URL, completion: @Sendable (Data) -> Void) async throws {
        let (data, _) = try await URLSession.shared.data(from: url)
        completion(data)
    }
}

Chyba: Práce s ne-Sendable typy z frameworků

// PROBLÉM: DateFormatter není Sendable
actor FormattingService {
    func format(date: Date) -> String {
        let formatter = DateFormatter() // Vytvořte lokálně
        formatter.dateFormat = "dd.MM.yyyy"
        return formatter.string(from: date)
    }
}

// ALTERNATIVA: Použijte Sendable alternativy
// Date.FormatStyle je Sendable
func formatDate(_ date: Date) -> String {
    date.formatted(.dateTime.day().month().year())
}

Testování souběžného kódu

Testování souběžného kódu vyžaduje trochu jiný přístup než běžné unit testy. Swift Testing framework naštěstí nabízí elegantní podporu pro async testy:

import Testing
import Synchronization

@Test
func testSafeCounterConcurrency() async {
    let counter = SafeCounter()

    await withTaskGroup(of: Void.self) { group in
        for _ in 0..<1000 {
            group.addTask { counter.increment() }
        }
        for _ in 0..<500 {
            group.addTask { counter.decrement() }
        }
    }

    #expect(counter.currentValue() == 500)
}

@Test
func testBankAccountTransfer() async {
    let alice = BankAccount(accountNumber: "A1", initialBalance: 1000)
    let bob = BankAccount(accountNumber: "B1", initialBalance: 500)

    let success = await alice.transfer(to: bob, amount: 300)

    #expect(success)
    #expect(await alice.balance == 700)
    #expect(await bob.balance == 800)
}

@Test
@MainActor
func testViewModelLoading() async {
    let viewModel = ProfileViewModel()

    await viewModel.loadProfile(userId: "test-user")

    #expect(!viewModel.isLoading)
    #expect(viewModel.errorMessage == nil)
}

Doporučené vzory a praktiky

Na závěr si shrňme doporučené vzory pro bezpečnou souběžnost ve Swift 6. Jsou to pravidla, která se mi v praxi opravdu osvědčila.

1. Preferujte hodnotové typy

Struktury a výčty jsou automaticky Sendable. Kdykoli je to možné, modelujte svá data jako hodnotové typy:

// Preferujte
struct NetworkRequest: Sendable {
    let url: URL
    let method: HTTPMethod
    let headers: [String: String]
}

// Místo
class NetworkRequest { ... }

2. Používejte Mutex pro synchronní přístup

Kdykoliv potřebujete synchronní přístup ke sdílenému stavu, sáhněte po Mutex. Vyhnete se @unchecked Sendable a získáte plné ověření od kompilátoru.

3. Aktory pro asynchronní izolaci

Pro služby a manažery, které pracují s asynchronním kódem, jsou aktory tou nejpřirozenější volbou. Využijte jejich izolaci stavu a automatickou serializaci přístupu.

4. Minimalizujte kritické sekce

// ŠPATNĚ: Příliš dlouhá kritická sekce
let data = Mutex<[Item]>([])
data.withLock { items in
    let processed = items.map { expensiveOperation($0) } // Blokuje ostatní vlákna!
    items = processed
}

// SPRÁVNĚ: Minimální kritická sekce
let snapshot = data.withLock { Array($0) } // Rychlé zkopírování
let processed = snapshot.map { expensiveOperation($0) } // Mimo zámek
data.withLock { $0 = processed } // Rychlý zápis

5. Postupná migrace

Nesnažte se migrovat celý projekt najednou. Zapněte varování, opravte je modul po modulu, a teprve když je vše čisté, přepněte na Swift 6. S příchodem Swift 6.2 a Approachable Concurrency je migrace jednodušší než kdy dřív.

Závěr

Swift 6 přinesl zásadní posun v tom, jak přistupujeme k souběžnému programování. Datové závody, které dříve představovaly noční můru každého vývojáře, teď zachytí kompilátor ještě před spuštěním aplikace. Máme k dispozici mocné nástroje:

  • Sendable pro bezpečný přenos dat mezi izolačními doménami
  • Aktory pro automatickou izolaci a serializaci stavu
  • Mutex pro efektivní synchronní přístup ke sdíleným datům
  • Region-based isolation pro chytřejší analýzu toku dat
  • Approachable Concurrency ve Swift 6.2 pro jednodušší mentální model

Klíčové poselství je jasné: bezpečnost souběžnosti ve Swift není jen o dodržování pravidel kompilátoru. Je to o psaní kódu, kterému můžete důvěřovat. Kódu, který nehavaruje v produkci kvůli zákeřnému datovému závodu, který se projevil jen u zlomku uživatelů.

A s každou novou verzí Swift se tento cíl stává dosažitelnějším.

Začněte ještě dnes — zapněte si StrictConcurrency varování ve svém projektu a podívejte se, co kompilátor najde. Možná budete překvapeni. Já rozhodně byl.

O Autorovi Editorial Team

Our team of expert writers and editors.