Approachable Concurrency in Swift 6.2: Zo Werkt Veilige Gelijktijdigheid

Ontdek hoe Swift 6.2 concurrency toegankelijker maakt met Default Actor Isolation en het nieuwe @concurrent-attribuut. Complete handleiding met praktische codevoorbeelden voor je iOS-apps.

Inleiding: Concurrency Hoeft Niet Moeilijk Te Zijn

Als je ooit met Swift Concurrency hebt gewerkt, ken je het gevoel waarschijnlijk wel. Je opent je project, schakelt strict concurrency checking in, en dan... een tsunami aan compilerwaarschuwingen en fouten. Sendable-conformances overal, @MainActor-annotaties op elke view, en mysterieuze foutmeldingen over actor isolation die je code onleesbaar maken. Eerlijk? Het was behoorlijk frustrerend.

Maar daar is verandering in gekomen.

Met Swift 6.2 en Xcode 26 heeft Apple dit fundamenteel aangepakt. De nieuwe Approachable Concurrency-features draaien de hele benadering om: in plaats van te beginnen in een multi-threaded wereld en overal veiligheidsannotaties toe te voegen, begin je nu in een single-threaded wereld waar alles standaard op de @MainActor draait. Pas wanneer je expliciet achtergrondwerk nodig hebt, stap je buiten die veilige omgeving. Dat voelt een stuk logischer, als je het mij vraagt.

In deze handleiding lopen we door alle belangrijke veranderingen in Swift 6.2 — van Default Actor Isolation en nonisolated(nonsending) tot het nieuwe @concurrent-attribuut. Met praktische codevoorbeelden die je direct kunt toepassen.

Wat Is Approachable Concurrency?

Approachable Concurrency is de overkoepelende term voor een reeks veranderingen in Swift 6.2 die het werken met gelijktijdigheid eenvoudiger en toegankelijker maken. Het kernidee? Progressive disclosure: Swift vraagt je alleen om zoveel over concurrency te begrijpen als je daadwerkelijk nodig hebt.

De belangrijkste pijlers zijn:

  • Default Actor Isolation (SE-0466): Alle code draait standaard op de @MainActor, tenzij je expliciet anders aangeeft.
  • Nonisolated Nonsending by Default (SE-0461): Nonisolated async functies draaien op de actor van de aanroeper, niet op een willekeurige achtergrondthread.
  • Het @concurrent attribuut: Een nieuw attribuut om expliciet aan te geven dat een functie op een achtergrondthread moet draaien.
  • Verbeterde protocol-conformances: Actor-geïsoleerde typen kunnen nu veiliger conformeren aan protocollen.
  • Automatische Sendable-inferentie: De compiler leidt @Sendable automatisch af wanneer dat veilig is.

Laten we elk van deze pijlers eens in detail bekijken.

Default Actor Isolation: Alles Op de MainActor

Dit is de verandering waar ik het meest enthousiast over ben. Default Actor Isolation, geïntroduceerd via SE-0466, is de meest ingrijpende vernieuwing in Swift 6.2. In plaats van aan te nemen dat code geen isolatie heeft (zoals in Swift 6.1 en eerder), neemt de compiler nu aan dat je code op de @MainActor moet draaien — tenzij je expliciet iets anders opgeeft.

Wat betekent dit in de praktijk?

Vóór Swift 6.2 moest je overal @MainActor toevoegen aan je views, view models en andere UI-gerelateerde code:

// Swift 6.1 - Veel annotaties nodig
@MainActor
class UserViewModel: ObservableObject {
    @Published var userName: String = ""
    @Published var isLoading: Bool = false

    @MainActor
    func loadUser() async {
        isLoading = true
        let user = try? await apiService.fetchUser()
        userName = user?.name ?? "Onbekend"
        isLoading = false
    }
}

Met Default Actor Isolation in Swift 6.2 wordt dit een stuk eenvoudiger:

