Approachable Concurrency ve Swift 6.2: Kompletní průvodce novým modelem souběžnosti

Swift 6.2 přináší Approachable Concurrency — váš kód je ve výchozím stavu jednovláknový na @MainActor. Naučte se používat @concurrent, postupnou migraci a nové příznaky kompilátoru s funkčními příklady.

Proč Swift 6.2 mění pravidla hry pro souběžnost

Ruku na srdce — kdo z vás si užil přechod na Swift 6? Hodiny strávené opravováním chybových hlášení o Sendable, izolaci aktorů a data race safety. Nebyla to zrovna procházka růžovým sadem. A Swift tým to moc dobře ví.

Přesně proto ve Swift 6.2 představili koncept nazvaný Approachable Concurrency. Česky bychom to mohli přeložit jako „přístupná souběžnost", ale upřímně — v komunitě se ustálil anglický termín a překládat ho je trochu zbytečné.

Základní myšlenka je překvapivě jednoduchá: Swift by po vás neměl chtít, abyste rozuměli celému modelu souběžnosti jen proto, že zavoláte jednu async funkci. Místo toho aplikuje princip progressive disclosure — odhaluje složitost postupně, jen když ji skutečně potřebujete.

A co to znamená v praxi? Váš kód je nyní ve výchozím stavu jednovláknový. Souběžnost přidáváte explicitně a vědomě. Žádné překvapení od kompilátoru, který na vás zničehonic začne křičet.

Co je Approachable Concurrency — nastavení, ne jen filozofie

Approachable Concurrency není jen marketingový termín. Je to skutečné nastavení v Xcode 26 (SWIFT_APPROACHABLE_CONCURRENCY), které po aktivaci zapne pět příznakových vlajek kompilátoru najednou:

  • DisableOutwardActorInference (SE-0401) — zabraňuje automatickému šíření izolace aktora z property wrapperů na celý typ
  • GlobalActorIsolatedTypesUsability (SE-0434) — usnadňuje práci s typy izolovanými na @MainActor
  • InferIsolatedConformances (SE-0470) — umožňuje izolované konformance protokolů
  • InferSendableFromCaptures (SE-0418) — automaticky odvozuje @Sendable z kontextu zachycení
  • NonisolatedNonsendingByDefault (SE-0461) — nejzásadnější změna, o které si povíme podrobněji

Nové projekty v Xcode 26 mají toto nastavení zapnuté automaticky. U existujících projektů je potřeba ho aktivovat ručně — ale nebojte, k tomu se dostaneme.

Default Actor Isolation: @MainActor jako výchozí stav

Společně s Approachable Concurrency přichází další klíčové nastavení — Default Actor Isolation (SE-0466). To říká kompilátoru jednoduchou věc: „Pokud výslovně neuvedu jinak, všechno běží na @MainActor."

Proč je to tak důležité?

Protože většina kódu v běžné iOS aplikaci stejně běží na hlavním vlákně. Práce s UI, view modely, navigace — to všechno. Jenže dřív jste museli každou třídu, strukturu a metodu ručně anotovat @MainActor, jinak vám kompilátor vynadal. Upřímně, bylo to únavné a frustrující.

Teď stačí jedno nastavení a celý váš target nebo balíček je implicitně na hlavním vlákně.

Aktivace v Xcode

V Build Settings vašeho projektu vyhledejte Default Actor Isolation a nastavte na MainActor. U nových projektů je to už výchozí hodnota.

Aktivace v Swift Package Manager

// swift-tools-version: 6.2

let package = Package(
    name: "MojeAplikace",
    targets: [
        .target(
            name: "MojeAplikace",
            swiftSettings: [
                .defaultIsolation(MainActor.self)
            ]
        )
    ]
)

Po aktivaci se všechny deklarace chovají, jako by měly anotaci @MainActor — bez toho, abyste ji museli psát ručně. Podívejme se na příklad:

// S defaultIsolation(MainActor.self) není potřeba @MainActor anotace
@Observable
class UserViewModel {
    var userName: String = ""
    var isLoading: Bool = false

    func loadUser() async {
        // Toto automaticky běží na MainActor
        MainActor.assertIsolated() // projde bez chyby
        isLoading = true
        // ...
    }
}

Jak se odhlásit z výchozí izolace

Pokud konkrétní metoda nebo vlastnost nemá běžet na hlavním vlákně, jednoduše ji označte jako nonisolated:

class DataProcessor {
    // Implicitně @MainActor díky defaultIsolation

