Swift Observation framework: Vodič za @Observable makro i reaktivno programiranje u SwiftUI

Sve o Swift Observation frameworku i @Observable makru — od migracije s ObservableObject, preko integracije sa SwiftUI i naprednih obrazaca, do optimizacije performansi i testiranja.

Uvod u Swift Observation framework

S iOS-om 17, macOS-om Sonoma i Xcode-om 15, Apple je predstavio Observation framework — potpuno novi pristup reaktivnom programiranju u Swiftu. I iskreno? Bio je krajnje vrijeme. Ovaj framework donosi temeljnu promjenu u načinu na koji upravljamo stanjem aplikacije i komuniciramo promjene podataka između modela i UI-a u SwiftUI-ju.

Srce svega je @Observable makro. On zamjenjuje stari ObservableObject protokol i donosi poboljšanja u performansama, čitljivosti koda i jednostavnosti korištenja. Umjesto da ručno označavate svaku varijablu s @Published, sad je dovoljno klasu označiti s @Observable — i to je to. Sustav automatski prati pristup svakom svojstvu.

Zašto je Apple uopće uveo ovaj framework? Pa, razloga je bilo više nego dovoljno.

Prvo, stari pristup s ObservableObject protokolom i Combineom imao je ozbiljne probleme s performansama. Svaka promjena bilo kojeg @Published svojstva uzrokovala je ponovni render svih pogleda koji su promatrali taj objekt — čak i ako nisu koristili promijenjeno svojstvo. Zamislite aplikaciju s 20 pogleda koji promatraju isti model. Promijenite jedno polje, a svih 20 se ponovno iscrtava. Užas.

Drugo, sintaksa je bila opterećena property wrapperima (@Published, @ObservedObject, @StateObject, @EnvironmentObject), što je stvaralo konfuziju — pogotovo za početnike koji su se tek upoznavali sa SwiftUI-jem.

I treće, Combine nikada nije dobio potpunu podršku za Swift concurrency. Observation framework je zato dizajniran od nule s podrškom za async/await i strukturiranu konkurentnost.

Hajde da u ovom vodiču detaljno prođemo sve aspekte Observation frameworka — od osnova, preko migracije, do naprednih obrazaca i optimizacije performansi.

Problemi s ObservableObject protokolom

Da bismo zaista razumjeli zašto je Observation framework toliko važan, moramo prvo pogledati što nije valjalo sa starim pristupom.

Nepotrebno ponovno iscrtavanje pogleda

Ovo je bio najkritičniji problem. Kada bi se bilo koje @Published svojstvo promijenilo, objectWillChange publisher je emitirao signal, a svi pretplaćeni pogledi su dobili obavijest — bez obzira na to koje se konkretno svojstvo promijenilo. Pogledajte ovaj primjer:

// Stari pristup s ObservableObject
class KorisnikModel: ObservableObject {
    @Published var ime: String = "Ana"
    @Published var prezime: String = "Kovačević"
    @Published var email: String = "[email protected]"
    @Published var brojPrijava: Int = 0
}

struct ImePrikaz: View {
    @ObservedObject var korisnik: KorisnikModel

    var body: some View {
        // Ovaj pogled se ponovno iscrtava čak i kada
        // se promijeni samo 'brojPrijava', iako ga uopće ne koristi!
        Text(korisnik.ime)
    }
}

struct BrojačPrijava: View {
    @ObservedObject var korisnik: KorisnikModel

    var body: some View {
        // Ovaj pogled se ponovno iscrtava i kada
        // se promijeni 'ime', iako prikazuje samo brojač
        Text("Prijave: \(korisnik.brojPrijava)")
    }
}

Dakle, svaka promjena bilo kojeg svojstva u KorisnikModel uzrokuje ponovni render oba pogleda. U velikim aplikacijama s kompleksnim modelima, ovo je vodilo do ozbiljnih problema s performansama — i frustracije developera.

Konfuzija oko property wrappera

Stari sustav zahtijevao je razumijevanje razlike između više property wrappera, a iskreno — znam da sam i sam više puta zamijenio krivog:

  • @StateObject — za stvaranje i posjedovanje instance observable objekta
  • @ObservedObject — za primanje reference na observable objekt izvana
  • @EnvironmentObject — za pristup observable objektu iz okruženja
  • @Published — za označavanje svojstava koja emitiraju promjene

Krivi odabir property wrappera često je vodio do suptilnih bugova. Na primjer, korištenje @ObservedObject umjesto @StateObject za objekt koji pogled sam stvara moglo je uzrokovati neočekivano ponašanje — objekt bi se ponovno inicijalizirao pri svakom ponovnom iscrtavanju. Klasična zamka.

Ovisnost o Combine frameworku

ObservableObject je bio duboko integriran s Combineom, pa je svaki model podataka implicitno ovisio o cijelom reaktivnom sustavu. A Combine nikad nije dobio punu podršku za Swift concurrency model (async/await, akteri, strukturirana konkurentnost), što je stvaralo nesklad između modernog Swift koda i starijeg reaktivnog pristupa.

