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:
withThrowingTaskGroupstvara grupu zadataka koji mogu baciti grešku- Svaki
addTaskdodaje 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
Sendabletipove - 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:
- Počnite s async/await — zamijenite completion handlere gdje god možete
- Koristite aktere za dijeljeno stanje — zaboravite na ručne DispatchQueue sinkronizacije
- Označite UI kod s @MainActor — ili uključite zadanu izolaciju u Swiftu 6.2
- Migrirajte postupno na Swift 6 — uključite stroge provjere kao upozorenja prije potpunog prelaska
- Koristite @concurrent za pozadinski rad — neka namjera koda bude jasna
- 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.