    nonisolated func processData(_ data: Data) -> ProcessedResult {
        // Běží na vlákně volajícího, ne nutně na MainActor
        return ProcessedResult(data: data)
    }
}

Důležitý detail (a tohle lidi občas překvapí): nelze odhlásit celý typ najednou — jen jednotlivé členy. Pokud potřebujete celý typ bez izolace, musíte buď explicitně přiřadit jinému aktoru, nebo označit všechny členy jako nonisolated.

NonisolatedNonsendingByDefault: Zásadní změna chování async funkcí

Tak, tohle je pravděpodobně nejdůležitější změna ve Swift 6.2 a zaslouží si podrobné vysvětlení. Mění totiž chování, na které jsme byli zvyklí od Swift 5.5.

Problém ve Swift 6.1 a starších verzích

Představte si tento kód:

class NetworkClient {
    func fetchPhotos() async throws -> [Photo] {
        let (data, _) = try await URLSession.shared.data(from: photosURL)
        return try JSONDecoder().decode([Photo].self, from: data)
    }
}

@MainActor
class PhotoViewModel {
    let client = NetworkClient()

    func loadPhotos() async {
        let photos = try? await client.fetchPhotos()
        // Aktualizace UI
    }
}

Ve Swift 6.1 (s přísným režimem souběžnosti) byste dostali chybu:

„sending main actor-isolated 'self.client' to nonisolated instance method risks causing data races"

Proč? Protože nonisolated async funkce se automaticky přesunuly na globální executor (vlákno na pozadí). Předání neodesílatelného (non-Sendable) objektu client z hlavního vlákna na vlákno na pozadí představovalo potenciální datový závod. Dává to smysl, ale v praxi to bylo šíleně otravné.

Nové chování ve Swift 6.2

Se zapnutým NonisolatedNonsendingByDefault se nonisolated async funkce chovají stejně jako synchronní nonisolated funkce — běží na vlákně volajícího. Pokud je zavoláte z @MainActor, poběží na hlavním vlákně. Žádné automatické přeskakování na pozadí.

Srovnejme si to v tabulce:

DeklaraceSwift 6.1Swift 6.2 (s příznaky)
nonisolated func sync()Vlákno volajícíhoVlákno volajícího (beze změny)
nonisolated func async() asyncGlobální executor (pozadí)Vlákno volajícího
@MainActor func async() asyncHlavní vláknoHlavní vlákno (beze změny)
@concurrent nonisolated func() asyncNeexistovaloGlobální executor (pozadí)

Výsledek? Předchozí kód s NetworkClient se nyní zkompiluje bez chyb. Metoda fetchPhotos() zdědí izolaci volajícího (@MainActor) a žádné nebezpečné předávání přes hranice souběžnosti nenastane. Konečně.

Atribut @concurrent: Vědomý vstup do souběžnosti

Takže pokud je teď výchozí chování „zůstaň na vlákně volajícího", jak přesunete práci na pozadí, když to skutečně potřebujete? Odpovědí je nový atribut @concurrent.

class ImageProcessor {
    // Tato metoda explicitně říká: "Chci běžet na pozadí"
    @concurrent
    nonisolated func processImage(_ data: Data) async throws -> UIImage {
        // Těžká práce s obrazem — nechceme blokovat hlavní vlákno
        let cgImage = try decodeImage(data)
        let filtered = applyFilters(to: cgImage)
        return UIImage(cgImage: filtered)
    }
}

@MainActor
class ImageViewModel {
    let processor = ImageProcessor()

    func handleImage(_ data: Data) async {
        // processImage poběží na pozadí díky @concurrent
        if let image = try? await processor.processImage(data) {
            // Zpátky na MainActor po await
            self.displayedImage = image
        }
    }
}

Několik pravidel pro @concurrent:

  • Lze použít pouze na nonisolated funkcích
  • Na aktoru musí být metoda navíc označena jako nonisolated
  • Obnovuje původní chování ze Swift 6.1 — funkce se přesune na globální executor

Filozofie je tu jednoznačná: souběžnost musí být vědomá volba, ne výchozí stav. Použijte @concurrent tam, kde opravdu potřebujete paralelní zpracování — typicky pro výpočetně náročné operace, práci s velkými daty nebo zpracování obrázků.

Kdy @concurrent použít a kdy ne

class DataService {
    // ✅ Použijte @concurrent — těžký výpočet, nechcete blokovat UI
    @concurrent
    nonisolated func compressVideo(_ url: URL) async throws -> Data {
        // Výpočetně náročná operace
        return try await VideoCompressor.compress(url)
    }

