Swift Concurrency: Vodič za async/await, aktere i strukturiranu konkurentnost

Kompletni vodič za Swift Concurrency — od async/await osnova i aktora do TaskGroup-a, Sendable protokola i pristupačne konkurentnosti u Swiftu 6.2. S praktičnim primjerima koda.

Uvod u Swift Concurrency

Konkurentno programiranje u Swiftu doživjelo je, bez pretjerivanja, pravu revoluciju. Od Swifta 5.5, kada su nam stigli async/await, akteri i strukturirana konkurentnost, pa sve do Swifta 6.2 i koncepta "Approachable Concurrency" — Apple je iz temelja promijenio način na koji pišemo asinkroni kod. Ako ste do sad koristili completion handlere, GCD ili OperationQueue, vjerujte mi — ovo je potpuno drugi svijet.

U ovom vodiču proći ćemo kroz sve bitne koncepte Swift Concurrency sustava. Od osnova async/await sintakse, preko aktora i Sendable protokola, do naprednijih tema poput TaskGroup-a i najnovijih promjena u Swiftu 6.2. Sve s primjerima koda koje možete odmah primijeniti u svojim projektima.

Zašto je ovo uopće toliko važno? Pa, svaka moderna iOS aplikacija mora raditi s mrežnim pozivima, bazama podataka, procesiranjem slika i hrpom drugih dugotrajnih operacija — a sve to bez zamrzavanja UI-ja. Swift Concurrency nam daje alate za to na siguran, čitljiv i (konačno) elegantan način.

Problemi starog pristupa: completion handleri i GCD

Prije nego zaronimo u novi sustav, bacimo pogled na to zašto je uopće bio potreban. Stari pristup asinkronom programiranju imao je ozbiljne mane koje su frustrirale developere godinama. I iskreno, svi smo barem jednom htjeli razbiti tipkovnicu zbog toga.

Callback hell

Completion handleri su bili standardni način za rukovanje asinkronim operacijama. Problem? Gnijezdenje. Svaki asinkroni poziv zahtijevao je closure, a kad bi trebali izvršiti više operacija sekvencijalno, kod je postajao nečitljiv:

// Stari pristup s completion handlerima - "callback hell"
func dohvatiKorisnikaProfil(korisnikId: String) {
    dohvatiKorisnika(id: korisnikId) { rezultat in
        switch rezultat {
        case .success(let korisnik):
            dohvatiPostove(zaKorisnika: korisnik.id) { rezultatPostova in
                switch rezultatPostova {
                case .success(let postovi):
                    dohvatiKomentare(zaPostove: postovi) { rezultatKomentara in
                        switch rezultatKomentara {
                        case .success(let komentari):
                            // Konačno imamo sve podatke...
                            DispatchQueue.main.async {
                                self.prikaziProfil(korisnik, postovi, komentari)
                            }
                        case .failure(let greska):
                            self.prikaziGresku(greska)
                        }
                    }
                case .failure(let greska):
                    self.prikaziGresku(greska)
                }
            }
        case .failure(let greska):
            self.prikaziGresku(greska)
        }
    }
}

Ovaj kod je teško čitati, teško održavati, i lako je zaboraviti pozvati completion handler na svim putanjama izvršenja. A upravo to — zaboravljeni completion handler — bio je izvor beskonačnih bugova koji su se teško pronalazili.

Problemi s GCD-om

Grand Central Dispatch je bio moćan alat, ali i prilično opasan. Ručno upravljanje redovima lako je vodilo do:

  • Data race uvjeta — dva threada istovremeno pristupaju istom podatku
  • Deadlockova — dva threada čekaju jedan drugoga
  • Thread explosion — prekomjerno stvaranje threadova koje iscrpljuje resurse
  • Teškog debugiranja — problemi s konkurentnošću su nedeterministički i gotovo ih je nemoguće pouzdano reproducirati

Kompajler nije mogao provjeriti ništa od toga u vrijeme prevođenja. Greške su se pojavljivale tek u produkciji, često pod sasvim specifičnim uvjetima opterećenja. Ugh.

Async/Await: Temelj moderne konkurentnosti

Ajmo na stvar. Ključna riječ async označava funkciju koja može privremeno pauzirati svoje izvršavanje, dok await označava mjesto gdje se ta pauza može dogoditi. Ovo je fundamentalna promjena u pristupu asinkronom programiranju.

Osnovna sintaksa

// Definiranje asinkrone funkcije
func dohvatiKorisnika(id: String) async throws -> Korisnik {
    let url = URL(string: "https://api.primjer.hr/korisnici/\(id)")!
    let (podaci, odgovor) = try await URLSession.shared.data(from: url)

    guard let httpOdgovor = odgovor as? HTTPURLResponse,
          httpOdgovor.statusCode == 200 else {
        throw MreznaGreska.neuspjesnoDohvacanje
    }

    return try JSONDecoder().decode(Korisnik.self, from: podaci)
}