// Swift 6.2 - MainActor is de standaard
class UserViewModel: ObservableObject {
    @Published var userName: String = ""
    @Published var isLoading: Bool = false

    func loadUser() async {
        isLoading = true
        let user = try? await apiService.fetchUser()
        userName = user?.name ?? "Onbekend"
        isLoading = false
    }
}

Zie je het verschil? Geen enkele @MainActor-annotatie meer nodig. De compiler weet automatisch dat deze klasse en al haar methoden op de main actor draaien. Dat scheelt niet alleen typwerk, maar maakt je code ook gewoon een stuk leesbaarder.

Inschakelen in je project

Nieuwe projecten aangemaakt in Xcode 26 hebben Approachable Concurrency standaard ingeschakeld. Voor bestaande projecten kun je het handmatig activeren via je Build Settings:

  • Approachable Concurrency: Zet dit op Yes
  • Default Actor Isolation: Zet dit op MainActor

Voor Swift Package Manager-projecten voeg je het volgende toe aan je Package.swift:

// Package.swift
let package = Package(
    name: "MijnProject",
    platforms: [.iOS(.v26)],
    targets: [
        .target(
            name: "MijnProject",
            swiftSettings: [
                .defaultIsolation(MainActor.self),
                .enableExperimentalFeature("NonisolatedNonsendingByDefault")
            ]
        )
    ]
)

Hoe de compiler hiermee omgaat

Wanneer Default Actor Isolation is ingeschakeld, behandelt de compiler elke declaratie alsof er @MainActor op staat, tenzij:

  • De declaratie expliciet gemarkeerd is als nonisolated
  • De declaratie is toegewezen aan een andere actor (bijv. @CustomActor)
  • De declaratie zich bevindt in een module die de feature niet heeft ingeschakeld

Dit geldt voor klassen, structs, enums, functies, properties en zelfs extensions. Het is echt een diepgaande verandering die de manier waarop je over concurrency nadenkt fundamenteel wijzigt.

Nonisolated Begrijpen: Het Verlaten van de MainActor

Oké, als alles standaard op de @MainActor draait — hoe voer je dan werk uit op een achtergrondthread? Hier komt het nonisolated-keyword om de hoek kijken. Maar let op: in Swift 6.2 heeft nonisolated een subtiel ander gedrag dan je misschien verwacht.

nonisolated(nonsending): De nieuwe standaard

In Swift 6.1 betekende nonisolated op een async functie dat de functie op de global executor zou draaien — effectief op een achtergrondthread. In Swift 6.2 verandert dit gedrag dankzij SE-0461.

Met de NonisolatedNonsendingByDefault-feature draait een nonisolated async functie nu standaard op de actor van de aanroeper. Dit wordt intern aangeduid als nonisolated(nonsending):

// Swift 6.2 met Approachable Concurrency
class DataProcessor {
    // Deze functie draait op de MainActor (standaard isolatie)
    func processData() async {
        let result = await transform(data: sampleData)
        // result is beschikbaar op de MainActor
        updateUI(with: result)
    }

    // nonisolated, maar draait op de actor van de aanroeper (MainActor)
    // dankzij nonsending-gedrag
    nonisolated func transform(data: Data) async -> ProcessedData {
        // Deze code draait op dezelfde actor als de aanroeper
        // In dit geval: de MainActor
        return ProcessedData(data: data)
    }
}

Het grote voordeel? Omdat je niet naar een ander isolatiedomein springt, hoef je je types niet Sendable te maken. De data blijft op dezelfde actor, dus er is simpelweg geen risico op data races.

Waarom is dit belangrijk?

In Swift 6.1 was het volgende patroon een veelvoorkomende bron van frustratie:

// Swift 6.1 - Problematisch
@MainActor
class ViewController {
    var items: [Item] = []

    func refresh() async {
        // fetchItems() is nonisolated async en draait op achtergrondthread
        // items moet Sendable zijn om veilig te verzenden
        let newItems = await fetchItems()
        items = newItems // Potentieel probleem: Sendable check
    }