Ograničenja s tipovima vrijednosti

ObservableObject je bio ograničen isključivo na klase. Nije postojao elegantan način za promatranje promjena u strukturama, što je često prisiljavalo developere da koriste klase tamo gdje bi struct bio puno prikladniji izbor.

@Observable makro — Kako funkcionira

Makro @Observable koristi Swift makro sustav (uveden u Swiftu 5.9) za automatsko generiranje koda koji prati pristup svojstvima i emitira obavijesti o promjenama. Ključna razlika? Observation framework prati pristup na razini pojedinog svojstva, ne cijelog objekta.

Osnovna sintaksa

Ovdje ćete vidjeti koliko je novi pristup čist i jednostavan:

import Observation

// Jednostavno označavanje klase s @Observable
@Observable
class KorisnikModel {
    var ime: String = "Ana"
    var prezime: String = "Kovačević"
    var email: String = "[email protected]"
    var brojPrijava: Int = 0
}

To je sve! Nema @Published, nema konformiranja protokolu, nema boilerplate koda. Makro automatski obrađuje sva pohranjena svojstva klase. Kad sam prvi put vidio koliko je to jednostavno, nisam mogao vjerovati.

Što makro generira pod haubom

Kad kompajler obradi @Observable makro, generira poprilično koda u pozadini. Evo pojednostavljenog prikaza:

// Ovo je približni prikaz koda koji makro generira
// (ne trebate ovo pisati ručno!)
class KorisnikModel {
    // Registar za praćenje pristupa svojstvima
    @ObservationIgnored
    private let _$observationRegistrar = ObservationRegistrar()

    // Izvorno svojstvo se pretvara u computed property
    // s pozivima za praćenje pristupa i mutacija
    @ObservationTracked
    var ime: String = "Ana" {
        get {
            // Obavijesti sustav da se pristupa ovom svojstvu
            access(keyPath: \.ime)
            return _ime
        }
        set {
            // Obavijesti sustav da se ovo svojstvo mijenja
            withMutation(keyPath: \.ime) {
                _ime = newValue
            }
        }
    }

    // Privatna varijabla za pohranu stvarne vrijednosti
    @ObservationIgnored
    private var _ime: String = "Ana"
}

// Konformiranje Observable protokolu
extension KorisnikModel: Observable { }

Ključni mehanizam funkcionira ovako: kad SwiftUI pogled pristupa svojstvu ime, sustav to registrira. Kad se svojstvo ime kasnije promijeni, sustav zna točno koji pogledi trebaju biti obaviješteni — samo oni koji su stvarno pristupili tom svojstvu. Pogledi koji koriste samo brojPrijava neće biti obaviješteni o promjeni imena. Elegantno, zar ne?

ObservationIgnored atribut

Ponekad imate svojstvo koje ne želite pratiti. Možda je to neki interni cache ili privremeni identifikator. Za to služi @ObservationIgnored:

@Observable
class DokumentModel {
    var naslov: String = ""          // Praćeno
    var sadržaj: String = ""         // Praćeno

    @ObservationIgnored
    var interniCache: [String: Any] = [:]  // NIJE praćeno

    @ObservationIgnored
    var privremeniID: UUID = UUID()        // NIJE praćeno
}

Svojstva označena s @ObservationIgnored se ponašaju kao obična svojstva — njihova promjena neće pokrenuti ažuriranje nijednog pogleda.

Funkcija withObservationTracking

Observation framework pruža i globalnu funkciju withObservationTracking za praćenje pristupa svojstvima izvan SwiftUI konteksta:

let korisnik = KorisnikModel()

// Praćenje pristupa svojstvima
withObservationTracking {
    // Sve što se pristupa unutar ovog bloka bit će praćeno
    print(korisnik.ime)
    print(korisnik.email)
} onChange: {
    // Ovaj blok se poziva kada se bilo koje
    // praćeno svojstvo promijeni (jednokratno!)
    print("Ime ili email su se promijenili!")
}

Jedna stvar koju morate zapamtiti: onChange blok se poziva samo jednom — nakon prve promjene bilo kojeg praćenog svojstva. Za kontinuirano praćenje, morate ponoviti poziv withObservationTracking. Ovo zna iznenaditi ljude prvi put.

Migracija s ObservableObject na @Observable

Dobra vijest — migracija je relativno jednostavna. Ali da bi sve prošlo glatko, treba imati sistematičan pristup. Evo vodiča korak po korak.

Korak 1: Transformacija modela podataka

Prvo transformiramo klasu modela. Promjene su minimalne — zapravo više brišete nego što dodajete:

// PRIJE: Stari pristup
class TrgovinaModel: ObservableObject {
    @Published var proizvodi: [Proizvod] = []
    @Published var košarica: [StavkaKošarice] = []
    @Published var učitavanje: Bool = false
    @Published var poruka: String? = nil

    func dohvatiProizvode() async {
        učitavanje = true
        defer { učitavanje = false }
        // ... dohvat podataka
    }
}

// POSLIJE: Novi pristup s @Observable
import Observation