// Pozivanje asinkrone funkcije
func ucitajProfil() async {
    do {
        let korisnik = try await dohvatiKorisnika(id: "12345")
        print("Dohvaćen korisnik: \(korisnik.ime)")
    } catch {
        print("Greška: \(error)")
    }
}

Primijetite koliko je ovo čišće od completion handler verzije. Kod izgleda sekvencijalno, lako se čita, a rukovanje greškama koristi standardni do-catch blok umjesto ugniježdenih switch naredbi. Razlika je, iskreno, nebo i zemlja.

Sekvencijalno izvršavanje

Pogledajmo kako onaj kaotični primjer s početka izgleda s async/await:

// Isti zadatak s async/await - čist i čitljiv kod
func dohvatiKorisnikaProfil(korisnikId: String) async throws {
    let korisnik = try await dohvatiKorisnika(id: korisnikId)
    let postovi = try await dohvatiPostove(zaKorisnika: korisnik.id)
    let komentari = try await dohvatiKomentare(zaPostove: postovi)

    await MainActor.run {
        prikaziProfil(korisnik, postovi, komentari)
    }
}

Šest linija umjesto trideset. Nema gnijezdenja. Nema ponovljenog rukovanja greškama. Kod je linearan i čitljiv kao da je sinkron.

Paralelno izvršavanje s async let

Kada operacije ne ovise jedna o drugoj, možemo ih pokrenuti paralelno koristeći async let:

func ucitajNadzornuPlocu() async throws -> NadzornaPloca {
    // Sve tri operacije se pokreću paralelno
    async let korisnik = dohvatiKorisnika(id: "12345")
    async let obavijesti = dohvatiObavijesti()
    async let statistika = dohvatiStatistiku()

    // Čekamo rezultate svih operacija
    return try await NadzornaPloca(
        korisnik: korisnik,
        obavijesti: obavijesti,
        statistika: statistika
    )
}

Ovdje se sve tri funkcije izvršavaju istovremeno. Runtime automatski upravlja threadovima — nema ručnog stvaranja redova ili dispatch grupa. I ako bilo koja operacija baci grešku, ostale se automatski otkazuju. Lijepo, zar ne?

Task: Pokretanje asinkronog koda

Task je most između sinkronog i asinkronog svijeta. Kad trebate pokrenuti asinkroni kod iz sinkronog konteksta (npr. iz viewDidLoad ili akcije gumba), Task je vaš prijatelj:

class ProfilViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        // Pokretanje asinkronog koda iz sinkronog konteksta
        Task {
            do {
                let korisnik = try await dohvatiKorisnika(id: "12345")
                // Automatski nasljeđuje MainActor izolaciju od UIViewController
                imeLabela.text = korisnik.ime
                emailLabela.text = korisnik.email
            } catch {
                prikaziGresku(error)
            }
        }
    }
}

Task prioriteti

Taskovi mogu imati različite prioritete koji utječu na redoslijed izvršavanja:

// Visoki prioritet - za operacije koje korisnik čeka
Task(priority: .high) {
    await ucitajGlavniSadrzaj()
}

// Nizak prioritet - za pozadinske operacije
Task(priority: .low) {
    await sinkronizirajPodatke()
}

// Odvojeni task - nema vezu s roditeljskim kontekstom
Task.detached(priority: .background) {
    await ocistiPredmemoriju()
}

Otkazivanje taskova

Svaki task se može otkazati, što je ključno za upravljanje resursima. Evo jednog uzorka koji sam osobno koristio nebrojeno puta — debounce za pretragu u stvarnom vremenu:

class PretragaViewController: UIViewController {
    private var pretragaTask: Task?

    func korisnikJeUpisao(tekst: String) {
        // Otkaži prethodni task pretrage
        pretragaTask?.cancel()

        pretragaTask = Task {
            // Pričekaj malo prije slanja zahtjeva (debounce)
            try? await Task.sleep(for: .milliseconds(300))

            // Provjeri je li task otkazan
            guard !Task.isCancelled else { return }

            do {
                let rezultati = try await pretrazi(tekst)
                prikaziRezultate(rezultati)
            } catch {
                if !(error is CancellationError) {
                    prikaziGresku(error)
                }
            }
        }
    }
}

Svaki novi unos korisnika otkazuje prethodni zahtjev i pokreće novi nakon kratke pauze. Jednostavno i učinkovito.

Strukturirana konkurentnost: TaskGroup