    nonisolated func fetchItems() async -> [Item] {
        // Draait op achtergrondthread in Swift 6.1
        return try! await api.getItems()
    }
}

In Swift 6.2 verdwijnt dit probleem omdat fetchItems() nu op dezelfde actor draait als de aanroeper. Geen Sendable-checks nodig, geen verrassende thread-switches, geen data races. Gewoon code die doet wat je verwacht.

@concurrent: Expliciet Naar de Achtergrond

Maar wat als je wél wilt dat een functie op een achtergrondthread draait? Denk aan zware berekeningen, image processing, of het parsen van grote JSON-bestanden. Daarvoor introduceert Swift 6.2 het @concurrent-attribuut.

Basis van @concurrent

Door een functie te markeren met @concurrent, geef je expliciet aan dat deze altijd op de global executor moet draaien, ongeacht waar deze wordt aangeroepen:

class ImageService {
    // Draait altijd op een achtergrondthread
    @concurrent
    nonisolated func processImage(_ image: UIImage) async -> UIImage {
        // Zware beeldbewerking hier
        let ciImage = CIImage(image: image)!
        let filter = CIFilter(name: "CIGaussianBlur")!
        filter.setValue(ciImage, forKey: kCIInputImageKey)
        filter.setValue(10.0, forKey: kCIInputRadiusKey)

        let context = CIContext()
        let outputImage = filter.outputImage!
        let cgImage = context.createCGImage(outputImage, from: outputImage.extent)!
        return UIImage(cgImage: cgImage)
    }
}

Let op dat @concurrent alleen kan worden toegepast op functies die nonisolated zijn. Best logisch eigenlijk — je kunt een functie niet tegelijkertijd aan een specifieke actor koppelen én op een willekeurige achtergrondthread laten draaien.

Wanneer gebruik je @concurrent?

Gebruik @concurrent wanneer:

  • CPU-intensief werk: Image processing, cryptografie, complexe berekeningen
  • Grote data parsing: JSON-decodering van grote datasets, XML-parsing
  • Bestandsoperaties: Het lezen of schrijven van grote bestanden
  • Blokkerende operaties: Elke operatie die de main thread merkbaar zou vertragen

Gebruik @concurrent niet wanneer:

  • De functie snel genoeg is om op de main thread te draaien
  • De functie UI-state leest of wijzigt
  • De functie geen async werk doet

Een praktisch voorbeeld: JSON-decodering

Hier is een voorbeeld dat ik zelf regelmatig gebruik — het splitsen van netwerk-requests en JSON-decodering:

struct APIClient {
    // Netwerk-request: draait op de aanroepers actor (MainActor)
    // omdat het snel is en geen zware verwerking doet
    nonisolated func fetchData(from url: URL) async throws -> Data {
        let (data, _) = try await URLSession.shared.data(from: url)
        return data
    }

    // JSON-decodering: draait op achtergrondthread
    // omdat het CPU-intensief kan zijn bij grote datasets
    @concurrent
    nonisolated func decode(_ type: T.Type, from data: Data) async throws -> T {
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        return try decoder.decode(type, from: data)
    }
}

// Gebruik in een ViewModel
class ProductViewModel {
    var products: [Product] = []
    var isLoading = false

    private let client = APIClient()

    func loadProducts() async {
        isLoading = true
        defer { isLoading = false }

        do {
            let url = URL(string: "https://api.example.com/products")!
            let data = try await client.fetchData(from: url)
            // decode() draait automatisch op achtergrondthread
            products = try await client.decode([Product].self, from: data)
        } catch {
            print("Fout bij laden: \(error)")
        }
    }
}

In dit voorbeeld draait fetchData(from:) op de MainActor (via nonsending), wat prima is omdat netwerk-IO niet CPU-intensief is. De decode(_:from:)-methode daarentegen is gemarkeerd met @concurrent omdat JSON-decodering bij grote datasets best merkbaar kan zijn op de main thread.

