Swift 6.2 ja lähestyttävä rinnakkaisuus: Käytännön opas, jota olet odottanut

Swift 6.2 mullistaa rinnakkaisuuden käsittelyn. Opi hallitsemaan MainActor-oletuseristys, @concurrent-attribuutti, nonisolated(nonsending) ja Observations-sekvenssi käytännön esimerkkien avulla.

Johdanto: Miksi Swift 6.2 on iso juttu

Swift 6.2 julkaistiin yhdessä Xcode 26:n ja iOS 26:n kanssa WWDC25-tapahtumassa, ja rehellisesti sanottuna — tämä on merkittävin muutos Swiftin rinnakkaisuusmallissa sitten async/await-syntaksin käyttöönoton. Tämä ei ole mikään pieni päivitys tai muutama uusi ominaisuus. Kyseessä on kokonaisvaltainen filosofinen suunnanmuutos.

Muutoksen ytimessä on käsite nimeltä "lähestyttävä rinnakkaisuus" (Approachable Concurrency). Perusajatus on oikeastaan aika yksinkertainen: suurin osa mobiilisovelluksen koodista ei tarvitse rinnakkaisuutta lainkaan.

Mieti hetki tyypillistä iOS-sovellusta. Se on pohjimmiltaan yksisäikeinen ohjelma, joka suorittaa käyttöliittymälogiikkaa pääsäikeessä ja silloin tällöin tekee verkkokutsuja tai raskaita laskentaoperaatioita taustalla. Silti Swiftin aiempi rinnakkaisuusmalli pakotti kehittäjät ymmärtämään monimutkaisia käsitteitä kuten Sendable-yhteensopivuus, aktorieristys ja tietokilpailuturvallisuus — vaikka he eivät varsinaisesti kirjoittaneet rinnakkaista koodia. Turhauttavaa, eikö?

Swift-tiimi julkaisi helmikuussa 2025 visioasiakirjan nimeltä "Improving the Approachability of Data-Race Safety", jossa esiteltiin asteittaisen paljastamisen (progressive disclosure) periaate. Idea on yksinkertainen: kehittäjän tarvitsee ymmärtää rinnakkaisuudesta vain sen verran kuin hän todella käyttää. Aloittelija voi kirjoittaa toimivaa Swift-koodia ymmärtämättä aktoreita, ja kokenut kehittäjä voi hienosäätää suorituskykyä — kumpikin ilman turhia varoituksia.

Tässä oppaassa käymme läpi kaikki Swift 6.2:n tärkeimmät rinnakkaisuusparannukset: oletuseristyksen MainActorilla, nonisolated(nonsending)-oletuskäyttäytymisen, uuden @concurrent-attribuutin, Observations-tyypin sekä lukuisia pienempiä parannuksia. Eli sukellettaan sisään.

Miksi rinnakkaisuus on ollut niin tuskallista?

Ennen kuin pääsemme uutuuksiin, pieni katsaus taaksepäin. Jos olet kamppaillut Swiftin rinnakkaisuusvaroitusten kanssa, et ole yksin.

Varoitusten tulva Swift 6:ssa

Kun Swift 6.0 otti käyttöön täyden tietokilpailuturvallisuustarkistuksen, monet projektit joutuivat kohtaamaan satojen — jopa tuhansien — varoitusten ja virheiden vyöryn. Vaikka koodin logiikka oli täysin oikein ja käytännössä yksisäikeistä, kääntäjä vaati Sendable-merkintöjä, @MainActor-annotaatioita ja eksplisiittistä aktorieristystä kaikkialta.

Kehittäjät, jotka halusivat vain päivittää projektinsa uusimpaan Swift-versioon, saattoivat käyttää päiviä pelkkien kääntäjävaroitusten korjaamiseen — ilman että koodin toiminnallisuus muuttui millään tavalla. Ei kovin motivoivaa.

Sendable-yhteensopivuuden ongelmat

Sendable-protokollan tarkoitus oli varmistaa, että tyypit voidaan turvallisesti lähettää aktorirajojen yli. Käytännössä tämä johti kuitenkin tilanteeseen, jossa lähes jokainen tyyppi tarvitsi joko eksplisiittisen Sendable-vaatimuksen tai @unchecked Sendable -merkinnän, joka käytännössä ohitti koko turvallisuustarkistuksen. Kolmannen osapuolen kirjastot, joita ei ollut päivitetty, aiheuttivat erityisen paljon harmia — kehittäjällä ei yksinkertaisesti ollut muuta vaihtoehtoa kuin odottaa kirjaston päivitystä.

Aktorieristyksen monimutkaisuus