@Observable
class TrgovinaModel {
    var proizvodi: [Proizvod] = []
    var košarica: [StavkaKošarice] = []
    var učitavanje: Bool = false
    var poruka: String? = nil

    func dohvatiProizvode() async {
        učitavanje = true
        defer { učitavanje = false }
        // ... dohvat podataka
    }
}

Uklonite ObservableObject konformiranje, maknite sve @Published atribute i dodajte @Observable makro. Gotovo.

Korak 2: Ažuriranje SwiftUI pogleda

Sljedeći korak je ažuriranje pogleda koji koriste model:

// PRIJE: Korištenje @StateObject i @ObservedObject
struct TrgovinaView: View {
    @StateObject private var model = TrgovinaModel()

    var body: some View {
        NavigationStack {
            ProizvodiLista(model: model)
        }
    }
}

struct ProizvodiLista: View {
    @ObservedObject var model: TrgovinaModel

    var body: some View {
        List(model.proizvodi) { proizvod in
            ProizvodRedak(proizvod: proizvod)
        }
    }
}

// POSLIJE: Korištenje @State i izravnog prosljeđivanja
struct TrgovinaView: View {
    @State private var model = TrgovinaModel()

    var body: some View {
        NavigationStack {
            ProizvodiLista(model: model)
        }
    }
}

struct ProizvodiLista: View {
    var model: TrgovinaModel  // Nema property wrappera!

    var body: some View {
        List(model.proizvodi) { proizvod in
            ProizvodRedak(proizvod: proizvod)
        }
    }
}

Primijetite razliku? ProizvodiLista sad prima model kao običnu varijablu. Bez wrappera, bez ceremonije.

Korak 3: Ažuriranje Environment pristupa

// PRIJE: @EnvironmentObject
struct PostavkeView: View {
    @EnvironmentObject var postavke: PostavkeModel

    var body: some View {
        Toggle("Tamni način", isOn: $postavke.tamniNačin)
    }
}

// Injektiranje u okolinu
ContentView()
    .environmentObject(postavke)

// POSLIJE: @Environment
struct PostavkeView: View {
    @Environment(PostavkeModel.self) var postavke

    var body: some View {
        @Bindable var postavke = postavke
        Toggle("Tamni način", isOn: $postavke.tamniNačin)
    }
}

// Injektiranje u okolinu
ContentView()
    .environment(postavke)

Obratite pažnju na par važnih promjena: @EnvironmentObject se zamjenjuje s @Environment(TipModel.self), a .environmentObject() s .environment(). Za dobivanje bindinga na svojstva, trebat ćete koristiti @Bindable lokalnu varijablu. Mali trik koji lako zaboravite.

Tablica migracije

Evo pregledne tablice zamjena za brzu referencu:

  • ObservableObject protokol → @Observable makro
  • @Published → obična var deklaracija
  • @StateObject@State
  • @ObservedObject → obična varijabla (bez property wrappera)
  • @EnvironmentObject@Environment(Tip.self)
  • .environmentObject(objekt).environment(objekt)

Integracija s SwiftUI

Observation framework je dizajniran za besprijekornu integraciju sa SwiftUI-jem. Pogledajmo kako se koristi u praksi.

Korištenje @State za lokalne modele

Kada pogled sam stvara i upravlja instancom observable objekta, koristite @State:

@Observable
class BrojačModel {
    var vrijednost: Int = 0
    var korak: Int = 1

    func povećaj() {
        vrijednost += korak
    }

    func smanji() {
        vrijednost -= korak
    }

    func resetiraj() {
        vrijednost = 0
    }
}

struct BrojačView: View {
    // @State osigurava da se instanca ne ponovno stvara
    // pri ponovnom iscrtavanju roditeljskog pogleda
    @State private var brojač = BrojačModel()

    var body: some View {
        VStack(spacing: 20) {
            Text("Vrijednost: \(brojač.vrijednost)")
                .font(.largeTitle)

            HStack(spacing: 16) {
                Button("- \(brojač.korak)") {
                    brojač.smanji()
                }

                Button("Resetiraj") {
                    brojač.resetiraj()
                }

                Button("+ \(brojač.korak)") {
                    brojač.povećaj()
                }
            }

            Stepper("Korak: \(brojač.korak)", value: $brojač.korak, in: 1...10)
        }
        .padding()
    }
}

Prosljeđivanje modela kao parametra

Kad prosljeđujete observable objekt podređenom pogledu, ne trebate nikakav property wrapper. Ozbiljno, ništa:

struct BrojačPrikazView: View {
    // Jednostavna varijabla - bez property wrappera!
    var brojač: BrojačModel

    var body: some View {
        // SwiftUI automatski prati koja svojstva se koriste
        Text("Trenutna vrijednost: \(brojač.vrijednost)")
            .font(.headline)
    }
}

SwiftUI automatski prati pristup svojstvima unutar body computed propertyja. Pogled BrojačPrikazView će se ponovno iscrtati samo kad se promijeni brojač.vrijednost, ne i kad se promijeni brojač.korak. Upravo ta preciznost čini Observation framework toliko moćnim.