De Drie Isolatiemodi Vergeleken

Om het overzichtelijk te houden, hier de drie isolatiemodi naast elkaar:

// 1. MainActor (standaard in Swift 6.2)
// Draait op de main thread - voor UI-werk
class MyViewModel {
    func updateUI() async {
        // Draait op MainActor (impliciet)
    }
}

// 2. nonisolated (nonsending standaard)
// Draait op de actor van de aanroeper
class MyService {
    nonisolated func lightWork() async {
        // Draait op dezelfde actor als wie het aanroept
        // Geen Sendable vereist
    }
}

// 3. @concurrent nonisolated
// Draait altijd op achtergrondthread
class MyProcessor {
    @concurrent
    nonisolated func heavyWork() async {
        // Draait altijd op global executor (achtergrondthread)
        // Parameters en return values moeten Sendable zijn
    }
}

Het beslissingsproces is eigenlijk vrij simpel: begin op de MainActor. Als een functie geen actor-state nodig heeft maar ook geen zwaar werk doet, maak het nonisolated. Alleen wanneer een functie echt CPU-intensief werk doet, voeg je @concurrent toe. Zo simpel is het.

Protocol Conformances en Actor Isolatie

Een van de meest frustrerende aspecten van Swift Concurrency vóór 6.2 was (naar mijn mening) het conformeren aan protocollen vanuit actor-geïsoleerde typen. Als je klasse op de @MainActor draaide en je wilde conformeren aan Equatable, Hashable of Codable, liep je al snel tegen compileerfouten aan.

Het probleem in Swift 6.1

// Swift 6.1 - Problematisch
@MainActor
class User: Equatable {
    var name: String
    var email: String

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

    // Compilerfout: Static method '==' isolated to global actor
    // 'MainActor' can not satisfy corresponding requirement from
    // protocol 'Equatable'
    static func == (lhs: User, rhs: User) -> Bool {
        lhs.name == rhs.name && lhs.email == rhs.email
    }
}

Echt vervelend, toch?

De oplossing in Swift 6.2

Swift 6.2 introduceert actor-geïsoleerde protocol conformances. Een @MainActor-type kan nu conformeren aan protocollen met een geïsoleerde conformance die alleen geldig is wanneer het wordt aangeroepen vanuit dezelfde actor:

// Swift 6.2 - Dit werkt nu gewoon
class User: Equatable {
    var name: String
    var email: String

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

    static func == (lhs: User, rhs: User) -> Bool {
        lhs.name == rhs.name && lhs.email == rhs.email
    }
}

De compiler begrijpt dat User op de @MainActor draait (dankzij Default Actor Isolation) en staat de Equatable-conformance gewoon toe. De conformance werkt wanneer je de vergelijking uitvoert vanuit de MainActor-context, wat logisch is voor UI-modellen.

Codable en achtergrondwerk

Bij Codable-conformances moet je iets meer nadenken. Als je modellen op de MainActor draaien maar je ze wilt decoderen op een achtergrondthread, heb je een keuze:

// Optie 1: Model op MainActor, decodering ook op MainActor
// Geschikt voor kleine modellen
class UserProfile: Codable {
    var name: String
    var bio: String
    var avatarURL: URL?
}

// Optie 2: Model expliciet nonisolated voor achtergronddecodering
nonisolated class APIResponse: Codable {
    var data: T
    var metadata: ResponseMetadata
}

Voor kleine modellen is decoderen op de MainActor meestal prima. Voor grote datasets of complexe structuren kun je het model expliciet nonisolated markeren.

Automatische Sendable-inferentie

Swift 6.2 verbetert ook de automatische inferentie van @Sendable voor methoden en key path literals. Als een methode behoort tot een Sendable-type, hoef je niet langer handmatig @Sendable toe te voegen. Dat scheelt weer een hoop boilerplate.

struct DataParser: Sendable {
    func parse(_ data: Data) -> [String] {
        // Parsing logica
        return String(data: data, encoding: .utf8)?
            .components(separatedBy: "\n") ?? []
    }
}