    // ❌ Nepoužívejte @concurrent — jednoduchá operace, není důvod
    // opouštět vlákno volajícího
    func formatUserName(_ user: User) -> String {
        return "\(user.firstName) \(user.lastName)"
    }

    // ✅ Použijte @concurrent — dlouhé stahování nechcete na hlavním vlákně
    @concurrent
    nonisolated func downloadLargeFile(_ url: URL) async throws -> Data {
        let (data, _) = try await URLSession.shared.data(from: url)
        return data
    }
}

Izolované konformance protokolů (SE-0470)

Dalším příjemným vylepšením jsou izolované konformance. Tohle je jedna z těch věcí, které vás dřív dokázaly pořádně potrápit. Představte si tento scénář:

// Swift 6.1 — problém
@MainActor
class UserModel: Equatable {
    var name: String

    // Kompilátor protestuje — == musí být nonisolated,
    // ale přistupuje k @MainActor vlastnostem
    static func == (lhs: UserModel, rhs: UserModel) -> Bool {
        return lhs.name == rhs.name
    }
}

Řešením bývaly ošklivé hacky s nonisolated nebo (ještě hůř) nonisolated(unsafe). Ve Swift 6.2 s InferIsolatedConformances kompilátor automaticky rozpozná, že konformance k Equatable je izolovaná na @MainActor — a povolí ji, pokud se používá pouze z tohoto aktora.

// Swift 6.2 — funguje čistě
@MainActor
class UserModel: Equatable {
    var name: String

    init(name: String) { self.name = name }

    static func == (lhs: UserModel, rhs: UserModel) -> Bool {
        return lhs.name == rhs.name // žádný problém
    }
}

Kompilátor zajistí, že tuto konformanci nepředáte mimo izolační doménu hlavního aktora. Pokud byste se pokusili porovnat dva UserModel z pozadí vlákna, dostali byste chybu při kompilaci. Fér řešení.

Automatické odvozování Sendable (SE-0418)

Tahle změna je menší, ale rozhodně ji oceníte. Ve Swift 6.1 jste museli ručně anotovat reference na metody:

// Swift 6.1 — potřebovali jste explicitní anotaci
struct SafeCounter: Sendable {
    var count: Int = 0

    func increment() -> Int {
        return count + 1
    }
}

// Při předání metody jako closure:
let counter = SafeCounter()
// Dříve mohlo vyžadovat ruční @Sendable anotaci
Task {
    let result = counter.increment // kompilátor si teď odvodí @Sendable sám
}

Ve Swift 6.2 kompilátor sám odvodí @Sendable pro reference na metody a key paths, pokud je to bezpečné. Méně boilerplate kódu, méně frustrace. Prostě to funguje.

Jak aktivovat Approachable Concurrency krok za krokem

Nový projekt v Xcode 26

Nemusíte dělat vůbec nic. Vážně. Xcode 26 vytváří nové projekty s těmito výchozími hodnotami:

  • Approachable Concurrency: Ano
  • Default Actor Isolation: MainActor

Váš kód je od první chvíle jednovláknový a na hlavním aktoru. Souběžnost přidáváte, až když ji potřebujete.

Existující projekt — doporučený postup

Tady je to trochu složitější, ale nic, co by se nedalo zvládnout.

Krok 1: Upgradujte na Xcode 26 a Swift 6.2.

Krok 2: Aktivujte příznaky postupně — rozhodně ne všechny najednou. Začněte s těmi méně rizikovými:

// Package.swift — postupná aktivace
.target(
    name: "MojeAplikace",
    swiftSettings: [
        // Fáze 1: Bezpečné změny
        .enableUpcomingFeature("DisableOutwardActorInference"),
        .enableUpcomingFeature("GlobalActorIsolatedTypesUsability"),
        .enableUpcomingFeature("InferSendableFromCaptures"),

        // Fáze 2: Přidat po ověření fáze 1
        .enableUpcomingFeature("InferIsolatedConformances"),

        // Fáze 3: Nejzásadnější změna — přidat jako poslední
        .enableUpcomingFeature("NonisolatedNonsendingByDefault"),

        // Volitelné: Výchozí izolace na MainActor
        .defaultIsolation(MainActor.self)
    ]
)

Krok 3: Po zapnutí NonisolatedNonsendingByDefault projděte všechny nonisolated async metody. Ty, které dříve běžely na pozadí a provádějí náročnou práci, označte @concurrent. Tohle je krok, který nesmíte vynechat.