Aktorieristys oli konseptina looginen, mutta käytännössä? Vaikeaselkoinen. Kehittäjien piti ymmärtää, milloin funktio suoritetaan MainActorilla, milloin taustasäikeessä, milloin tarvitaan await ja milloin ei. Erityisen hämmentävää oli se, että nonisolated async -funktiot saattoivat yllättäen siirtyä pois kutsujansa aktorikontekstista. Tämä johti vaikeasti jäljitettäviin bugeihin käyttöliittymäpäivityksissä.

Kuilu yksinkertaisen ja rinnakkaisen koodin välillä

Ehkä suurin ongelma oli se, ettei ollut olemassa mitään helppoa välimuotoa. Joko kirjoitit täysin synkronista koodia tai jouduit ymmärtämään koko rinnakkaisuusmallin kaikkine vivahteissaan. Tämä kuilu pelotti erityisesti aloittelevia kehittäjiä.

Käytännössä monet tiimit päätyivät joko sivuuttamaan varoitukset kokonaan, käyttämään @unchecked Sendable -merkintöjä kaikkialla tai jäämään vanhempiin Swift-versioihin. Mikään näistä ei ollut hyvä ratkaisu. Swift-yhteisössä alkoi kuulua yhä äänekkäämpiä vaatimuksia siitä, että mallin täytyy muuttua.

Lähestyttävän rinnakkaisuuden kolmivaiheinen malli

Swift 6.2:n lähestyttävä rinnakkaisuus perustuu kolmivaiheiseen malliin, joka noudattaa asteittaisen paljastamisen periaatetta. Jokainen vaihe tuo lisää monimutkaisuutta, mutta vain silloin kun kehittäjä itse tarvitsee sitä. Tämä on se ratkaiseva ero.

Vaihe 1: Yksinkertainen yksisäikeinen koodi

Ensimmäisessä vaiheessa kirjoitat tavallista, peräkkäistä Swift-koodia. Oletuseristys MainActorilla tarkoittaa, että kaikki koodi suoritetaan pääsäikeessä — aivan kuten perinteisissä yksisäikeisissä ohjelmissa. Sinun ei tarvitse tietää mitään aktoreista, Sendable-protokollasta tai tietokilpailuturvallisuudesta. Koodi yksinkertaisesti toimii.

Vaihe 2: Asynkroninen koodi ilman tietokilpailuvirheitä

Toisessa vaiheessa alat käyttää async/await-syntaksia verkkokutsujen tekemiseen. nonisolated(nonsending)-oletuskäyttäytyminen varmistaa, että asynkroniset funktiot pysyvät kutsujansa aktorikontekstissa. Voit siis kirjoittaa async-koodia ilman huolta siitä, millä säikeellä se suoritetaan.

Vaihe 3: Suorituskyvyn optimointi rinnakkaisuudella

Vasta kolmannessa vaiheessa, kun haluat eksplisiittisesti siirtää raskasta laskentaa taustasäikeelle, käytät @concurrent-attribuuttia. Tässä vaiheessa sinun on ymmärrettävä Sendable-vaatimukset, mutta vain niiltä osin kuin se on kyseisen funktion kannalta välttämätöntä.

Tämä malli on käänteentekevä, koska se poistaa aiemman "kaikki tai ei mitään" -asetelman. Jokainen kehittäjä voi oppia rinnakkaisuutta omaan tahtiinsa.

Oletuseristys MainActorilla (SE-0466)

SE-0466 on yksi Swift 6.2:n merkittävimmistä muutoksista. Se mahdollistaa kokonaisen moduulin oletuseristyksen MainActorille — käytännössä kaikki moduulin tyypit ja funktiot saavat implisiittisen @MainActor-merkinnän, ellei toisin erikseen määritellä.

Miten se toimii käytännössä?

Kun oletuseristys on päällä, kääntäjä käsittelee jokaista tyyppiä ja funktiota moduulissa ikään kuin se olisi merkitty @MainActor-attribuutilla. Kaikki koodi suoritetaan pääsäikeessä, aivan kuten perinteisessä iOS-sovelluksessa ennen rinnakkaisuusmallien aikaa. Ja mikä parasta — tämä poistaa valtaosan niistä turhauttavista "vääristä positiivisista" varoituksista.

Käyttöönotto Package.swift-tiedostossa

Oletuseristyksen saa käyttöön lisäämällä .defaultIsolation(MainActor.self) kohteen swiftSettings-asetuksiin:

.target(
    name: "MyApp",
    swiftSettings: [
        .defaultIsolation(MainActor.self)
    ]
)

Xcode 26 -projektissa sama asetus löytyy Build Settings -osiosta kohdasta "Default Actor Isolation", jonka voi asettaa arvoon MainActor.

Käytännön esimerkki

Kun oletuseristys on päällä, alla oleva koodi toimii ilman yhtään @MainActor-merkintää:

// Kaikki tässä moduulissa on implisiittisesti @MainActor
class ViewModel {
    var items: [String] = []
    var isLoading = false

    func loadItems() async {
        isLoading = true
        let result = try? await fetchItems()
        items = result ?? []
        isLoading = false
    }
}

struct ContentView: View {
    @State private var viewModel = ViewModel()

    var body: some View {
        List(viewModel.items, id: \.self) { item in
            Text(item)
        }
        .task {
            await viewModel.loadItems()
        }
    }
}

Aiemmin tämä koodi olisi vaatinut eksplisiittisiä @MainActor-merkintöjä sekä ViewModel-luokalle että näkymälle. Nyt ne ovat implisiittisiä, ja koodi on puhtaampaa.

Eristyksestä poikkeaminen

Jos tarvitset tyypin, joka ei ole MainActor-eristetty, käytä nonisolated-avainsanaa:

// Tämä luokka ei ole MainActor-eristetty,
// vaikka oletuseristys on päällä
nonisolated class DataParser {
    func parse(_ data: Data) -> [Item] {
        // Tämä voidaan suorittaa millä tahansa säikeellä
        return try! JSONDecoder().decode([Item].self, from: data)
    }
}

Hyvä tietää: oletuseristys koskee vain sitä moduulia, jossa se on määritelty. Kolmannen osapuolen kirjastot säilyttävät oman eristystasonsa. Voit siis ottaa ominaisuuden käyttöön yksi moduuli kerrallaan — ei tarvitse tehdä kaikkea kerralla.

nonisolated(nonsending) oletusarvoisesti (SE-0461)

SE-0461 muuttaa perustavanlaatuisesti sitä, miten nonisolated async -funktiot käyttäytyvät. Tämä on ehkä teknisesti merkittävin muutos Swift 6.2:ssa, koska se korjaa yhden yleisimmistä virhelähteistä asynkronisessa koodissa.

Ongelma Swift 6.1:ssä

Aiemmin nonisolated async -funktiot suoritettiin aina globaalilla rinnakkaisella suorittimella, riippumatta siitä mistä kontekstista ne kutsuttiin. MainActorilla kutsuttu funktio siirtyi siis automaattisesti taustasäikeelle — usein kehittäjän tietämättä:

// Swift 6.1 -käyttäytyminen
@MainActor
class ViewModel {
    func updateUI() async {
        await fetchData() // ⚠️ Siirtyy pois MainActorilta!
    }

    nonisolated func fetchData() async {
        // Suoritetaan taustasäikeessä
        // MainActor.assertIsolated() // 💥 Kaatuu!
    }
}

Kehittäjät olettivat luonnollisesti, että koska fetchData() on osa samaa luokkaa ja sitä kutsutaan MainActorilta, se myös suoritetaan MainActorilla. Mutta ei — funktio hyppäsi hiljaisesti taustasäikeelle. Tämä aiheutti vaikeasti löydettäviä bugeja erityisesti käyttöliittymäpäivityksissä.

Ratkaisu Swift 6.2:ssa

Swift 6.2:ssa nonisolated async -funktiot perivät oletusarvoisesti kutsujansa aktorikontekstin. Tätä kutsutaan nonisolated(nonsending)-käyttäytymiseksi:

// Swift 6.2 -käyttäytyminen
@MainActor
class ViewModel {
    func updateUI() async {
        await fetchData() // ✅ Pysyy MainActorilla
    }

    nonisolated func fetchData() async {
        // Perii kutsujan kontekstin
        MainActor.assertIsolated() // ✅ Toimii!
    }
}

Paljon intuitiivisempaa. Funktio pysyy sillä aktorilla, jolta sitä kutsuttiin, ellei toisin erikseen määritellä.

Käyttöönotto

SE-0461 on saatavilla upcoming feature -lippuna:

.enableUpcomingFeature("NonisolatedNonsendingByDefault")

Xcode 26:ssa tämä löytyy osana "Approachable Concurrency" -kääntäjäasetusta Build Settings -osiossa.

Huomio olemassa olevasta koodista

Tärkeä varoitus: tämä muutos voi vaikuttaa olemassa olevan koodin käyttäytymiseen. Jos sinulla on nonisolated async -funktioita, jotka on tarkoituksella suunniteltu taustasäikeelle, ne pysyvät nyt kutsujansa aktorilla. Merkitse ne silloin eksplisiittisesti @concurrent-attribuutilla.

@concurrent-attribuutti: Taustatyö selkeästi

@concurrent on Swift 6.2:n tapa sanoa: "haluan tämän funktion suoritettavan taustalla". Se on vastakohta uudelle nonisolated(nonsending)-oletuskäyttäytymiselle ja vastaa käytännössä Swift 6.1:n vanhaa nonisolated async -käyttäytymistä.