Korištenje @Environment za dijeljene modele

Za modele koji trebaju biti dostupni kroz cijelu hijerarhiju pogleda, koristite @Environment:

@Observable
class AplikacijskoStanje {
    var prijavljeniKorisnik: Korisnik? = nil
    var tema: Tema = .svijetla
    var jezik: String = "hr"

    var jePrijavljen: Bool {
        prijavljeniKorisnik != nil
    }
}

// Korijenski pogled injektira stanje u okolinu
@main
struct MojaAplikacija: App {
    @State private var stanje = AplikacijskoStanje()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(stanje)
        }
    }
}

// Bilo koji pogled u hijerarhiji može pristupiti stanju
struct ProfilView: View {
    @Environment(AplikacijskoStanje.self) var stanje

    var body: some View {
        if let korisnik = stanje.prijavljeniKorisnik {
            VStack {
                Text("Dobrodošli, \(korisnik.ime)!")
                Text("Jezik: \(stanje.jezik)")
            }
        } else {
            Text("Niste prijavljeni")
        }
    }
}

Korištenje @Bindable za dvosmjerno vezivanje

@Bindable je novi property wrapper koji omogućuje stvaranje bindinga ($ sintaksa) na svojstva observable objekata. Ovo je jedan od onih detalja koji čine svakodnevni rad s Observation frameworkom zaista ugodnim:

@Observable
class FormularModel {
    var ime: String = ""
    var email: String = ""
    var prihvaćamUvjete: Bool = false
    var odabranaZemlja: String = "Hrvatska"

    var jeValidan: Bool {
        !ime.isEmpty && email.contains("@") && prihvaćamUvjete
    }
}

struct RegistracijaView: View {
    @State private var formular = FormularModel()

    var body: some View {
        Form {
            Section("Osobni podaci") {
                TextField("Ime", text: $formular.ime)
                TextField("Email", text: $formular.email)
            }

            Section("Postavke") {
                Picker("Zemlja", selection: $formular.odabranaZemlja) {
                    Text("Hrvatska").tag("Hrvatska")
                    Text("Slovenija").tag("Slovenija")
                    Text("Srbija").tag("Srbija")
                }

                Toggle("Prihvaćam uvjete", isOn: $formular.prihvaćamUvjete)
            }

            Button("Registriraj se") {
                // Obrada registracije
            }
            .disabled(!formular.jeValidan)
        }
    }
}

// Ako model dolazi izvana (nije @State), koristimo @Bindable
struct UređivanjeFormularaView: View {
    @Bindable var formular: FormularModel

    var body: some View {
        TextField("Ime", text: $formular.ime)
        TextField("Email", text: $formular.email)
    }
}

// Ili kao lokalna varijabla za @Environment modele
struct PostavkeFormularView: View {
    @Environment(FormularModel.self) var formular

    var body: some View {
        @Bindable var formular = formular
        TextField("Ime", text: $formular.ime)
    }
}

Napredni obrasci

Sad dolazimo do zanimljivog dijela. Observation framework podržava mnoge napredne obrasce koji su bili teški ili gotovo nemogući s ObservableObject protokolom.

Računalna (computed) svojstva

Jedna od stvari koja me najviše oduševila je automatska podrška za computed svojstva. Framework sam prati pristup pohranjenim svojstvima koja se koriste unutar računalnih svojstava:

@Observable
class NarudžbaModel {
    var stavke: [Stavka] = []
    var popust: Double = 0.0
    var poreznaStopa: Double = 0.25

    // Računalno svojstvo - automatski se ažurira
    // kada se promijene 'stavke', 'popust' ili 'poreznaStopa'
    var ukupnaCijena: Double {
        let međuzbroj = stavke.reduce(0) { $0 + $1.cijena * Double($1.količina) }
        let cijenaSPopustom = međuzbroj * (1.0 - popust)
        let porez = cijenaSPopustom * poreznaStopa
        return cijenaSPopustom + porez
    }

    var brojStavki: Int {
        stavke.reduce(0) { $0 + $1.količina }
    }

    var jePrazna: Bool {
        stavke.isEmpty
    }
}

struct NarudžbaPregled: View {
    var narudžba: NarudžbaModel

    var body: some View {
        VStack {
            // Ovaj pogled se ažurira samo kada se promijene
            // svojstva koja utječu na 'ukupnaCijena'
            Text("Ukupno: \(narudžba.ukupnaCijena, specifier: "%.2f") €")
            Text("Broj stavki: \(narudžba.brojStavki)")
        }
    }
}

S ObservableObject bi za ovo morali ručno upravljati ovisnostima. Ovdje to radi automatski.

Ugniježđeni observable objekti

Rad s ugniježđenim observable objektima je još jedno područje gdje Observation framework zaista blista:

@Observable
class Adresa {
    var ulica: String = ""
    var grad: String = ""
    var poštanskiBroj: String = ""
    var država: String = "Hrvatska"

    var potpunaAdresa: String {
        "\(ulica), \(poštanskiBroj) \(grad), \(država)"
    }
}