// Swift 6.1: handmatige @Sendable annotatie nodig
// let parser = DataParser()
// Task.detached {
//     let result = await (@Sendable { parser.parse(data) })()
// }

// Swift 6.2: automatisch afgeleid
let parser = DataParser()
Task.detached {
    let result = parser.parse(data) // Werkt zonder @Sendable
}

Een welkome verbetering, vooral als je veel werkt met Task.detached en andere contexten waar Sendable-conformance vereist is.

Migratie: Van Swift 6.1 naar Swift 6.2

De overstap naar Approachable Concurrency is optioneel — je bestaande Swift 6.1-code blijft gewoon werken. Maar als je wilt migreren (en dat raad ik je aan), zijn hier de stappen:

Stap 1: Feature flags inschakelen

Schakel Approachable Concurrency in via Xcode Build Settings of je Package.swift, zoals eerder beschreven. Mijn tip: begin met één target om het effect te testen voordat je het overal inschakelt.

Stap 2: Overbodige @MainActor-annotaties verwijderen

Met Default Actor Isolation ingeschakeld kun je de meeste expliciete @MainActor-annotaties verwijderen. Dat voelt bijna therapeutisch:

// Voor migratie
@MainActor
class SettingsViewModel: ObservableObject {
    @MainActor @Published var theme: Theme = .system
    @MainActor @Published var notificationsEnabled: Bool = true

    @MainActor
    func saveSettings() async throws {
        try await settingsService.save(theme: theme,
                                        notifications: notificationsEnabled)
    }
}

// Na migratie
class SettingsViewModel: ObservableObject {
    @Published var theme: Theme = .system
    @Published var notificationsEnabled: Bool = true

    func saveSettings() async throws {
        try await settingsService.save(theme: theme,
                                        notifications: notificationsEnabled)
    }
}

Stap 3: Achtergrondwerk identificeren met @concurrent

Dit is de stap waar je het meest op moet letten. Zoek functies die voorheen nonisolated waren en impliciet op een achtergrondthread draaiden. Als ze CPU-intensief werk doen, voeg dan @concurrent toe:

// Voor migratie (Swift 6.1)
class ImageCache {
    nonisolated func resizeImage(_ image: UIImage,
                                 to size: CGSize) async -> UIImage {
        // Draaide impliciet op achtergrondthread in 6.1
        // Zware bewerking...
        return resizedImage
    }
}

// Na migratie (Swift 6.2)
class ImageCache {
    @concurrent
    nonisolated func resizeImage(_ image: UIImage,
                                 to size: CGSize) async -> UIImage {
        // @concurrent garandeert achtergrondthread
        // Zware bewerking...
        return resizedImage
    }
}

Stap 4: Sendable-conformances opschonen

Met minder actor-hops heb je mogelijk minder Sendable-conformances nodig. Controleer je types en verwijder onnodige conformances die alleen bestonden om data tussen actors te verzenden.

Stap 5: Testen en valideren

Na de migratie is het cruciaal om grondig te testen. Let vooral op:

  • Functies die nu op de MainActor draaien maar dat voorheen niet deden
  • Prestatieproblemen door te veel werk op de main thread
  • Derde-partij bibliotheken die nog niet zijn bijgewerkt voor Swift 6.2

Praktijkvoorbeeld: Een Complete App-architectuur

Laten we alle concepten samenbrengen in een realistisch voorbeeld. Dit is het soort architectuur dat ik zou gebruiken voor een nieuwe app in Swift 6.2:

import SwiftUI
import Foundation

// MARK: - Modellen (MainActor standaard)

struct Article: Codable, Identifiable {
    let id: Int
    let title: String
    let body: String
    let publishedAt: Date
}

// MARK: - Netwerklaag

struct NetworkClient {
    enum NetworkError: Error {
        case invalidURL
        case invalidResponse
        case decodingFailed
    }

