Approachable Concurrency er Apples paraplybetegnelse for en række ændringer, der skal gøre concurrency mere tilgængelig — uden at gå på kompromis med data race-sikkerheden. Hovedfilosofien kan koges ned til tre punkter:
- Progressive disclosure — du betaler kun for den concurrency, du faktisk bruger.
- Single-threaded by default — typisk app-kode kører på main actor uden eksplicit annotation.
- Eksplicit opt-in til parallelisme — vil du have baggrundsarbejde, beder du om det med
@concurrent.
Resultatet? De fleste udviklere kan skrive helt almindelig app-kode uden at tænke på Sendable, isolation domains eller global actors — indtil de selv vælger at gøre det for ydeevnens skyld.
Forudsætninger og opsætning
For at følge med har du brug for:
- Xcode 16.4 eller nyere (anbefales: Xcode 17 fra foråret 2026).
- Swift 6.2-toolchainen, som er bundtet med Xcode 16.4+.
- Et projekt sat op med Swift Language Mode = 6 i Build Settings.
Tjek din Swift-version i terminalen:
swift --version
# Apple Swift version 6.2 (swiftlang-6.2.0.xx.x clang-1700.x.x.x)
Default actor isolation: Single-threaded by default
Den vigtigste ændring i Swift 6.2 er, efter min mening, SE-0466: Control default actor isolation. Den gør det muligt for et helt modul at standardisere isolation til main actor, så du slipper for at skrive @MainActor på hver eneste type i din app. (Hvis du nogensinde har vedligeholdt en SwiftUI-app med 50+ view models, ved du præcis, hvilken befrielse det her er.)
Sådan slår du det til
I dit Xcode-projekt går du til Build Settings og finder feltet Default Actor Isolation. Sæt det til MainActor. Bruger du Swift Package Manager, sætter du flagget direkte i Package.swift:
// Package.swift
.target(
name: "MyApp",
swiftSettings: [
.defaultIsolation(MainActor.self),
.enableUpcomingFeature("NonIsolatedNonSendingByDefault")
]
)
Hvad ændrer sig i din kode?
Før Swift 6.2 (eller uden flaget) skulle du skrive sådan her:
@MainActor
final class ProfileViewModel: ObservableObject {
@Published var name: String = ""
@MainActor
func updateName(_ newName: String) {
name = newName
}
}
Med default main actor isolation aktiveret kan du skrive præcis det samme — bare uden annotationerne:
final class ProfileViewModel {
var name: String = ""
func updateName(_ newName: String) {
name = newName
}
}
Compileren behandler implicit klassen som @MainActor. Det betyder, at almindelige scripts, små apps og prototyper opfører sig som single-threaded programmer — uden at du behøver at lære hele actor-isolation-modellen, før du overhovedet får sat din første knap op.
nonisolated(nonsending): Køres på callerens actor
Inden Swift 6.2 var der en irriterende asymmetri mellem synkrone og asynkrone nonisolated-funktioner:
- En synkron
nonisolated-funktion kørte på callerens actor.
- En asynkron
nonisolated-funktion hoppede til den globale concurrent executor.
Den lille uoverensstemmelse var årsag til en latterlig mængde data race-fejl og overraskelser. SE-0461: Async function isolation løser problemet med nonisolated(nonsending) — en async-funktion, der nedarver isolation fra sin caller.
Eksempel: Før og efter
Tidligere skulle du være meget forsigtig med async-funktioner i nonisolated-kontekst:
// Swift 6.0 / 6.1 — switcher til global executor
class DataLoader {
nonisolated func fetch() async throws -> [Item] {
// Kører IKKE på callerens actor.
// Hvis du kalder den fra @MainActor, sker der en kontekstskifte.
try await URLSession.shared.data(from: url).0.decode()
}
}
I Swift 6.2 kan du markere funktionen som nonisolated(nonsending), så den kører dér, hvor den bliver kaldt fra:
class DataLoader {
nonisolated(nonsending) func fetch() async throws -> [Item] {
// Kører på callerens actor (typisk main actor i UI-kode).
// Ingen unødvendig kontekstskifte.
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([Item].self, from: data)
}
}
Aktivér det som standard
Vil du have alle dine nonisolated async-funktioner til at opføre sig sådan, så slå upcoming-flaget til:
// Package.swift
.enableUpcomingFeature("NonIsolatedNonSendingByDefault")
Det er en del af Swift 7's planlagte standarder. Adoptér det nu, så slipper du for migrationsarbejde senere.
@concurrent: Eksplicit baggrundsarbejde
Når default isolation flytter sig mod main actor, har vi brug for et nyt værktøj til at sige "nej, denne funktion skal absolut køre på en baggrundstråd". Det er præcis, hvad @concurrent gør.
actor ImageProcessor {
// Kører på actorens executor — fint til lette opgaver
func resize(_ image: UIImage) async -> UIImage { ... }
// Eksplicit baggrundstråd — perfekt til CPU-tunge opgaver
@concurrent
nonisolated func applyHeavyFilter(_ data: Data) async -> Data {
// Kører på den globale concurrent executor.
// Frigiver main actor og actoren selv.
return performExpensiveImageProcessing(data)
}
}
Reglerne for @concurrent
@concurrent kan kun anvendes på nonisolated-funktioner.
- Du kan ikke kombinere
@concurrent med @MainActor eller en anden global actor — det ville være selvmodsigende.
- Alt input til en
@concurrent-funktion skal være Sendable.
- Funktionen får sit eget isolation domain — du kan ikke længere referere til actorens state inde i kroppen uden eksplicit hop.
Hvornår skal du bruge @concurrent?
Brug @concurrent, når du har:
- JSON-decoding af store payloads.
- Billed- eller videoprocessering.
- Krypterings- eller hashing-operationer.
- Computational-tunge algoritmer som sortering eller filtrering af store datasæt.
Brug det ikke, når operationen primært er I/O-baseret. URLSession.shared.data(from:) suspenderer allerede pænt selv, og du opnår intet (og taber faktisk lidt) ved at flytte den til en baggrundstråd.
Hurtigreference: Hvor kører min funktion?
Det her er det skema, jeg selv har klistret op ved siden af min skærm. Helt ærligt — det sparer mig fem minutter, hver gang jeg bliver i tvivl:
| Erklæring | Async? | Hvor kører den? |
nonisolated | Synkron | Callerens actor |
nonisolated | Async (Swift 6.1) | Global executor |
nonisolated(nonsending) | Async | Callerens actor |
@concurrent nonisolated | Async | Global executor |
@MainActor | Begge | Main actor |
Den nye Observations async sequence
Som komplement til Observation-frameworket har Swift 6.2 fået en ny async sequence-type kaldet Observations. Den lader dig streame transactional ændringer fra et @Observable-objekt:
@Observable
final class Cart {
var items: [Item] = []
var discount: Double = 0
var total: Double {
items.reduce(0) { $0 + $1.price } * (1 - discount)
}
}
let cart = Cart()
Task {
for await snapshot in Observations({ cart.total }) {
print("Ny total:", snapshot)
}
}
Det smarte: alle synkrone ændringer batches sammen og leveres som ét snapshot, når runtime'en når et naturligt suspensionspunkt. Det betyder ingen redundante UI-opdateringer og ingen mellemtilstande, som dine subscribers utilsigtet ser. Farvel til halvt opdaterede totals i din checkout-flow.
Migrationsstrategi: Fra Swift 6.0/6.1 til 6.2
Apple anbefaler en gradvis migration, og det er et godt råd. Her er en pragmatisk plan, jeg har set fungere godt i flere danske teams (inklusive et par fintech-projekter, hvor stabilitet var kritisk):
Trin 1: Opdatér Xcode og toolchain
Skift til Xcode 16.4 eller nyere. Hold dit Swift Language Mode på 6, men opgradér compileren. Eksisterende kode skal stadig kompilere — og hvis ikke, så er det her, du opdager det.
Trin 2: Aktivér migrations-tooling
Tilføj følgende til dit target's Build Settings (eller Package.swift):
// Package.swift
swiftSettings: [
.enableUpcomingFeature("NonIsolatedNonSendingByDefault"),
.swiftLanguageMode(.v6)
]
Brug Xcode's nye "Migrate to Swift 6.2"-fix-its. De inserter automatisk @concurrent dér, hvor den eksisterende opførsel kræver det. Det er ikke perfekt, men det rammer 80 % af tilfældene.
Trin 3: Opt-in til default main actor isolation per modul
Start med UI-modulet. Det er typisk mest main-actor-tungt og får den største kodereduktion:
.target(
name: "AppUI",
swiftSettings: [
.defaultIsolation(MainActor.self)
]
)
Lad netværks-, persistence- og service-moduler beholde nonisolated default. De har sjældent gavn af main actor — og du vil virkelig ikke have, at en database-query pludselig blokerer UI-tråden.
Trin 4: Audit dine async nonisolated-funktioner
Med NonIsolatedNonSendingByDefault aktivt skifter eksisterende async nonisolated-funktioner adfærd: de begynder at køre på callerens actor i stedet for global executor. Gennemgå dem, og marker de tunge operationer eksplicit med @concurrent. Det her er trinnet, som er nemt at undervurdere — så tag dig tid.
Praktisk eksempel: En ende-til-ende refactoring
Lad os se på en typisk feed-loader, sådan som den så ud i Swift 6.0, og hvordan den bør se ud i Swift 6.2:
Før (Swift 6.0)
@MainActor
final class FeedViewModel: ObservableObject {
@Published var posts: [Post] = []
private let service: FeedService
init(service: FeedService) { self.service = service }
func reload() async {
do {
posts = try await service.fetchPosts()
} catch {
print("Fejl:", error)
}
}
}
final class FeedService: Sendable {
nonisolated func fetchPosts() async throws -> [Post] {
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([Post].self, from: data)
}
}
Efter (Swift 6.2 med Approachable Concurrency)
// Default isolation = MainActor for hele modulet
final class FeedViewModel {
var posts: [Post] = []
private let service: FeedService
init(service: FeedService) { self.service = service }
func reload() async {
do {
posts = try await service.fetchPosts()
} catch {
print("Fejl:", error)
}
}
}
final class FeedService: Sendable {
// Kører på callerens main actor — ingen kontekstskifte
nonisolated(nonsending) func fetchPosts() async throws -> [Post] {
let (data, _) = try await URLSession.shared.data(from: url)
// Tung decoding offloades eksplicit
return try await decode(data)
}
// CPU-tung — tvinges til baggrundstråd
@concurrent
nonisolated func decode(_ data: Data) async throws -> [Post] {
try JSONDecoder().decode([Post].self, from: data)
}
}
Forskellen? Koden er kortere, intentionen er tydeligere, og du har eksplicit kontrol over præcis hvilke operationer, der hopper til baggrundstråd. Det her er den slags refactoring, der giver mening — ikke kun for compileren, men også for den kollega, der skal læse koden om seks måneder.
Almindelige faldgruber (og hvordan du undgår dem)
"Min @concurrent-funktion fanger en isolated parameter"
Hvis en closure inde i din @concurrent-funktion fanger en isolated parameter (fx en @MainActor-bundet self), ignorerer compileren i visse versioner attributten. Løsningen: flyt den fangede tilstand ud i en lokal let før closuren — eller hop eksplicit til den korrekte actor.
"Mine tests fryser nu"
Hvis dine XCTest-tests pludselig hænger efter migration, er det ofte fordi de kører på main actor og venter på en async-funktion, der nu kører på samme actor. Markér testen som nonisolated, eller flyt den tunge logik til en @concurrent-funktion. Jeg har selv ramt det her cirka tre gange — det er ikke åbenlyst første gang.
"Jeg får 'sending' parameter-fejl"
Når du krydser actor-grænser, skal data overføres som sending. Det er et nyt nøgleord, der signalerer, at compileren midlertidigt ejer værdien og kan flytte den sikkert til en anden isolation domain. Marker parametren med sending, og giv afkald på enhver yderligere brug af værdien efter kaldet.
Ofte stillede spørgsmål
Hvad er forskellen på @concurrent og nonisolated i Swift 6.2?
Begge fjerner en funktion fra en specifik actor, men de adskiller sig i opførsel for async-funktioner. nonisolated alene (særligt med NonIsolatedNonSendingByDefault aktivt) lader async-funktionen køre på callerens actor. @concurrent tvinger funktionen til at køre på den globale concurrent executor — altså på en baggrundstråd uafhængigt af, hvem der kalder den.
Skal jeg aktivere default MainActor isolation i alle mine moduler?
Nej. Aktivér det i UI-tunge moduler (Views, ViewModels, App-laget), hvor de fleste typer alligevel skal være på main actor. Lad netværks-, database- og service-lag beholde standard nonisolated, så du ikke utilsigtet flasker tunge operationer op på main thread.
Er Swift 6.2 bagudkompatibel med kode fra Swift 6.0?
Ja. Swift 6.2 er en kildekompatibel udvidelse af Swift 6. Eksisterende kode kompilerer som før. Adfærdsændringerne sker kun, når du eksplicit aktiverer upcoming-features som NonIsolatedNonSendingByDefault eller skifter til defaultIsolation(MainActor.self).
Hvad er Observations async sequence god til?
Observations giver en transaktionel stream af ændringer fra et @Observable-objekt. Den er ideel til at synkronisere UI-tilstand med eksterne systemer (analytics, persistence, web sockets) uden at skulle skrive Combine-pipelines. Hver opdatering repræsenterer en konsistent snapshot, så du undgår mellemtilstande og redundante refresh-rounds.
Erstatter Swift Testing XCTest helt i 2026?
Nej, ikke endnu. Brug Swift Testing til alle nye unit tests — det integrerer fint med Swift 6.2's concurrency-model og kører tests parallelt by default. Behold XCTest til UI-tests (XCUIApplication) og performance-tests (measure), som Swift Testing endnu ikke understøtter direkte.
Konklusion
Swift 6.2's Approachable Concurrency er det første store skridt mod et concurrency-system, der både er sikkert og tilgængeligt. Default actor isolation fjerner ceremoni fra typisk app-kode, nonisolated(nonsending) fjerner overraskelser fra async-funktioner, og @concurrent giver dig eksplicit kontrol, når du har brug for at offloade tunge operationer.
For 2026 er anbefalingen klar: opgradér til Swift 6.2, aktivér default main actor isolation i dine UI-moduler, og adoptér NonIsolatedNonSendingByDefault-flaget tidligt for at være klar til Swift 7. De ekstra timer, du investerer nu, sparer dig dage af debugging længere fremme. Det er et af de få tilfælde, hvor "skift værktøj nu" rent faktisk er den lette vej.