Milloin sitä tarvitaan?

@concurrent-attribuuttia käytetään, kun funktio tekee raskasta työtä, joka ei saa tukkia pääsäiettä. Tyypillisiä käyttötapauksia:

  • Kuvankäsittely — filttereiden soveltaminen, koon muuttaminen
  • JSON-dekoodaus — suurten datajoukkojen jäsentäminen
  • Tiedosto-operaatiot — luku- ja kirjoitusoperaatiot
  • Monimutkaiset laskennat — algoritmit, salaus, pakkaaminen
  • Verkkokutsut — pitkäkestoiset lataukset tai lähetykset

Käytännön esimerkki

Katsotaan esimerkki kuvankäsittelystä taustasäikeellä:

class ImageProcessor {
    @concurrent
    nonisolated func processImage(_ data: Data) async throws -> UIImage {
        // Suoritetaan rinnakkaisella säikeellä
        let processed = try await applyFilters(to: data)
        return processed
    }

    @concurrent
    nonisolated func applyFilters(to data: Data) async throws -> UIImage {
        // Raskas laskenta taustalla
        guard let image = UIImage(data: data) else {
            throw ImageError.invalidData
        }

        let ciImage = CIImage(image: image)!
        let filter = CIFilter(name: "CIPhotoEffectNoir")!
        filter.setValue(ciImage, forKey: kCIInputImageKey)

        guard let output = filter.outputImage else {
            throw ImageError.filterFailed
        }

        let context = CIContext()
        guard let cgImage = context.createCGImage(output, from: output.extent) else {
            throw ImageError.renderFailed
        }

        return UIImage(cgImage: cgImage)
    }
}

@MainActor
class PhotoViewModel: ObservableObject {
    @Published var image: UIImage?
    let processor = ImageProcessor()

    func loadPhoto(from data: Data) async {
        // processImage suoritetaan taustalla @concurrent ansiosta
        if let result = try? await processor.processImage(data) {
            self.image = result // Palaa automaattisesti MainActorille
        }
    }
}

Huomaa miten @concurrent ja nonisolated toimivat yhdessä. @concurrent kertoo kääntäjälle, että funktio suoritetaan erillisellä säikeellä. Kun loadPhoto kutsuu processImage-metodia, suoritus siirtyy automaattisesti taustasäikeelle, ja kun tulos palautetaan, palataan takaisin MainActorille. Kaikki tapahtuu automaattisesti await-avainsanan ansiosta.

Tämä malli on kriittinen käyttöliittymäsovelluksissa. Pääsäikeen tukkiminen raskaalla laskennalla johtaa hitauteen ja huonoon käyttäjäkokemukseen — ja käyttäjät huomaavat sen kyllä.

@concurrent vs. Task.detached

Aiemmin monet käyttivät Task.detached-kutsua siirtääkseen työn taustasäikeelle. @concurrent on eleganttimpi ratkaisu, koska se kuvaa funktion tarkoituksen suoraan sen määrittelyssä eikä kutsupaikassa. Parempi luettavuus, parempi ylläpidettävyys.

Observations: Uusi tapa seurata tilaa

Swift 6.2 tuo mukanaan uuden Observations-tyypin, joka on AsyncSequence-yhteensopiva rakenne @Observable-luokkien tilan seuraamiseen. Se korvaa monissa tapauksissa aiemman withObservationTracking-funktion ja tarjoaa huomattavasti paremman kehittäjäkokemuksen.

Perusesimerkki

@Observable
class ShoppingCart {
    var items: [CartItem] = []
    var totalPrice: Double = 0.0

    func addItem(_ item: CartItem) {
        items.append(item)
        totalPrice += item.price
    }
}

// Tilan seuraaminen Observations-sekvenssin avulla
let cart = ShoppingCart()

Task {
    let priceUpdates = Observations { cart.totalPrice }

    for await newTotal in priceUpdates {
        print("Kokonaishinta päivittyi: \(newTotal) €")
    }
}

Observations tarkkailee automaattisesti kaikkia ominaisuuksia, joihin viitataan sen sulkeumassa. Yllä seurataan vain totalPrice-ominaisuutta, joten sekvenssi tuottaa uuden arvon aina kun hinta muuttuu. Jos viittaisit useampaan ominaisuuteen, minkä tahansa niistä muuttuminen laukaisisi päivityksen.

Transaktionaaliset päivitykset

Yksi Observations-tyypin hienoimmista ominaisuuksista on transaktionaalisuus. Kun useita ominaisuuksia muutetaan synkronisesti, tarkkailijat näkevät vain lopputuloksen — eivät välitiloja. Transaktio päättyy seuraavaan await-kohtaan. Eli alla tarkkailija saa vain yhden päivityksen, vaikka kolme ominaisuutta muuttuu:

@Observable
class OrderManager {
    var items: [OrderItem] = []
    var totalPrice: Double = 0.0
    var itemCount: Int = 0

    func addItem(_ item: OrderItem) {
        // Nämä kaikki muutokset ovat osa samaa transaktiota
        items.append(item)
        totalPrice += item.price
        itemCount += 1
        // Tarkkailija saa yhden päivityksen kaikkien muutosten jälkeen
    }
}

Task {
    let orderUpdates = Observations {
        (orderManager.items, orderManager.totalPrice, orderManager.itemCount)
    }

    for await (items, total, count) in orderUpdates {
        print("Tilaus päivittyi: \(count) tuotetta, yhteensä \(total) €")
    }
}

Did-set-semantiikka

Observations-sekvenssit käyttävät did-set-semantiikkaa, eli arvot vastaanotetaan sen jälkeen kun ominaisuudet on muutettu. Tämä eroaa aiemmasta withObservationTracking-funktiosta, joka käytti will-set-semantiikkaa. Did-set on intuitiivisempi — vastaanotettu arvo on aina ajan tasalla.

Muistinhallinta ja weak self

Kun käytät Observations-sekvenssiä luokan sisällä, muista huolehtia muistinhallinnasta [weak self] -kaappauksella. Vahvat viittaussilmukat ovat edelleen mahdollisia (ja yleinen virhe):

@Observable
class DataManager {
    var data: [String] = []
    private var observationTask: Task?

    func startObserving(_ source: DataSource) {
        observationTask = Task { [weak self, weak source] in
            guard let source else { return }

            let updates = Observations { source.latestData }

            for await newData in updates {
                guard let self else { return }
                self.data = newData
            }
        }
    }

    func stopObserving() {
        observationTask?.cancel()
        observationTask = nil
    }
}

Huomaa miten sekä self että tarkkailtava objekti kaapataan heikolla viittauksella. guard let -tarkistus varmistaa, ettei sekvenssi jatku turhaan jos kohde on jo vapautettu muistista.

Käytännön migraatio-opas

Hyvä uutinen: olemassa olevan projektin migratointi voidaan tehdä asteittain. Ei tarvitse muuttaa kaikkea kerralla.

Vaihe 1: Päivitä työkaluketju

Varmista, että käytät Xcode 26:ta tai uudempaa. Swift 6.2:n ominaisuudet eivät ole saatavilla aiemmissa versioissa. Päivitä myös projektin Swift-kieliversio.

Vaihe 2: Ota käyttöön ominaisuudet asteittain

Suosittelen ottamaan ominaisuudet käyttöön tässä järjestyksessä:

  1. DefaultIsolation — ota käyttöön oletuseristys MainActorilla
  2. NonisolatedNonsendingByDefault — muuta asynkronisten funktioiden oletuskäyttäytyminen
  3. InferIsolatedConformances — anna kääntäjän päätellä eristetyt protokollayhteensopivuudet
  4. GlobalActorIsolatedTypesUsability — paranna globaaliaktorieristettyjen tyyppien käytettävyyttä
  5. InferSendableFromCaptures — päättele Sendable automaattisesti kaapatuista arvoista

Täydellinen Package.swift-konfiguraatio

Tässä koko konfiguraatio, joka ottaa käyttöön kaikki suositellut ominaisuudet:

// Package.swift
let package = Package(
    name: "MySwiftApp",
    platforms: [.iOS(.v26), .macOS(.v26)],
    targets: [
        .executableTarget(
            name: "MySwiftApp",
            swiftSettings: [
                .defaultIsolation(MainActor.self),
                .enableUpcomingFeature("NonisolatedNonsendingByDefault"),
                .enableUpcomingFeature("DisableOutwardActorInference"),
                .enableUpcomingFeature("GlobalActorIsolatedTypesUsability"),
                .enableUpcomingFeature("InferIsolatedConformances"),
                .enableUpcomingFeature("InferSendableFromCaptures")
            ]
        )
    ]
)

Vinkkejä asteittaiseen käyttöönottoon

  • Aloita yhdestä moduulista. Jos projektisi koostuu useista moduuleista, ota ominaisuudet käyttöön ensin yhdessä ja varmista että kaikki pelaa ennen laajentamista.
  • Tarkista @concurrent-tarpeet. Kun otat käyttöön NonisolatedNonsendingByDefault-ominaisuuden, käy läpi kaikki nonisolated async -funktiot. Jos jokin niistä on tarkoitettu taustasäikeelle, lisää @concurrent.
  • Testaa perusteellisesti. Erityisesti UI-päivitykset ja verkkokutsut on syytä testata huolellisesti, koska aktorikontekstin muutokset voivat vaikuttaa suoritusjärjestykseen.
  • Hyödynnä kääntäjän varoituksia. Swift 6.2:n kääntäjä antaa selkeitä varoituksia ja korjausehdotuksia — seuraa niitä migraation aikana.
  • Älä poista vanhoja @MainActor-merkintöjä heti. Ne eivät aiheuta haittaa, ja voit poistaa ne myöhemmin koodin siistimisvaiheessa.