@Observable
class Osoba {
    var ime: String = ""
    var prezime: String = ""
    var adresa: Adresa = Adresa()  // Ugniježđeni observable objekt

    var punoIme: String {
        "\(ime) \(prezime)"
    }
}

struct OsobaView: View {
    @State private var osoba = Osoba()

    var body: some View {
        Form {
            Section("Ime") {
                TextField("Ime", text: $osoba.ime)
                TextField("Prezime", text: $osoba.prezime)
            }

            Section("Adresa") {
                // SwiftUI prati pristup kroz ugniježđene objekte
                TextField("Ulica", text: $osoba.adresa.ulica)
                TextField("Grad", text: $osoba.adresa.grad)
                TextField("Poštanski broj", text: $osoba.adresa.poštanskiBroj)
            }

            Section("Pregled") {
                Text(osoba.punoIme)
                Text(osoba.adresa.potpunaAdresa)
            }
        }
    }
}

SwiftUI prati promjene kroz lanac ugniježđenih observable objekata. Kad se promijeni osoba.adresa.grad, samo pogledi koji pristupaju tom svojstvu se ponovno iscrtavaju. Nema nepotrebnog renderiranja.

Kolekcije observable objekata

Rad s kolekcijama observable objekata zahtijeva malo pažnje, ali rezultat je elegantan. Evo kompletnog primjera s listom zadataka:

@Observable
class Zadatak: Identifiable {
    let id = UUID()
    var naslov: String
    var jeZavršen: Bool = false
    var prioritet: Prioritet = .srednji

    init(naslov: String) {
        self.naslov = naslov
    }
}

enum Prioritet: String, CaseIterable {
    case nizak = "Nizak"
    case srednji = "Srednji"
    case visok = "Visok"
}

@Observable
class ZadaciModel {
    var zadaci: [Zadatak] = []

    // Računalna svojstva za filtriranje
    var nezavršeniZadaci: [Zadatak] {
        zadaci.filter { !$0.jeZavršen }
    }

    var završeniZadaci: [Zadatak] {
        zadaci.filter { $0.jeZavršen }
    }

    var postotakZavršenosti: Double {
        guard !zadaci.isEmpty else { return 0 }
        let završeni = Double(završeniZadaci.count)
        return završeni / Double(zadaci.count) * 100
    }

    func dodajZadatak(_ naslov: String) {
        zadaci.append(Zadatak(naslov: naslov))
    }

    func ukloniZadatak(_ zadatak: Zadatak) {
        zadaci.removeAll { $0.id == zadatak.id }
    }
}

struct ZadaciView: View {
    @State private var model = ZadaciModel()
    @State private var noviNaslov = ""

    var body: some View {
        NavigationStack {
            List {
                Section("Za napraviti (\(model.nezavršeniZadaci.count))") {
                    ForEach(model.nezavršeniZadaci) { zadatak in
                        ZadatakRedak(zadatak: zadatak)
                    }
                }

                Section("Završeno (\(model.završeniZadaci.count))") {
                    ForEach(model.završeniZadaci) { zadatak in
                        ZadatakRedak(zadatak: zadatak)
                    }
                }
            }
            .navigationTitle("Zadaci")
            .toolbar {
                ToolbarItem(placement: .bottomBar) {
                    HStack {
                        TextField("Novi zadatak", text: $noviNaslov)
                        Button("Dodaj") {
                            model.dodajZadatak(noviNaslov)
                            noviNaslov = ""
                        }
                        .disabled(noviNaslov.isEmpty)
                    }
                }
            }
        }
    }
}

struct ZadatakRedak: View {
    @Bindable var zadatak: Zadatak

    var body: some View {
        HStack {
            // Samo ovaj redak se ažurira kada se promijeni
            // stanje konkretnog zadatka
            Toggle(isOn: $zadatak.jeZavršen) {
                Text(zadatak.naslov)
                    .strikethrough(zadatak.jeZavršen)
            }

            Spacer()

            Text(zadatak.prioritet.rawValue)
                .font(.caption)
                .foregroundStyle(.secondary)
        }
    }
}

Ovo je zaista lijepo rješenje — svaki redak u listi neovisno prati promjene svog Zadatak objekta. Kad označite jedan zadatak kao završen, samo se taj redak i zaglavlja sekcija ažuriraju. Ostatak liste ostaje netaknut.

TaskGroup i Observation — Kombiniranje konkurentnosti s promatranjem

Observation framework se izvrsno slaže sa Swiftovim modelom strukturirane konkurentnosti. Pogledajmo kako kombinirati TaskGroup, async/await i observable objekte u praksi.

Asinkrono dohvaćanje podataka

@Observable
class VijestiFeed {
    var vijesti: [Vijest] = []
    var kategorije: [Kategorija] = []
    var učitavanje: Bool = false
    var greška: String? = nil

