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:
ObservableObjectprotokol →@Observablemakro@Published→ običnavardeklaracija@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
bodycomputed 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
- Počnite s novim značajkama — Sve nove modele i poglede pišite koristeći
@Observable. Ne morate odmah migrirati sve. - Migrirajte lisne poglede prvo — Počnite s pogledima najdublje u hijerarhiji i radite prema vrhu.
- Testirajte svaki korak — Nakon svake migracije temeljito testirajte. Ozbiljno, nemojte preskočiti ovaj korak.
- 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/awaiti 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.