Dok async let radi odlično za fiksni broj paralelnih operacija, TaskGroup je dizajniran za situacije kad unaprijed ne znate koliko zadataka imate. Zamislite da trebate dohvatiti podatke za niz korisnika ili preuzeti hrpu slika — upravo za to je TaskGroup tu.

Osnove TaskGroup-a

func dohvatiSveKorisnike(idovi: [String]) async throws -> [Korisnik] {
    try await withThrowingTaskGroup(of: Korisnik.self) { grupa in
        for id in idovi {
            grupa.addTask {
                try await self.dohvatiKorisnika(id: id)
            }
        }

        var korisnici: [Korisnik] = []
        for try await korisnik in grupa {
            korisnici.append(korisnik)
        }
        return korisnici
    }
}

Par ključnih stvari ovdje:

  • withThrowingTaskGroup stvara grupu zadataka koji mogu baciti grešku
  • Svaki addTask dodaje novi paralelni zadatak
  • Iteracija kroz grupu (for try await) čeka rezultate kako pristižu
  • Ako bilo koji zadatak baci grešku, svi ostali se automatski otkazuju
  • Nijedan child task ne može preživjeti opseg zatvaranja — to je ta "strukturiranost"

Ograničavanje broja istovremenih zadataka

U praksi ćete često morati ograničiti koliko paralelnih operacija pokrećete. Recimo, ako dohvaćate 100 slika, definitivno ne želite poslati 100 mrežnih zahtjeva odjednom:

func preuzimanjeSlika(urlovi: [URL]) async throws -> [UIImage] {
    let maksimalnoIstovremeno = 5

    return try await withThrowingTaskGroup(of: (Int, UIImage).self) { grupa in
        var slike: [UIImage?] = Array(repeating: nil, count: urlovi.count)
        var indeks = 0

        // Pokreni početni set zadataka
        for _ in 0..

Ovaj uzorak osigurava da nikad nema više od 5 istovremenih zahtjeva, a ipak maksimalno koristi dostupne resurse. Win-win.

Akteri: Zaštita dijeljenog stanja

Akteri su potpuno novi referentni tip u Swiftu, dizajnirani za siguran pristup dijeljenom promjenjivom stanju. Akter automatski serijalizira pristup svim svojim svojstvima i metodama, eliminirajući data race uvjete u vrijeme prevođenja. Mislim da je ovo jedna od najelegantnijih stvari u cijelom sustavu.

Definiranje aktera

actor BrojacPreuzimanja {
    private var brojPreuzimanja: [URL: Int] = [:]
    private var ukupnoPreuzeto: Int = 0

    func zabiljeziPreuzimanje(url: URL) {
        brojPreuzimanja[url, default: 0] += 1
        ukupnoPreuzeto += 1
    }

    func dohvatiBrojPreuzimanja(url: URL) -> Int {
        brojPreuzimanja[url] ?? 0
    }

    func dohvatiUkupno() -> Int {
        ukupnoPreuzeto
    }
}

// Korištenje aktera - pristup zahtijeva await
let brojac = BrojacPreuzimanja()

Task {
    await brojac.zabiljeziPreuzimanje(url: nekilURL)
    let ukupno = await brojac.dohvatiUkupno()
    print("Ukupno preuzeto: \(ukupno)")
}

Svaki poziv metode aktera izvana zahtijeva await. Razlog je jednostavan — akter serijalizira pristup, pa ako drugi task već pristupa akteru, naš poziv mora pričekati. Unutar samog aktera, metode mogu slobodno pristupati svojstvima bez await.

Izolacija aktera

Kompajler strogo provodi izolaciju aktera. Pokušaj direktnog pristupa svojstvima izvana rezultira greškom pri prevođenju:

actor Spremnik {
    var podaci: [String] = []

    func dodaj(_ element: String) {
        podaci.append(element)
    }
}

let spremnik = Spremnik()

// GREŠKA: Actor-isolated property 'podaci' can not be
// referenced from a non-isolated context
// let p = spremnik.podaci

// ISPRAVNO: Koristimo await
Task {
    await spremnik.dodaj("test")
}

Ovo je ogromna prednost u odnosu na stari sustav. Kompajler sam otkriva potencijalne data race uvjete i sprječava ih prije nego što se kod uopće pokrene. Manje bugova u produkciji, manje stresa za vas.

nonisolated metode

Ponekad akter ima svojstva ili metode kojima zaštita izolacije zapravo nije potrebna. Za to koristimo nonisolated:

actor KorisnikServis {
    let bazniURL: URL  // Nepromjenjivo - sigurno za dijeljenje
    var predmemorija: [String: Korisnik] = [:]

    init(bazniURL: URL) {
        self.bazniURL = bazniURL
    }

    // nonisolated jer pristupa samo nepromjenjivom svojstvu
    nonisolated func stvoriURL(zaKorisnika id: String) -> URL {
        bazniURL.appendingPathComponent("korisnici/\(id)")
    }

    func dohvati(id: String) async throws -> Korisnik {
        if let predmemorirani = predmemorija[id] {
            return predmemorirani
        }

        let url = stvoriURL(zaKorisnika: id)
        let (podaci, _) = try await URLSession.shared.data(from: url)
        let korisnik = try JSONDecoder().decode(Korisnik.self, from: podaci)
        predmemorija[id] = korisnik
        return korisnik
    }
}

MainActor: Rad s korisničkim sučeljem

@MainActor je globalni akter koji osigurava da se kod izvršava na glavnoj niti — a to je nužno za ažuriranje UI elemenata. Svi koji su ikad dobili onu poznatu ljubičastu upozorenje u Xcodeu znaju o čemu pričam.

Označavanje klasa i funkcija

// Cijela klasa izolirana na MainActor
@MainActor
class ProfilViewModel {
    var ime: String = ""
    var email: String = ""
    var ucitava: Bool = false
    var poruka: String?

    func ucitajProfil(id: String) async {
        ucitava = true
        poruka = nil

        do {
            let korisnik = try await KorisnikServis.shared.dohvati(id: id)
            // Sigurno ažuriramo UI svojstva - garantirano smo na glavnoj niti
            ime = korisnik.ime
            email = korisnik.email
        } catch {
            poruka = "Greška pri učitavanju: \(error.localizedDescription)"
        }

        ucitava = false
    }
}

Selektivna izolacija

Ne morate izolirati cijelu klasu. Možete označiti samo pojedine metode ili svojstva, što je korisno kad imate miks UI i pozadinske logike:

class ObradaSlike {
    // Obrada se izvršava na pozadinskoj niti
    func primijeniFiltre(na sliku: UIImage) async -> UIImage {
        // Skupa operacija - ne želimo blokirati UI
        let obradena = await primijeniGaussianBlur(sliku)
        let konacna = await primijeniVinjetu(obradena)
        return konacna
    }

    // Samo prikaz rezultata ide na glavnu nit
    @MainActor
    func prikaziRezultat(_ slika: UIImage, u imageView: UIImageView) {
        imageView.image = slika
    }
}

Sendable protokol: Sigurnost prijenosa podataka

Sendable je protokol-oznaka koji govori kompajleru da se tip može sigurno prenositi između različitih konteksta konkurentnosti. Zvuči apstraktno, ali u praksi je ovo temelj Swift-ove zaštite od data race bugova.

Koji tipovi su automatski Sendable?

  • Tipovi vrijednosti (struct, enum) čija su sva svojstva također Sendable
  • Akteri — uvijek Sendable jer imaju ugrađenu izolaciju
  • Nepromjenjive klase — klase kojima su sva svojstva let
  • Osnovni tipovi — Int, String, Bool, Double i slični
// Automatski Sendable - sva svojstva su value tipovi
struct Poruka: Sendable {
    let id: UUID
    let tekst: String
    let datum: Date
}

// Klasa mora eksplicitno konformirati Sendable
// i mora biti 'final' s nepromjenjivim svojstvima
final class Konfiguracija: Sendable {
    let apiKljuc: String
    let bazniURL: URL

    init(apiKljuc: String, bazniURL: URL) {
        self.apiKljuc = apiKljuc
        self.bazniURL = bazniURL
    }
}

// NEĆE SE PREVESTI - 'var' svojstvo u Sendable klasi
// final class NeispravaKonfig: Sendable {
//     var apiKljuc: String  // Greška!
// }

@Sendable zatvaranja

Zatvaranja koja se prosljeđuju između konteksta konkurentnosti moraju biti označena kao @Sendable. Ovo je nešto na što ćete se brzo naviknuti:

func izvrsiNaPozadini(_ posao: @Sendable @escaping () async -> Void) {
    Task.detached {
        await posao()
    }
}

// Sendable zatvaranje ne može uhvatiti promjenjive reference
var brojac = 0
izvrsiNaPozadini {
    // GREŠKA: Mutation of captured var 'brojac' in
    // concurrently-executing code
    // brojac += 1
}

AsyncSequence i AsyncStream

AsyncSequence omogućuje iteraciju kroz niz vrijednosti koje pristižu asinkrono. Zamislite to kao asinkronu verziju Sequence protokola — izuzetno korisno za streamove podataka, praćenje lokacije, WebSocket poruke i slično.

AsyncStream za prilagođene sekvence

// Stvaranje prilagođenog async streama za praćenje lokacije
func streamLokacija() -> AsyncStream {
    AsyncStream { nastavak in
        let upravitelj = LokacijskiUpravitelj()

        upravitelj.naNovuLokaciju = { lokacija in
            nastavak.yield(lokacija)
        }

        nastavak.onTermination = { _ in
            upravitelj.zaustaviPracenje()
        }

        upravitelj.pokreniPracenje()
    }
}

// Korištenje streama
func pratiKorisnika() async {
    for await lokacija in streamLokacija() {
        await azurirajMapu(lokacija: lokacija)

        if lokacija.horizontalAccuracy < 10 {
            print("Precizna lokacija: \(lokacija.coordinate)")
        }
    }
}

Transformacije nad AsyncSequence

AsyncSequence podržava iste transformacije kao i obične sekvence, tako da se možete osjećati kao doma:

func pratiVaznePromjene() async {
    let stream = streamTemperatura()

    // Filtriraj, transformiraj i obradi
    for await upozorenje in stream
        .filter({ $0 > 38.0 })
        .map({ temp in "UPOZORENJE: Temperatura \(temp)°C!" })
        .prefix(10) {

        await posaljiObavijest(upozorenje)
    }
}

Globalni akteri: Vlastiti akteri za izolaciju

Osim @MainActor, možete definirati i vlastite globalne aktere za grupiranje operacija koje trebaju biti serijalizirane. Ovo je posebno korisno za stvari poput pristupa bazi podataka ili file sistemu:

// Definiranje vlastitog globalnog aktera za rad s bazom podataka
@globalActor
actor BazaPodatakaAktor {
    static let shared = BazaPodatakaAktor()
    private init() {}
}

// Korištenje globalnog aktera
@BazaPodatakaAktor
class SpremisteKorisnika {
    private var baza: SQLiteBaza

    init(putanja: String) {
        baza = SQLiteBaza(putanja: putanja)
    }

    func spremi(_ korisnik: Korisnik) throws {
        try baza.izvrsi(
            "INSERT INTO korisnici (id, ime, email) VALUES (?, ?, ?)",
            parametri: [korisnik.id, korisnik.ime, korisnik.email]
        )
    }

    func dohvatiSve() throws -> [Korisnik] {
        try baza.upit("SELECT * FROM korisnici")
    }
}

// Svi pozivi su automatski serijalizirani na BazaPodatakaAktor
Task {
    let spremiste = SpremisteKorisnika(putanja: "/putanja/do/baze.db")
    try await spremiste.spremi(noviKorisnik)
    let sviKorisnici = try await spremiste.dohvatiSve()
}

Ovaj uzorak osigurava da se svi pristupi bazi serijaliziraju, sprječavajući istovremene upise koji bi mogli korumpirati podatke. U praksi, ovo eliminira čitavu kategoriju bugova.

Swift 6 i stroga provjera konkurentnosti

Swift 6 donosi punu provjeru data race sigurnosti u vrijeme prevođenja. Ovo je možda i najvažnija promjena u cijeloj priči — kompajler sada može garantirati da vaš kod nema data race uvjete. Zvuči gotovo predobro da bi bilo istinito, ali stvarno funkcionira.

Što se mijenja?

U Swift 6 jezičnom modu, kompajler strogo provjerava:

  • Svaki prijenos podataka između konteksta konkurentnosti mora koristiti Sendable tipove
  • Promjenjivo dijeljeno stanje mora biti zaštićeno akterom ili drugom sinkronizacijom
  • Funkcije i svojstva moraju eksplicitno deklarirati izolaciju aktera
  • Zatvaranja koja prelaze granice konkurentnosti moraju biti @Sendable

Postupna migracija

Dobra vijest — ne morate sve migrirati odjednom. Možete uključiti strogu provjeru kao upozorenja prije nego potpuno prijeđete na Swift 6 mod:

// U Package.swift
.target(
    name: "MojaAplikacija",
    swiftSettings: [
        .enableUpcomingFeature("StrictConcurrency")
    ]
)

// Ili u Xcode build settings:
// SWIFT_STRICT_CONCURRENCY = complete

Preporuka je migrirati modul po modul. Počnite s manjim modulima koji imaju manje ovisnosti, pa se krećite prema većima. Tako ćete izbjeći lavinu grešaka koja može biti prilično obeshrabrujuća.

Swift 6.2: Pristupačna konkurentnost

Swift 6.2, objavljen uz WWDC 2025, donosi koncept "Approachable Concurrency" — pristupačne konkurentnosti. Cilj je smanjiti trenje pri pisanju konkurentnog koda i učiniti data race sigurnost prirodnijim dijelom svakodnevnog rada.

Zadana MainActor izolacija

Najveća promjena u Swiftu 6.2 je mogućnost postavljanja zadane izolacije na @MainActor za cijeli modul. Svaki novi projekt kreiran u Xcodeu 26 imat će opciju "Default Actor Isolation" postavljenu na "MainActor":

// U Xcode 26, s uključenom MainActor izolacijom,
// ovaj kod je automatski izoliran na MainActor

class MojViewModel {
    var ime: String = ""       // Automatski @MainActor

    func ucitaj() async {      // Automatski @MainActor
        ime = "Ana"
    }
}

// Nema potrebe za eksplicitnim @MainActor!

Ovo eliminira jednu od najčešćih frustracija — zaboravljanje dodati @MainActor na UI kod. S ovom postavkom, sav kod u modulu je automatski izoliran na glavnoj niti osim ako eksplicitno kažete drugačije. Manje boilerplatea, manje grešaka.

nonisolated(nonsending): Kontekst pozivatelja

U ranijim verzijama Swifta, nonisolated async metode su se automatski prebacivale na globalni izvršitelj (tj. pozadinsku nit). U Swiftu 6.2, nonisolated(nonsending) osigurava da funkcija ostaje u kontekstu pozivatelja:

class ObradaPodataka {
    // Ova funkcija se izvršava u kontekstu pozivatelja
    // Ako je pozovete s MainActor-a, izvršit će se na glavnoj niti
    nonisolated(nonsending) func obradi(_ tekst: String) async -> String {
        // Obrada teksta...
        return tekst.uppercased()
    }
}

// Poziv s MainActor-a - izvršava se na glavnoj niti
@MainActor
func primjer() async {
    let obrada = ObradaPodataka()
    let rezultat = await obrada.obradi("test")
    // Nikada nismo napustili glavnu nit
}

@concurrent atribut

Novi @concurrent atribut jasno označava da se funkcija treba izvršavati na pozadinskoj niti, čak i kad je pozvana s MainActora:

@MainActor
class SlikaViewModel {
    var slika: UIImage?

    // Eksplicitno označeno za izvršavanje u pozadini
    @concurrent
    func primijeniFiltar(na izvornuSliku: UIImage) async -> UIImage {
        // Ova obrada se uvijek izvršava u pozadini,
        // bez obzira odakle je pozvana
        let ciFilter = CIFilter(name: "CIGaussianBlur")!
        ciFilter.setValue(CIImage(image: izvornuSliku), forKey: kCIInputImageKey)
        ciFilter.setValue(10.0, forKey: kCIInputRadiusKey)

        let kontekst = CIContext()
        let izlaz = ciFilter.outputImage!
        let cgSlika = kontekst.createCGImage(izlaz, from: izlaz.extent)!
        return UIImage(cgImage: cgSlika)
    }

    func ucitajIPrimijeniFiltar() async {
        let izvorna = UIImage(named: "fotografija")!
        // primijeniFiltar se izvršava u pozadini zahvaljujući @concurrent
        let filtrirana = await primijeniFiltar(na: izvorna)
        // Natrag smo na MainActor-u
        slika = filtrirana
    }
}

Ključna razlika između @concurrent i starog nonisolated? Jasnoća namjere. S @concurrent, eksplicitno komunicirate da ova operacija ide u pozadinu. Funkcija označena s @concurrent automatski postaje nonisolated, tako da ne trebate navesti oboje.

Pristupačna konkurentnost u praksi

Evo kako izgleda tipični SwiftUI pogled s postavkama Swifta 6.2 — primijetit ćete koliko manje ceremonije ima:

// S uključenom zadanom MainActor izolacijom u Xcode 26
import SwiftUI
import Observation

@Observable
class PopisZadatakaViewModel {
    var zadaci: [Zadatak] = []
    var ucitava = false
    var porukaGreske: String?

    // Automatski @MainActor zahvaljujući zadanoj izolaciji modula
    func ucitajZadatke() async {
        ucitava = true
        defer { ucitava = false }

        do {
            zadaci = try await ZadatakServis.shared.dohvatiSve()
        } catch {
            porukaGreske = error.localizedDescription
        }
    }

    // Teška obrada ide u pozadinu
    @concurrent
    func izvezi() async throws -> Data {
        let encoder = JSONEncoder()
        encoder.outputFormatting = .prettyPrinted
        return try encoder.encode(zadaci)
    }
}

struct PopisZadatakaView: View {
    @State private var viewModel = PopisZadatakaViewModel()

    var body: some View {
        NavigationStack {
            Group {
                if viewModel.ucitava {
                    ProgressView("Učitavanje...")
                } else {
                    List(viewModel.zadaci) { zadatak in
                        ZadatakRedak(zadatak: zadatak)
                    }
                }
            }
            .navigationTitle("Moji zadaci")
            .task {
                await viewModel.ucitajZadatke()
            }
        }
    }
}

Praktični uzorci i najbolje prakse

Sad kad smo pokrili sve ključne koncepte, pogledajmo neke uzorke koji se stalno pojavljuju u stvarnim aplikacijama. Ovo su stvari koje ćete pisati iznova i iznova.

Mrežni sloj s async/await

enum MreznaGreska: Error {
    case nevaljanURL
    case nevaljanOdgovor(statusKod: Int)
    case dekodiranje(Error)
}

actor MrezniKlijent {
    private let sesija: URLSession
    private let dekoder: JSONDecoder

    init() {
        let konfiguracija = URLSessionConfiguration.default
        konfiguracija.timeoutIntervalForRequest = 30
        sesija = URLSession(configuration: konfiguracija)
        dekoder = JSONDecoder()
        dekoder.dateDecodingStrategy = .iso8601
    }

    func dohvati(
        _ tip: T.Type,
        url: URL,
        zaglavlja: [String: String] = [:]
    ) async throws -> T {
        var zahtjev = URLRequest(url: url)
        zaglavlja.forEach { zahtjev.setValue($1, forHTTPHeaderField: $0) }

        let (podaci, odgovor) = try await sesija.data(for: zahtjev)

        guard let httpOdgovor = odgovor as? HTTPURLResponse else {
            throw MreznaGreska.nevaljanOdgovor(statusKod: 0)
        }

        guard (200...299).contains(httpOdgovor.statusCode) else {
            throw MreznaGreska.nevaljanOdgovor(statusKod: httpOdgovor.statusCode)
        }

        do {
            return try dekoder.decode(T.self, from: podaci)
        } catch {
            throw MreznaGreska.dekodiranje(error)
        }
    }
}

Predmemorija s akterom

actor Predmemorija {
    private var spremiste: [Kljuc: UnosPredmemorije] = [:]
    private let maksimalnoTrajanje: TimeInterval

    struct UnosPredmemorije {
        let vrijednost: Vrijednost
        let datumStvaranja: Date
    }

    init(maksimalnoTrajanje: TimeInterval = 300) {
        self.maksimalnoTrajanje = maksimalnoTrajanje
    }

    func dohvati(_ kljuc: Kljuc) -> Vrijednost? {
        guard let unos = spremiste[kljuc] else { return nil }

        if Date().timeIntervalSince(unos.datumStvaranja) > maksimalnoTrajanje {
            spremiste.removeValue(forKey: kljuc)
            return nil
        }

        return unos.vrijednost
    }

    func spremi(_ vrijednost: Vrijednost, zaKljuc kljuc: Kljuc) {
        spremiste[kljuc] = UnosPredmemorije(
            vrijednost: vrijednost,
            datumStvaranja: Date()
        )
    }

    func ocisti() {
        let sada = Date()
        spremiste = spremiste.filter { _, unos in
            sada.timeIntervalSince(unos.datumStvaranja) <= maksimalnoTrajanje
        }
    }
}

// Korištenje
let predmemorija = Predmemorija(maksimalnoTrajanje: 600)

func dohvatiKorisnikaSPredmemorijom(id: String) async throws -> Korisnik {
    if let predmemorirani = await predmemorija.dohvati(id) {
        return predmemorirani
    }

    let korisnik = try await mrezniKlijent.dohvati(Korisnik.self, url: korisnikURL(id))
    await predmemorija.spremi(korisnik, zaKljuc: id)
    return korisnik
}

Debouncer s async/await

actor Debouncer {
    private var zadatak: Task?
    private let interval: Duration

    init(interval: Duration) {
        self.interval = interval
    }

    func izvrsi(_ operacija: @Sendable @escaping () async -> Void) {
        zadatak?.cancel()
        zadatak = Task {
            do {
                try await Task.sleep(for: interval)
                guard !Task.isCancelled else { return }
                await operacija()
            } catch {
                // Task je otkazan - to je očekivano ponašanje
            }
        }
    }
}

// Korištenje u SwiftUI
@Observable
@MainActor
class PretragaViewModel {
    var upit: String = "" {
        didSet {
            Task { await debouncer.izvrsi { [weak self] in
                guard let self else { return }
                await self.izvrsiPretragu()
            }}
        }
    }

    var rezultati: [Rezultat] = []
    private let debouncer = Debouncer(interval: .milliseconds(300))

    private func izvrsiPretragu() async {
        guard !upit.isEmpty else {
            rezultati = []
            return
        }

        do {
            rezultati = try await PretragaServis.shared.pretrazi(upit)
        } catch {
            print("Greška pretrage: \(error)")
        }
    }
}

Rukovanje greškama u konkurentnom kodu

Pravilno rukovanje greškama je ključno u asinkronom kodu. Mrežni zahtjevi padaju, serveri ne odgovaraju, korisnici gube vezu — moramo biti spremni na sve. Evo nekoliko korisnih uzoraka.

Pokušaj s ponovnim isprobavanjem (retry)

func dohvatiSPonovnimPokusajem(
    maksimalnoPokusaja: Int = 3,
    pocetnaOdgoda: Duration = .seconds(1),
    operacija: () async throws -> T
) async throws -> T {
    var zadnjaGreska: Error?
    var odgoda = pocetnaOdgoda

    for pokusaj in 1...maksimalnoPokusaja {
        do {
            return try await operacija()
        } catch {
            zadnjaGreska = error

            if pokusaj < maksimalnoPokusaja {
                try await Task.sleep(for: odgoda)
                odgoda *= 2  // Eksponencijalni backoff
            }
        }
    }

    throw zadnjaGreska!
}

// Korištenje
let korisnik = try await dohvatiSPonovnimPokusajem {
    try await mrezniKlijent.dohvati(Korisnik.self, url: url)
}

Timeout uzorak

func sTimeoutom(
    sekundi: Double,
    operacija: @Sendable () async throws -> T
) async throws -> T {
    try await withThrowingTaskGroup(of: T.self) { grupa in
        grupa.addTask {
            try await operacija()
        }

        grupa.addTask {
            try await Task.sleep(for: .seconds(sekundi))
            throw TimeoutGreska()
        }

        // Uzmi prvi rezultat (ili grešku)
        let rezultat = try await grupa.next()!
        grupa.cancelAll()
        return rezultat
    }
}

struct TimeoutGreska: Error {}

// Korištenje
do {
    let podaci = try await sTimeoutom(sekundi: 10) {
        try await dohvatiVelikePodatke()
    }
} catch is TimeoutGreska {
    print("Operacija je istekla nakon 10 sekundi")
}

Testiranje asinkronog koda

Na kraju, ali nikako manje važno — testiranje. Swift Testing framework nativno podržava asinkrone testove, pa je pisanje testova za konkurentni kod postalo znatno ugodnije:

import Testing

@Suite("Testovi mrežnog klijenta")
struct MrezniKlijentTestovi {

    @Test("Uspješno dohvaćanje korisnika")
    func dohvatiKorisnika() async throws {
        let klijent = MrezniKlijent()
        let korisnik = try await klijent.dohvati(
            Korisnik.self,
            url: URL(string: "https://api.test.hr/korisnici/1")!
        )

        #expect(korisnik.id == "1")
        #expect(!korisnik.ime.isEmpty)
    }

    @Test("Timeout baca grešku")
    func timeoutGreska() async {
        await #expect(throws: TimeoutGreska.self) {
            try await sTimeoutom(sekundi: 0.1) {
                try await Task.sleep(for: .seconds(10))
            }
        }
    }

    @Test("Predmemorija vraća spremljenu vrijednost")
    func predmemorijaTijekRada() async {
        let predmemorija = Predmemorija()

        await predmemorija.spremi("vrijednost", zaKljuc: "kljuc")
        let rezultat = await predmemorija.dohvati("kljuc")

        #expect(rezultat == "vrijednost")
    }
}

Zaključak

Swift Concurrency je fundamentalno promijenio način na koji pišemo asinkroni kod u Apple ekosustavu. Umjesto ručnog upravljanja threadovima i completion handlerima, sada imamo sustav koji je:

  • Siguran — kompajler otkriva data race uvjete u vrijeme prevođenja
  • Čitljiv — async/await čini asinkroni kod linearnim i jednostavnim za praćenje
  • Strukturiran — TaskGroup i async let osiguravaju pravilno upravljanje životnim ciklusom zadataka
  • Performantan — kooperativni thread pool efikasno koristi sistemske resurse

Evo ključnih preporuka za svakodnevni rad:

  1. Počnite s async/await — zamijenite completion handlere gdje god možete
  2. Koristite aktere za dijeljeno stanje — zaboravite na ručne DispatchQueue sinkronizacije
  3. Označite UI kod s @MainActor — ili uključite zadanu izolaciju u Swiftu 6.2
  4. Migrirajte postupno na Swift 6 — uključite stroge provjere kao upozorenja prije potpunog prelaska
  5. Koristite @concurrent za pozadinski rad — neka namjera koda bude jasna
  6. Testirajte asinkroni kod — Swift Testing framework to čini jednostavnim

Swift Concurrency nije samo nova sintaksa — to je promjena paradigme koja čini naše aplikacije sigurnijima i jednostavnijima za održavanje. A uz Swift 6.2, ta promjena postaje pristupačnija nego ikad prije.

O Autoru Editorial Team

Our team of expert writers and editors.