    // Paralelno dohvaćanje podataka s TaskGroup
    func dohvatiSvePodatke() async {
        učitavanje = true
        greška = nil

        defer { učitavanje = false }

        do {
            // Koristimo TaskGroup za paralelno dohvaćanje
            await withTaskGroup(of: Void.self) { grupa in
                // Zadatak za dohvat vijesti
                grupa.addTask {
                    do {
                        let dohvaćeneVijesti = try await self.dohvatiVijesti()
                        await MainActor.run {
                            self.vijesti = dohvaćeneVijesti
                        }
                    } catch {
                        await MainActor.run {
                            self.greška = "Greška pri dohvatu vijesti: \(error.localizedDescription)"
                        }
                    }
                }

                // Zadatak za dohvat kategorija
                grupa.addTask {
                    do {
                        let dohvaćeneKategorije = try await self.dohvatiKategorije()
                        await MainActor.run {
                            self.kategorije = dohvaćeneKategorije
                        }
                    } catch {
                        await MainActor.run {
                            self.greška = "Greška pri dohvatu kategorija: \(error.localizedDescription)"
                        }
                    }
                }
            }
        }
    }

    private func dohvatiVijesti() async throws -> [Vijest] {
        // Simulacija mrežnog poziva
        try await Task.sleep(for: .seconds(1))
        return [
            Vijest(naslov: "Swift 6.0 objavljen", kategorija: "Tehnologija"),
            Vijest(naslov: "Nova verzija Xcodea", kategorija: "Alati")
        ]
    }

    private func dohvatiKategorije() async throws -> [Kategorija] {
        try await Task.sleep(for: .seconds(0.5))
        return [
            Kategorija(naziv: "Tehnologija"),
            Kategorija(naziv: "Alati"),
            Kategorija(naziv: "Tutoriali")
        ]
    }
}

struct VijestiFeedView: View {
    @State private var feed = VijestiFeed()

    var body: some View {
        NavigationStack {
            Group {
                if feed.učitavanje {
                    ProgressView("Učitavanje...")
                } else if let greška = feed.greška {
                    ContentUnavailableView(
                        "Greška",
                        systemImage: "exclamationmark.triangle",
                        description: Text(greška)
                    )
                } else {
                    List(feed.vijesti) { vijest in
                        VStack(alignment: .leading) {
                            Text(vijest.naslov)
                                .font(.headline)
                            Text(vijest.kategorija)
                                .font(.caption)
                                .foregroundStyle(.secondary)
                        }
                    }
                }
            }
            .navigationTitle("Vijesti")
            .task {
                await feed.dohvatiSvePodatke()
            }
        }
    }
}

Kontinuirano praćenje s withObservationTracking i async

Za scenarije izvan SwiftUI-ja, možete koristiti withObservationTracking u asinkronom kontekstu. Ovo je korisno kad trebate reagirati na promjene u pozadinskim procesima:

// Funkcija za kontinuirano praćenje promjena
func pratiPromjene(_ model: KorisnikModel) async {
    // Petlja za kontinuirano praćenje
    while !Task.isCancelled {
        // Čekanje na promjenu praćenih svojstava
        let promjena = await withCheckedContinuation { nastavak in
            withObservationTracking {
                // Pristupamo svojstvima koja želimo pratiti
                _ = model.ime
                _ = model.email
            } onChange: {
                // Obavijesti nastavak da se dogodila promjena
                nastavak.resume(returning: true)
            }
        }

        if promjena {
            print("Korisnik ažuriran: \(model.ime), \(model.email)")
        }
    }
}

Performanse i optimizacija

Hajdemo pričati o performansama — jer je to jedan od najvažnijih razloga za prelazak na Observation framework.

Selektivno promatranje — ključ performansi

Kako smo već spominjali, framework prati pristup na razini pojedinog svojstva. To znači da SwiftUI precizno zna koji pogled koristi koja svojstva i može minimizirati nepotrebna iscrtavanja. Pogledajte ovaj primjer:

@Observable
class AplikacijskiModel {
    var korisničkoIme: String = ""
    var brojObavijesti: Int = 0
    var tema: String = "svijetla"
    var jezik: String = "hr"
    var zadnjaPrijava: Date = Date()
    var statistika: [String: Int] = [:]
}

// Ovaj pogled prati SAMO 'brojObavijesti'
struct ObavijestiBadge: View {
    var model: AplikacijskiModel

    var body: some View {
        // Ponovno iscrtavanje samo kada se 'brojObavijesti' promijeni
        if model.brojObavijesti > 0 {
            Text("\(model.brojObavijesti)")
        }
    }
}

// Ovaj pogled prati SAMO 'korisničkoIme'
struct KorisničkiPozdrav: View {
    var model: AplikacijskiModel

    var body: some View {
        // Ponovno iscrtavanje samo kada se 'korisničkoIme' promijeni
        Text("Bok, \(model.korisničkoIme)!")
    }
}

// Ovaj pogled prati SAMO 'tema'
struct TemaIndikator: View {
    var model: AplikacijskiModel

    var body: some View {
        Image(systemName: model.tema == "tamna" ? "moon.fill" : "sun.max.fill")
    }
}