    // Netwerk-requests: nonisolated, draait op aanroepers actor
    nonisolated func fetch(from urlString: String) async throws -> Data {
        guard let url = URL(string: urlString) else {
            throw NetworkError.invalidURL
        }
        let (data, response) = try await URLSession.shared.data(from: url)
        guard let httpResponse = response as? HTTPURLResponse,
              (200...299).contains(httpResponse.statusCode) else {
            throw NetworkError.invalidResponse
        }
        return data
    }

    // JSON-decodering: achtergrondthread voor grote datasets
    @concurrent
    nonisolated func decode(
        _ type: T.Type,
        from data: Data
    ) async throws -> T {
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
        return try decoder.decode(type, from: data)
    }
}

// MARK: - Repository

class ArticleRepository {
    private let client = NetworkClient()
    private let baseURL = "https://api.example.com"

    func fetchArticles() async throws -> [Article] {
        let data = try await client.fetch(from: "\(baseURL)/articles")
        return try await client.decode([Article].self, from: data)
    }

    func fetchArticle(id: Int) async throws -> Article {
        let data = try await client.fetch(from: "\(baseURL)/articles/\(id)")
        return try await client.decode(Article.self, from: data)
    }
}

// MARK: - ViewModel

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

    private let repository = ArticleRepository()

    func loadArticles() async {
        isLoading = true
        errorMessage = nil
        defer { isLoading = false }

        do {
            articles = try await repository.fetchArticles()
        } catch {
            errorMessage = "Kan artikelen niet laden: \(error.localizedDescription)"
        }
    }
}

// MARK: - View

struct ArticleListView: View {
    @State private var viewModel = ArticleListViewModel()

    var body: some View {
        NavigationStack {
            Group {
                if viewModel.isLoading {
                    ProgressView("Laden...")
                } else if let error = viewModel.errorMessage {
                    ContentUnavailableView(
                        "Fout",
                        systemImage: "exclamationmark.triangle",
                        description: Text(error)
                    )
                } else {
                    List(viewModel.articles) { article in
                        VStack(alignment: .leading, spacing: 8) {
                            Text(article.title)
                                .font(.headline)
                            Text(article.body)
                                .font(.subheadline)
                                .foregroundStyle(.secondary)
                                .lineLimit(2)
                        }
                    }
                }
            }
            .navigationTitle("Artikelen")
            .task {
                await viewModel.loadArticles()
            }
        }
    }
}

Kijk eens hoe schoon deze code is. Geen enkele @MainActor-annotatie, en toch is alles thread-safe. De NetworkClient gebruikt nonisolated voor het fetch-werk (dat op de aanroepers actor draait) en @concurrent voor de zware JSON-decodering. Precies zoals het hoort.

Veelgemaakte Fouten en Valkuilen

Bij het werken met Approachable Concurrency zijn er een paar valkuilen waar ik je graag voor wil waarschuwen:

1. Te veel werk op de MainActor

Het grootste risico van Default Actor Isolation is dat je per ongeluk zwaar werk op de main thread plaatst. Houd je UI responsief door CPU-intensief werk expliciet te markeren met @concurrent:

// Fout: zware berekening op MainActor
class AnalyticsProcessor {
    func processLargeDataset(_ data: [DataPoint]) -> AnalyticsResult {
        // Dit blokkeert de UI!
        return data.reduce(into: AnalyticsResult()) { result, point in
            result.addProcessedPoint(heavyComputation(point))
        }
    }
}

// Correct: verplaats naar achtergrondthread
class AnalyticsProcessor {
    @concurrent
    nonisolated func processLargeDataset(_ data: [DataPoint]) async -> AnalyticsResult {
        return data.reduce(into: AnalyticsResult()) { result, point in
            result.addProcessedPoint(heavyComputation(point))
        }
    }
}

2. @concurrent vergeten bij migratie