Krok 4: Využijte migrační nástroje Swiftu — umí identifikovat nekompatibilní vzory a automaticky aplikovat opravy.

Aktivace v Xcode Build Settings

Pro stávající projekt hledejte v Build Settings tato nastavení:

  • Approachable Concurrency (SWIFT_APPROACHABLE_CONCURRENCY) → nastavte na Yes
  • Default Actor Isolation → nastavte na MainActor

Jednotlivé příznaky můžete vyhledávat i samostatně — například „nonisolated" vás dovede k přepínači NonisolatedNonsendingByDefault.

Kombinace nastavení a jejich vliv na chování

Chování vašeho kódu se dramaticky liší podle kombinace dvou nastavení. Pojďme si to rozebrat.

Approachable Concurrency: Vypnuto + Default Isolation: nonisolated

Tradiční chování ze Swift 6.0/6.1. Async funkce přeskakují na pozadí. Nejvíce chybových hlášení kompilátoru. Maximální souběžnost — ale taky maximální bolest hlavy.

Approachable Concurrency: Zapnuto + Default Isolation: nonisolated

Vyvážený režim. Async funkce zůstávají na vlákně volajícího (díky SE-0461). Méně chyb kompilátoru, ale typy nejsou implicitně na @MainActor. Vhodné pokud máte hodně kódu, který záměrně neběží na hlavním vlákně.

Approachable Concurrency: Zapnuto + Default Isolation: MainActor

Doporučená konfigurace pro většinu iOS aplikací. Všechno běží na hlavním vlákně, pokud výslovně neuvedete jinak. Minimální třecí plochy s kompilátorem. Souběžnost přidáváte vědomě pomocí @concurrent. Osobně bych začal tady.

Praktický příklad: View model před a po migraci

Teorie je fajn, ale ukažme si realistický view model a jak se změní při přechodu na Swift 6.2.

Swift 6.1 — plný anotací

@MainActor
@Observable
class ArticleListViewModel {
    var articles: [Article] = []
    var isLoading = false
    var errorMessage: String?

    private let apiClient: APIClient

    init(apiClient: APIClient) {
        self.apiClient = apiClient
    }

    func loadArticles() async {
        isLoading = true
        errorMessage = nil

        do {
            // Potenciální problém — apiClient není Sendable
            articles = try await apiClient.fetchArticles()
        } catch {
            errorMessage = error.localizedDescription
        }

        isLoading = false
    }
}

Swift 6.2 — s Approachable Concurrency a defaultIsolation(MainActor)

// Žádný @MainActor — je implicitní díky defaultIsolation
@Observable
class ArticleListViewModel {
    var articles: [Article] = []
    var isLoading = false
    var errorMessage: String?

    private let apiClient: APIClient

    init(apiClient: APIClient) {
        self.apiClient = apiClient
    }

    func loadArticles() async {
        isLoading = true
        errorMessage = nil

        do {
            // Žádný problém — fetchArticles() zdědí izolaci MainActor
            articles = try await apiClient.fetchArticles()
        } catch {
            errorMessage = error.localizedDescription
        }

        isLoading = false
    }
}

// Pokud APIClient provádí síťová volání,
// těžké metody můžete explicitně přesunout na pozadí:
class APIClient {
    @concurrent
    nonisolated func fetchLargeDataset() async throws -> [DataItem] {
        // Běží na pozadí díky @concurrent
        let (data, _) = try await URLSession.shared.data(from: datasetURL)
        return try JSONDecoder().decode([DataItem].self, from: data)
    }

    // Tato metoda zdědí izolaci volajícího (MainActor)
    func fetchArticles() async throws -> [Article] {
        let (data, _) = try await URLSession.shared.data(from: articlesURL)
        return try JSONDecoder().decode([Article].self, from: data)
    }
}

Všimněte si, jak se kód zjednodušil — méně anotací, méně šumu, a přitom stále bezpečný z hlediska souběžnosti. To je přesně ta „přístupnost", o kterou Swift týmu šlo.

Na co si dát pozor při migraci

Migrace na Approachable Concurrency není bez úskalí. Tady jsou problémy, na které jsem narazil (nebo na ně narazíte vy).

1. Výkon — blokování hlavního vlákna

Tohle je největší riziko. Metody, které dříve automaticky běžely na pozadí, nyní běží na hlavním vlákně. Pokud máte nonisolated async metodu, která provádí těžký výpočet, a zapomenete přidat @concurrent, zablokujete UI. A uživatelé si toho všimnou.