U starom sustavu, promjena brojObavijesti uzrokovala bi ponovno iscrtavanje sva tri pogleda. S Observation frameworkom? Samo ObavijestiBadge. Razlika u performansama je ogromna, posebno u većim aplikacijama.

Optimizacijski savjeti

Iako framework automatski optimizira iscrtavanja, postoji nekoliko tehnika za dodatni boost:

  • Razdvajanje velikih modela — Umjesto jednog golemog modela, razmislite o razdvajanju na manje, fokusirane modele. Lakše je pratiti i održavati.
  • Korištenje @ObservationIgnored — Označite svojstva koja se često mijenjaju ali ne utječu na UI s @ObservationIgnored. Na primjer, interni cachevi ili debug podatci.
  • Izbjegavanje pristupa nepotrebnim svojstvima — U body computed propertyju pristupajte samo svojstvima koja su stvarno potrebna za prikaz.
  • Korištenje podpogleda — Razdvajajte složene poglede na manje komponente. Svaka komponenta prati samo svoja svojstva.
// LOŠE: Jedan veliki pogled pristupa svim svojstvima
struct LošView: View {
    var model: VelikiModel

    var body: some View {
        VStack {
            Text(model.naslov)      // Prati 'naslov'
            Text(model.opis)        // Prati 'opis'
            Text("\(model.brojač)") // Prati 'brojač'
            // Promjena BILO KOJEG od ovih uzrokuje iscrtavanje CIJELOG pogleda
        }
    }
}

// DOBRO: Razdvojeni podpogledi, svaki prati samo svoja svojstva
struct DobriView: View {
    var model: VelikiModel

    var body: some View {
        VStack {
            NaslovView(model: model)  // Prati samo 'naslov'
            OpisView(model: model)    // Prati samo 'opis'
            BrojačView(model: model)  // Prati samo 'brojač'
        }
    }
}

Testiranje @Observable klasa

Evo nečeg što ćete voljeti: testiranje observable klasa je značajno jednostavnije nego testiranje ObservableObject klasa. Nema ovisnosti o Combineu. Observable klase su obične klase s običnim svojstvima — testirate ih izravno, bez ikakve posebne infrastrukture.

Osnovno jedinično testiranje

import Testing
@testable import MojaAplikacija

// Testiranje s novim Swift Testing frameworkom
struct ZadaciModelTestovi {

    @Test("Dodavanje novog zadatka")
    func dodavanjeZadatka() {
        // Priprema
        let model = ZadaciModel()

        // Radnja
        model.dodajZadatak("Kupiti kruh")

        // Provjera
        #expect(model.zadaci.count == 1)
        #expect(model.zadaci.first?.naslov == "Kupiti kruh")
        #expect(model.zadaci.first?.jeZavršen == false)
    }

    @Test("Završavanje zadatka")
    func završavanjeZadatka() {
        let model = ZadaciModel()
        model.dodajZadatak("Testni zadatak")

        // Označavanje kao završen
        model.zadaci.first?.jeZavršen = true

        #expect(model.završeniZadaci.count == 1)
        #expect(model.nezavršeniZadaci.isEmpty)
    }

    @Test("Izračun postotka završenosti")
    func postotakZavršenosti() {
        let model = ZadaciModel()
        model.dodajZadatak("Zadatak 1")
        model.dodajZadatak("Zadatak 2")
        model.dodajZadatak("Zadatak 3")
        model.dodajZadatak("Zadatak 4")

        // Završavamo 2 od 4 zadatka
        model.zadaci[0].jeZavršen = true
        model.zadaci[1].jeZavršen = true

        #expect(model.postotakZavršenosti == 50.0)
    }

    @Test("Uklanjanje zadatka")
    func uklanjanje() {
        let model = ZadaciModel()
        model.dodajZadatak("Zadatak za ukloniti")
        let zadatak = model.zadaci.first!

        model.ukloniZadatak(zadatak)

        #expect(model.zadaci.isEmpty)
    }
}

Testiranje asinkronih operacija

struct VijestiFeedTestovi {

    @Test("Uspješno dohvaćanje podataka")
    func dohvaćanjePodataka() async {
        let feed = VijestiFeed()

        // Provjera početnog stanja
        #expect(feed.vijesti.isEmpty)
        #expect(feed.kategorije.isEmpty)
        #expect(feed.učitavanje == false)

        // Dohvat podataka
        await feed.dohvatiSvePodatke()

        // Provjera nakon dohvata
        #expect(!feed.vijesti.isEmpty)
        #expect(!feed.kategorije.isEmpty)
        #expect(feed.učitavanje == false)
        #expect(feed.greška == nil)
    }
}

Testiranje praćenja promjena s withObservationTracking

struct PraćenjePromjenaTestovi {

    @Test("Detekcija promjene svojstva")
    func detekcijaPromjene() async {
        let model = KorisnikModel()
        var promjenaDetektirana = false

        // Postavljanje praćenja
        withObservationTracking {
            _ = model.ime  // Pratimo svojstvo 'ime'
        } onChange: {
            promjenaDetektirana = true
        }

        // Promjena praćenog svojstva
        model.ime = "Novo Ime"

        // Kratko čekanje za propagaciju
        try? await Task.sleep(for: .milliseconds(10))

        #expect(promjenaDetektirana == true)
    }
}