Kokonainen esimerkki: Säätietosovelluksen ViewModel

Tarkastellaan realistista esimerkkiä, joka hyödyntää kaikkia Swift 6.2:n uusia rinnakkaisuusominaisuuksia. Rakennetaan yksinkertainen säätietosovellus.

Datamallit

struct Weather: Codable, Sendable {
    let temperature: Double
    let description: String
    let humidity: Int
    let windSpeed: Double
}

struct DayForecast: Codable, Sendable, Identifiable {
    let id: UUID
    let date: Date
    let highTemperature: Double
    let lowTemperature: Double
    let description: String
    let icon: String
}

Verkkoasiakasluokka

Verkkoasiakasluokka käyttää @concurrent-attribuuttia, koska verkkokutsut ja JSON-dekoodaus kannattaa suorittaa taustalla:

class NetworkClient {
    @concurrent
    nonisolated func fetchCurrentWeather(city: String) async throws -> Weather {
        let url = URL(string: "https://api.weather.com/current?city=\(city)")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode(Weather.self, from: data)
    }

    @concurrent
    nonisolated func fetchForecast(city: String) async throws -> [DayForecast] {
        let url = URL(string: "https://api.weather.com/forecast?city=\(city)")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode([DayForecast].self, from: data)
    }
}

ViewModel

ViewModel käyttää @Observable-makroa ja hyödyntää async let -sidontaa rinnakkaisten kutsujen tekemiseen:

@Observable
class WeatherViewModel {
    var currentWeather: Weather?
    var forecast: [DayForecast] = []
    var isLoading = false
    var errorMessage: String?

    private let networkClient = NetworkClient()

    func loadWeather(for city: String) async {
        isLoading = true
        errorMessage = nil

        do {
            async let weather = networkClient.fetchCurrentWeather(city: city)
            async let days = networkClient.fetchForecast(city: city)

            let (weatherResult, forecastResult) = try await (weather, days)

            currentWeather = weatherResult
            forecast = forecastResult
        } catch {
            errorMessage = "Säätietojen lataaminen epäonnistui: \(error.localizedDescription)"
        }

        isLoading = false
    }
}

Muutama huomio tästä koodista. WeatherViewModel ei tarvitse eksplisiittistä @MainActor-merkintää — oletuseristys hoitaa sen. loadWeather-funktio voi turvallisesti muokata UI-tilaa, koska se suoritetaan MainActorilla. Ja async let -sidonta mahdollistaa kahden verkkokutsun suorittamisen rinnakkain, kumpikin omalla taustasäikeellään.

Tämä on juuri se malli, johon Swift 6.2 pyrkii: suurin osa koodista suoritetaan yksinkertaisesti pääsäikeessä ilman ylimääräisiä merkintöjä, ja vain oikeasti raskaat operaatiot merkitään taustalle. Säikeistyksestä ei tarvitse murehtia muualla kuin siellä, missä se on oikeasti relevanttia.

SwiftUI-näkymä

struct WeatherView: View {
    @State private var viewModel = WeatherViewModel()

    var body: some View {
        NavigationStack {
            VStack {
                if viewModel.isLoading {
                    ProgressView("Ladataan säätietoja...")
                } else if let weather = viewModel.currentWeather {
                    WeatherCard(weather: weather)

                    List(viewModel.forecast) { day in
                        ForecastRow(forecast: day)
                    }
                } else if let error = viewModel.errorMessage {
                    ContentUnavailableView(
                        "Virhe",
                        systemImage: "exclamationmark.triangle",
                        description: Text(error)
                    )
                }
            }
            .navigationTitle("Sää")
            .task {
                await viewModel.loadWeather(for: "Helsinki")
            }
        }
    }
}

Tilan seuraaminen Observations-sekvenssin avulla

Jos haluat seurata tiettyä ominaisuutta ohjelmallisesti (vaikkapa analytiikkaa varten), Observations-sekvenssi on siihen juuri oikea työkalu:

// Esimerkki: Seurataan lämpötilan muutoksia analytiikkaa varten
class WeatherAnalytics {
    private var trackingTask: Task?