// ⚠️ POZOR — po migraci toto běží na hlavním vlákně!
class JSONParser {
    func parseLargeJSON(_ data: Data) async throws -> [Item] {
        // Toto může trvat sekundy pro velká data
        return try JSONDecoder().decode([Item].self, from: data)
    }
}

// ✅ SPRÁVNĚ — přidejte @concurrent pro těžkou práci
class JSONParser {
    @concurrent
    nonisolated func parseLargeJSON(_ data: Data) async throws -> [Item] {
        return try JSONDecoder().decode([Item].self, from: data)
    }
}

2. Migrace SPM balíčků

Nastavení defaultIsolation v Xcode projektu se nepropaguje do Swift balíčků. Každý target musí mít vlastní nastavení v Package.swift. Na tohle se snadno zapomene.

3. Postupná aktivace je klíčová

Neaktivujte všechny příznaky najednou — věřte mi, není to dobrý nápad. Začněte s nejméně invazivními (DisableOutwardActorInference, InferSendableFromCaptures) a postupně přidávejte další. U NonisolatedNonsendingByDefault si nejprve projděte všechny nonisolated async metody a rozhodněte, které potřebují @concurrent.

4. Vlastní aktoři nejsou ovlivněni

Nastavení defaultIsolation(MainActor.self) neovlivňuje typy, které mají vlastní izolaci aktora. Vaše vlastní aktory a vlastní globální aktory fungují stejně jako dřív — žádné překvapení.

Vztah k existujícím vzorům souběžnosti

Pokud jste četli náš předchozí článek o bezpečnosti dat ve Swift 6 (Sendable, aktory a Mutex), možná se ptáte: je tohle všechno stále relevantní?

Odpověď je ano.

Approachable Concurrency nemění základní principy — Sendable, aktory a Mutex stále existují a jsou důležité. Co se mění, je to, kdy na ně narazíte. S novým modelem na ně narazíte teprve ve chvíli, kdy vědomě zavedete souběžnost — tedy když použijete @concurrent, vytvoříte Task.detached nebo zavedete vlastního aktora.

Pro většinu aplikací to znamená: napíšete svůj kód, všechno funguje na hlavním vlákně, a teprve když identifikujete úzké hrdlo výkonu, přesunete konkrétní operaci na pozadí. V ten moment vyřešíte otázky kolem Sendable a izolace. Progresivní přístup, přesně jak bylo zamýšleno.

Často kladené otázky

Musím migrovat na Swift 6.2 hned?

Ne, nemusíte. Swift 6.0 a 6.1 jsou stále plně podporovány. Nicméně Apple vyžaduje od dubna 2026 používání Xcode 26 a iOS 26 SDK pro odesílání do App Store, takže upgrade na Swift 6.2 je prakticky nevyhnutelný. Approachable Concurrency je ale opt-in nastavení — nemusíte ho aktivovat okamžitě.

Je XCTest framework v Xcode 26 deprecated kvůli Swift 6.2?

Ne, XCTest není deprecated a nemá to se Swift 6.2 přímou souvislost. Apple představil nový Swift Testing framework, ale XCTest je nadále plně podporován. Klidně je v jednom projektu kombinujte.

Může zapnutí Approachable Concurrency rozbít mou existující aplikaci?

Ano, ale ne na úrovni kompilace — spíše na úrovni runtime chování. Hlavním rizikem je NonisolatedNonsendingByDefault, kdy metody, které dříve běžely na pozadí, nyní běží na hlavním vlákně. To může způsobit zpomalení UI. Proto doporučujeme postupnou migraci a důkladný audit všech async metod.

Jak se liší @concurrent od Task.detached?

@concurrent je atribut na deklaraci funkce — říká, že tato funkce vždy poběží na globálním executoru, bez ohledu na to, odkud je zavolána. Task.detached vytváří nový nestrukturovaný task, který nemá vazbu na rodičovský kontext. Oba dosahují přesunu práce na pozadí, ale @concurrent je deklarativní a čistější pro definice API. Task.detached se hodí spíš pro ad-hoc operace na pozadí.

Funguje Approachable Concurrency i pro watchOS a tvOS?

Ano. Approachable Concurrency je vlastnost kompilátoru Swift 6.2, ne specifické platformy. Funguje na všech Apple platformách — iOS, macOS, watchOS, tvOS i visionOS. Nastavení defaultIsolation a všechny příznaky fungují identicky bez ohledu na cílovou platformu.

O Autorovi Editorial Team

Our team of expert writers and editors.