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
@Sendablez 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:
| Deklarace | Swift 6.1 | Swift 6.2 (s příznaky) |
|---|---|---|
nonisolated func sync() | Vlákno volajícího | Vlákno volajícího (beze změny) |
nonisolated func async() async | Globální executor (pozadí) | Vlákno volajícího |
@MainActor func async() async | Hlavní vlákno | Hlavní vlákno (beze změny) |
@concurrent nonisolated func() async | Neexistovalo | Globá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
nonisolatedfunkcí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.