    func startTracking(_ viewModel: WeatherViewModel) {
        trackingTask = Task { [weak viewModel] in
            guard let viewModel else { return }

            let temperatureChanges = Observations {
                viewModel.currentWeather?.temperature
            }

            for await temperature in temperatureChanges {
                if let temp = temperature {
                    print("Lämpötila päivittyi: \(temp)°C")
                    await AnalyticsService.shared.log(
                        event: "temperature_update",
                        value: temp
                    )
                }
            }
        }
    }

    func stopTracking() {
        trackingTask?.cancel()
        trackingTask = nil
    }
}

Nimetyt tehtävät ja parempi debuggaus

Swift 6.2 tuo SE-0469:n myötä mahdollisuuden nimetä Task-tehtäviä. Tämä on yllättävän suuri parannus käytännön kehitystyössä.

Tehtävien nimeäminen

Aiemmin tehtävät olivat nimettömiä, mikä teki niiden tunnistamisesta debuggerissa melko hankalaa. Nyt voit antaa jokaiselle tehtävälle selkeän nimen:

Task(name: "Säätietojen lataus") {
    await viewModel.loadWeather(for: "Helsinki")
}

Task(name: "Kuvan käsittely - profiilikuva") {
    await imageProcessor.processProfileImage(data)
}

// Tehtäväryhmissä jokainen alatehtävä voidaan nimetä
await withTaskGroup(of: UIImage.self) { group in
    for (index, imageData) in imageDatas.enumerated() {
        group.addTask(name: "Kuvan käsittely \(index + 1)/\(imageDatas.count)") {
            try await processor.processImage(imageData)
        }
    }
}

Nimen lukeminen ohjelmallisesti

Tehtävän nimi on luettavissa myös koodista käsin — kätevää erityisesti lokituksessa:

func performWork() async {
    if let taskName = Task.name {
        print("Suoritetaan tehtävää: \(taskName)")
    }
    // ... varsinainen työ
}

LLDB-debuggerin parannukset

Swift 6.2 parantaa myös LLDB-debuggerin tukea asynkroniselle koodille merkittävästi:

  • Luotettava asynkroninen askellus — voit nyt astua asynkronisten funktioiden sisään ilman että debuggeri "kadottaa" suorituksen kontekstin säikeen vaihdon yhteydessä.
  • Tehtäväkontekstin näyttäminen — breakpointissa LLDB näyttää nyt millä tehtävällä koodi suoritetaan, pelkän säikenumeron sijaan.
  • Parannettu kutsupinon näyttö — LLDB seuraa nyt tehtävän asynkronista kutsupinoa, joten näet koko kutsuketjun vaikka suoritus olisi siirtynyt eri säikeelle.
  • Nimettyjen tehtävien tunnistaminen — nimetyt tehtävät näkyvät LLDB:ssä nimellään, mikä helpottaa monimutkaisten tehtävähierarkioiden ymmärtämistä.

Nämä parannukset yhdessä nimettyjen tehtävien kanssa tekevät rinnakkaisen koodin debuggauksesta vihdoin käytännöllistä. Se on ollut pitkään yksi suurimmista kipupisteistä.

Lisäominaisuudet

Pääominaisuuksien lisäksi Swift 6.2 sisältää muitakin tärkeitä parannuksia.

InferIsolatedConformances (SE-0470)

Tämä ratkaisee yleisen ongelman, jossa MainActor-eristetty tyyppi ei voinut automaattisesti täyttää protokollavaatimuksia samalla eristyksellä. Nyt kääntäjä päättelee tämän automaattisesti:

protocol DataProvider {
    func fetchData() async -> [String]
}

// Swift 6.2 päättelee automaattisesti, että tämä
// yhteensopivuus on @MainActor-eristetty
class MyDataProvider: DataProvider {
    func fetchData() async -> [String] {
        return ["Helsinki", "Espoo", "Tampere"]
    }
}

GlobalActorIsolatedTypesUsability (SE-0434)

Tekee globaaliaktorieristettyjen tyyppien käytöstä joustavampaa. Kääntäjä päättelee automaattisesti @Sendable-yhteensopivuuden globaaliaktorieristettyille funktioille ja sulkeumille. Lisäksi Sendable-tyyppisten tallennettujen ominaisuuksien käsittely muuttuu luontevammaksi moduulin sisällä.

InferSendableFromCaptures

Parantaa Swiftin kykyä päätellä @Sendable-yhteensopivuus automaattisesti metodeille ja avainpolku-literaaleille. Jos metodi kuuluu Sendable-tyypille, kääntäjä hoitaa merkinnät puolestasi.

DisableOutwardActorInference

Estää aktorieristyksen "leviämisen" tyypistä sen jäseniin odottamattomilla tavoilla. Aiemmin kääntäjä saattoi päätellä eristyksen kontekstista tavalla, joka ei ollut tarkoitettu. Nyt eristys on aina eksplisiittistä tai peräisin oletuseristyksestä.