Najbolje prakse i česte pogreške

Na temelju iskustva zajednice (i mojih vlastitih grešaka), evo najvažnijih savjeta za rad s Observation frameworkom.

1. Koristite @Observable samo za klase

Makro @Observable radi isključivo s klasama. Za tipove vrijednosti, koristite obična Swift svojstva u kombinaciji s @State:

// ISPRAVNO: @Observable na klasi
@Observable
class KorisnikModel {
    var ime: String = ""
}

// NEISPRAVNO: @Observable ne radi na strukturama
// @Observable
// struct KorisnikPodaci { ... } // Greška kompajlera!

2. Preferite @State za vlasništvo, obične varijable za reference

Koristite @State samo u pogledu koji stvara i posjeduje instancu modela. Svi ostali pogledi primaju model kao običnu varijablu:

// Pogled koji posjeduje model
struct RoditeljskiView: View {
    @State private var model = MojModel() // Vlasnik

    var body: some View {
        DijeteView(model: model) // Prosljeđivanje
    }
}

// Pogled koji koristi model
struct DijeteView: View {
    var model: MojModel // Obična varijabla - BEZ @State!

    var body: some View {
        Text(model.naslov)
    }
}

3. Razdvajajte modele prema odgovornosti

Umjesto jednog monolitnog modela, razdvojite logiku u manje, fokusirane modele. Dobivate bolju modularnost, lakše testiranje i bolje performanse. Win-win-win.

4. Koristite @MainActor za sigurna UI ažuriranja

// SIGURNO: Korištenje @MainActor
@Observable
@MainActor
class SigurniModel {
    var podaci: [String] = []

    func dohvati() async {
        let rezultati = await mrežniPoziv()
        // Sigurno jer je cijela klasa na glavnom akteru
        podaci = rezultati
    }

    // Mrežni poziv se izvršava izvan glavnog aktera
    nonisolated func mrežniPoziv() async -> [String] {
        // ... dohvat podataka
        return []
    }
}

5. Izbjegavajte miješanje starog i novog pristupa

Ovo je česta greška, pogotovo tijekom migracije. Nemojte istovremeno koristiti oba sustava na istoj klasi:

// POGREŠNO: Istovremeno korištenje @Observable i ObservableObject
@Observable
class MojModel: ObservableObject { // Ne radite ovo!
    @Published var ime: String = "" // Konfuzija!
}

// ISPRAVNO: Koristite samo jedan pristup
@Observable
class MojModel {
    var ime: String = ""
}

Savjeti za postupnu migraciju

  1. Počnite s novim značajkama — Sve nove modele i poglede pišite koristeći @Observable. Ne morate odmah migrirati sve.
  2. Migrirajte lisne poglede prvo — Počnite s pogledima najdublje u hijerarhiji i radite prema vrhu.
  3. Testirajte svaki korak — Nakon svake migracije temeljito testirajte. Ozbiljno, nemojte preskočiti ovaj korak.
  4. Migrirajte modele prije pogleda — Prvo pretvorite model u @Observable, pa tek onda ažurirajte poglede koji ga koriste.

Zaključak

Swift Observation framework i @Observable makro predstavljaju ogroman korak naprijed za Swift i SwiftUI ekosustav. Ovaj framework rješava dugogodišnje probleme s ObservableObject protokolom i donosi poboljšanja koja se osjete u svakodnevnom radu.

Evo što smo pokrili u ovom vodiču:

  • Selektivno praćenje svojstava — Pogledi se ažuriraju samo kad se promijene svojstva koja stvarno koriste. Performanse su dramatično bolje.
  • Jednostavnija sintaksa — Nema više @Published, a broj property wrappera u pogledima je značajno smanjen.
  • Bolja integracija s konkurentnošću — Dizajniran za rad s async/await i strukturiranom konkurentnošću.
  • Lakše testiranje — Observable klase su obične klase s običnim svojstvima. Trivijalno za testiranje.
  • Automatska podrška za computed svojstva — Nema ručnog upravljanja ovisnostima.
  • Elegantna podrška za ugniježđene objekte — Praćenje promjena funkcionira kroz lance ugniježđenih observable objekata.

Observation framework zahtijeva iOS 17, macOS Sonoma, watchOS 10 ili tvOS 17. Ako vaša aplikacija još podržava starije verzije sustava, pogledajte open-source paket swift-perception koji omogućuje korištenje istih obrazaca na starijim platformama.

Moj savjet? Počnite koristiti @Observable za sve nove značajke već danas, a postojeći kod postupno migrirajte prema smjernicama iz ovog vodiča. Prelazak na Observation framework nije samo pitanje modernosti — to je konkretno poboljšanje kvalitete, performansi i održivosti vaših SwiftUI aplikacija. A kad jednom osjetite koliko je jednostavniji za korištenje, nećete se htjeti vratiti na stari pristup.