Dit is een subtiele maar belangrijke valkuil. Als je migreert van Swift 6.1, moet je bewust kiezen welke nonisolated-functies @concurrent nodig hebben. In 6.1 draaiden alle nonisolated async functies op een achtergrondthread; in 6.2 draaien ze op de aanroepers actor. Vergeet niet om zwaar werk te markeren!

3. Sendable-vereisten bij @concurrent

Wanneer je @concurrent gebruikt, spring je naar een ander isolatiedomein. Dit betekent dat parameters en returnwaarden Sendable moeten zijn:

// Dit compileert niet als MyData niet Sendable is
@concurrent
nonisolated func process(_ data: MyData) async -> MyResult {
    // ...
}

// Oplossing 1: Maak MyData Sendable
struct MyData: Sendable {
    let values: [Int]
}

// Oplossing 2: Gebruik nonisolated zonder @concurrent
// als je geen achtergrondthread nodig hebt
nonisolated func process(_ data: MyData) async -> MyResult {
    // Draait op dezelfde actor, geen Sendable nodig
}

4. Third-party bibliotheken

Niet alle bibliotheken zijn al bijgewerkt voor Swift 6.2. Als een bibliotheek nog nonisolated functies heeft die verwachten op een achtergrondthread te draaien, kunnen ze nu op je MainActor terechtkomen. Houd dit in de gaten en werk bibliotheken bij wanneer updates beschikbaar zijn.

Prestatie-overwegingen

Een veelgestelde vraag die ik ook vaak krijg: veroorzaakt het draaien van meer code op de MainActor prestatieproblemen?

Het korte antwoord: voor de meeste apps niet.

De meeste app-code is UI-gerelateerd en moet sowieso op de main thread draaien. Door dit expliciet te maken met Default Actor Isolation, voorkom je juist onnodige thread-switches die overhead veroorzaken:

  • Minder context switches: Elke keer dat je van de ene actor naar de andere springt, is er overhead. Minder hops betekent betere prestaties.
  • Minder Sendable checks: Zonder actor-hops zijn er minder runtime checks nodig voor thread-safety.
  • Betere cache-lokaliteit: Code die op dezelfde thread draait, profiteert van betere CPU-cache-prestaties.

De regel is simpel: meet eerst, optimaliseer daarna. Gebruik Instruments om te controleren of er daadwerkelijk main thread-blokkering optreedt voordat je code naar een achtergrondthread verplaatst.

Samenvatting en Aanbevelingen

Swift 6.2's Approachable Concurrency is, wat mij betreft, een van de beste verbeteringen aan de taal in jaren. Hier zijn de belangrijkste punten:

  • Begin op de MainActor: Met Default Actor Isolation draait alles standaard op de main thread. Veilig en eenvoudig.
  • Gebruik nonisolated voor lichte taken: Functies die geen actor-state nodig hebben maar ook niet CPU-intensief zijn, markeer je als nonisolated. Ze draaien dan op de actor van de aanroeper.
  • Gebruik @concurrent voor zwaar werk: Alleen wanneer een functie echt CPU-intensief werk doet, verplaats je het naar een achtergrondthread met @concurrent.
  • Migreer geleidelijk: Je hoeft niet alles in één keer om te zetten. Begin met nieuwe code en migreer bestaande code stap voor stap.
  • Test grondig: Let op prestatieproblemen en onverwacht gedrag na het inschakelen van de nieuwe features.

De toekomst van Swift Concurrency ziet er helder uit. Apple heeft duidelijk geluisterd naar de feedback van de community en een pad gecreëerd dat zowel veilig als toegankelijk is. Of je nu een ervaren concurrency-expert bent of net begint met async/await — Swift 6.2 maakt het werken met gelijktijdigheid eenvoudiger dan ooit.

Klaar om te beginnen? Open Xcode 26, maak een nieuw project aan, en ervaar zelf hoe soepel concurrency kan zijn wanneer je niet langer vecht tegen de compiler — maar ermee samenwerkt.

Over de Auteur Editorial Team

Our team of expert writers and editors.