Suorituskyky ja muistikäyttö

Lähestyttävä rinnakkaisuus ei paranna pelkästään kehittäjäkokemusta — siitä seuraa myös konkreettisia suorituskykyetuja.

Vähemmän turhia aktorinvaihtoja

Koska nonisolated(nonsending)-funktiot pysyvät nyt kutsujansa aktorilla, turhia aktorinvaihtoja tapahtuu huomattavasti vähemmän. Jokainen vaihto vaatii kontekstin tallentamisen ja palauttamisen, ja nyt ne turhat eliminoidaan automaattisesti.

Tehokkaammat Observations-päivitykset

Observations-tyypin transaktionaalisuus tarkoittaa, että käyttöliittymä päivitetään harvemmin mutta tehokkaammin. Synkroniset muutokset kootaan yhteen ja lähetetään yhtenä päivityksenä, mikä vähentää turhia UI-renderöintejä merkittävästi.

Tietoisempi rinnakkaisuus

Koska taustasäikeelle siirtyminen on nyt eksplisiittinen valinta @concurrent-attribuutin kautta, kehittäjät tekevät tietoisempia päätöksiä. Rinnakkaisuutta käytetään vain siellä missä se on oikeasti tarpeen, mikä johtaa tehokkaampaan resurssien käyttöön.

Parhaat käytännöt ja yhteenveto

Swift 6.2:n lähestyttävä rinnakkaisuus on iso askel eteenpäin. Se tekee rinnakkaisuudesta sen, mitä sen olisi aina pitänyt olla — työkalun jota käytetään tarvittaessa, ei estettä joka on ylitettävä joka projektissa.

Uudet projektit

  • Ota käyttöön defaultIsolation(MainActor.self) kaikissa uusissa projekteissa. Poistaa valtaosan turhista varoituksista.
  • Ota käyttöön NonisolatedNonsendingByDefault varmistaaksesi intuitiivisen käyttäytymisen.
  • Käytä @Observable-makroa ja Observations-sekvenssiä tilan hallintaan Combinen sijaan.

Olemassa olevat projektit

  • Migratoikaa moduuli kerrallaan. Aloita uusimmista moduuleista tai niistä joissa on eniten varoituksia.
  • Testaa jokaisen vaiheen jälkeen. Erityisesti NonisolatedNonsendingByDefault voi muuttaa olemassa olevan koodin käyttäytymistä.
  • Merkitse taustatehtävät @concurrent-attribuutilla. Käy läpi kaikki nonisolated async -funktiot ja varmista oikeat merkinnät.

Rinnakkaisuuden suunnittelu

  • Käytä @concurrent vain kun tarvitset taustasuoritusta. Älä merkitse funktioita varmuuden vuoksi — käytä sitä vain kun se on oikeasti tarpeen.
  • Hyödynnä async let -sidontaa. Se on selkein tapa ilmaista rinnakkaisia operaatioita.
  • Suosi Observations-sekvenssiä. Se on selkeämpi ja tehokkaampi kuin vanha withObservationTracking.
  • Nimeä tehtävät. Task(name:) helpottaa debuggausta valtavasti, erityisesti monimutkaisissa sovelluksissa.

Yhteenveto

Swift 6.2:n lähestyttävä rinnakkaisuus edustaa merkittävää filosofista muutosta. Sen sijaan että kaikki pakotetaan ymmärtämään koko rinnakkaisuusmalli kerralla, Swift tarjoaa nyt portaittaisen polun:

  1. Aloittelija kirjoittaa tavallista koodia, joka toimii turvallisesti MainActorilla.
  2. Keskitason kehittäjä käyttää async/await-syntaksia luottaen siihen, että koodi pysyy oikealla aktorilla.
  3. Kokenut kehittäjä optimoi suorituskykyä @concurrent-attribuutilla ja eksplisiittisellä rinnakkaisuudella.

Tämä malli tekee Swiftistä vihdoin kielen, jossa rinnakkaisuus on oikeasti lähestyttävää — ei vain teoriassa. Jokainen kehittäjä voi valita oman tasonsa ilman turhia esteitä.

Swift 6.2 ja Xcode 26 ovat saatavilla nyt. Suosittelen aloittamaan uuden projektin näillä ominaisuuksilla tai kokeilemaan niitä pienessä olemassa olevassa moduulissa. Kun näet miten paljon puhtaampaa koodista tulee, et halua palata takaisin. Oma kokemukseni on, että suurin ero näkyy siinä miten paljon vähemmän aikaa menee kääntäjävaroitusten kanssa kamppailuun — ja miten paljon enemmän aikaa jää varsinaiseen kehittämiseen.