Prečo @Observable mení spôsob, akým píšeme SwiftUI aplikácie
Ruku na srdce — ak ste niekedy ladili zbytočné prekresľovanie views v SwiftUI, viete, aký frustrujúci dokáže byť ObservableObject. Stačilo zmeniť jedinú @Published vlastnosť a celý strom views sa prekresľoval. Aj keď väčšina z nich tú zmenu vôbec nepotrebovala.
S príchodom iOS 17 a Swift 5.9 Apple konečne predstavil framework Observation a makro @Observable, ktoré tento problém riešia od základov. A úprimne? Je to jedna z najlepších zmien v SwiftUI za posledné roky.
V tomto sprievodcovi si ukážeme, ako @Observable funguje, prečo je výkonnejší ako starý prístup, ako migrovať existujúci kód a aké pokročilé vzory môžete využiť v produkčných aplikáciách. Príklady sú aktualizované pre Swift 6 a Xcode 16.
Čo je Observation framework a makro @Observable
Observation je natívny Swift framework, ktorý implementuje návrhový vzor pozorovateľa (observer pattern) priamo na úrovni jazyka. Na rozdiel od ObservableObject, ktorý bol postavený na Combine a vyžadoval explicitné označovanie vlastností cez @Published, @Observable sleduje prístup k vlastnostiam automaticky.
Kľúčový rozdiel je v granularite. ObservableObject fungoval na princípe push — pri akejkoľvek zmene oznámil všetkým pozorovateľom, že sa niečo zmenilo. Nerozlišoval, čo presne. @Observable funguje na princípe pull — SwiftUI sleduje, ktoré konkrétne vlastnosti view skutočne číta, a prekresľuje ho iba vtedy, keď sa zmenia práve tie.
Takže žiadne zbytočné prekresľovania. Konečne.
Ako to vyzerá v praxi
Vytvorenie pozorovateľnej triedy je krásne minimalistické:
import Observation
@Observable
class UserProfile {
var meno = "Ján"
var priezvisko = "Novák"
var vek = 30
var jeAktivny = true
}
To je všetko. Žiadny @Published, žiadny protokol ObservableObject. Makro @Observable pri kompilácii automaticky transformuje uložené vlastnosti na sledované a pridá konformitu k protokolu Observable. Keď som to prvýkrát videl, nechcel som tomu uveriť — naozaj to stačí.
Správne property wrappery: @State, @Environment a @Bindable
S Observation frameworkom sa zmenili aj property wrappery, ktoré používate vo views. Zabudnite na @StateObject, @ObservedObject a @EnvironmentObject — nahradzujú ich jednoduchšie alternatívy.
@State pre vlastníctvo objektu
Ak view vlastní inštanciu modelu a riadi jej životný cyklus, použite @State:
struct ProfilView: View {
@State private var profil = UserProfile()
var body: some View {
VStack {
Text(profil.meno)
Text("Vek: \(profil.vek)")
}
}
}
Dôležitá vec — @State sa tu správa inak ako pri hodnotových typoch. SwiftUI uchováva referenčnú identitu objektu naprieč prekresľovaniami, ale inicializátor sa môže volať opakovane. Preto nikdy nevykonávajte vedľajšie efekty (sieťové volania, registráciu notifikácií) v init() vášho @Observable modelu. Viac o tom nižšie v sekcii bežných chýb.
@Environment pre zdieľanie cez hierarchiu
Ak potrebujete zdieľať model naprieč viacerými views, vložte ho do prostredia:
// V nadradenej view
@State private var nastavenia = AppSettings()
var body: some View {
ContentView()
.environment(nastavenia)
}
// V podriadenej view
struct ContentView: View {
@Environment(AppSettings.self) private var nastavenia
var body: some View {
Toggle("Tmavý režim", isOn: $nastavenia.tmavy)
}
}
Namiesto .environmentObject() teraz používate .environment() a namiesto @EnvironmentObject jednoducho @Environment s typom. Jednoduchšie a čistejšie.
@Bindable pre obojsmerné väzby
Keď potrebujete vytvoriť binding na vlastnosť @Observable objektu, tu prichádza @Bindable:
struct EditorView: View {
@Bindable var profil: UserProfile
var body: some View {
Form {
TextField("Meno", text: $profil.meno)
TextField("Priezvisko", text: $profil.priezvisko)
Stepper("Vek: \(profil.vek)", value: $profil.vek)
}
}
}
@Bindable je ľahký wrapper, ktorý umožňuje syntaxu $ pre vytváranie väzieb priamo z pozorovateľných vlastností. Nič viac, nič menej.
Rozhodovací strom — ktorý wrapper použiť
Apple odporúča pomerne jednoduchú logiku:
- View vlastní objekt? →
@State - Objekt má byť globálne dostupný v hierarchii? →
@Environment - Potrebujete len väzby na vlastnosti? →
@Bindable - Žiadne z vyššie uvedených? → obyčajná vlastnosť (
letalebovar)
Až na pár okrajových prípadov sa s touto logikou dostanete ďaleko.
Výkonnostné výhody: Prečo @Observable prekresľuje menej
Toto je podľa mňa ten najdôležitejší dôvod, prečo migrovať. Poďme si ukázať, prečo je @Observable výkonnejší na konkrétnom príklade. Predstavte si model s piatimi vlastnosťami:
@Observable
class Dashboard {
var pocetObjednavok = 0
var trzby = 0.0
var noviZakaznici = 0
var priemernaCena = 0.0
var poslednaDavka = ""
}
A máte view, ktorý zobrazuje len pocetObjednavok:
struct OrderCountView: View {
var dashboard: Dashboard
var body: some View {
Text("Objednávky: \(dashboard.pocetObjednavok)")
}
}
S ObservableObject by sa tento view prekresľoval pri zmene akejkoľvek z piatich vlastností. S @Observable sa prekresľuje iba pri zmene pocetObjednavok. V aplikácii s desiatkami views a modelov s mnohými vlastnosťami je to dramatický rozdiel.
Pri veľkých zoznamoch je efekt ešte výraznejší. Ak máte List s tisíckami položiek a zmeníte jednu, s @Observable sa prekresľuje iba riadok, ktorý tú vlastnosť číta. Ostatné riadky zostávajú nedotknuté. V praxi som videl prípady, kde to znížilo počet prekresľovaní o 80 % a viac.
Migrácia z ObservableObject na @Observable krok za krokom
Migrácia existujúceho kódu je prekvapivo priamočiara. Tak poďme na to.
Krok 1: Transformujte model
Pred migráciou:
import Combine
class AppSettings: ObservableObject {
@Published var tmavy = false
@Published var velkostPisma: Double = 14
@Published var jazyk = "sk"
}
Po migrácii:
import Observation
@Observable
class AppSettings {
var tmavy = false
var velkostPisma: Double = 14
var jazyk = "sk"
}
Odstráňte konformitu k ObservableObject, všetky @Published anotácie a import Combine (ak ho nepoužívate inde). Pridajte makro @Observable. A to je celý krok jedna.
Krok 2: Aktualizujte property wrappery vo views
| Starý prístup | Nový prístup |
|---|---|
@StateObject | @State |
@ObservedObject | obyčajná vlastnosť alebo @Bindable |
@EnvironmentObject | @Environment |
.environmentObject() | .environment() |
Krok 3: Označte nesledované vlastnosti
Ak máte vlastnosti, ktoré nemajú spúšťať prekresľovanie — napríklad interné cache alebo počítadlá — označte ich makrom @ObservationIgnored:
@Observable
class MediaPlayer {
var aktualnaStopa = ""
var jePrehravanie = false
@ObservationIgnored
var internyBuffer: [Data] = []
@ObservationIgnored
var pocetPrehratychStopy = 0
}
Krok 4: Testujte inkrementálne
Migrujte po jednom modeli a testujte. Oba prístupy môžu koexistovať v jednom projekte — len ich nekombinujte v jednej triede. Osobne odporúčam začať s najjednoduchšími modelmi a postupne sa prepracovať k zložitejším.
Vnorené pozorovateľné objekty a kolekcie
Jednou z najväčších výhod Observation frameworku je podpora vnorených objektov. A tu sa naplno ukazuje, prečo sa oplatí migrovať.
S ObservableObject ste museli používať rôzne workaroundy — manuálne objectWillChange.send() alebo @Published na vnorené objekty. S @Observable to funguje priamo:
@Observable
class Obchod {
var nazov = ""
var adresa = Adresa()
}
@Observable
class Adresa {
var ulica = ""
var mesto = ""
var psc = ""
}
View, ktorý číta obchod.adresa.mesto, sa automaticky prekresľuje pri zmene mesto vnoreného objektu. Bez akýchkoľvek trikov.
Dôležité upozornenie pri kolekciách
Pri práci s kolekciami @Observable objektov buďte trochu opatrní. Ak máte pole [Produkt] a zmeníte vlastnosť jedného produktu, view čítajúci tú konkrétnu vlastnosť sa prekresľuje správne. Ale ak view číta len referenciu na pole (napríklad produkty.count), zmena vnútri jednotlivého produktu ho neovplyvní. Čo je zvyčajne presne to, čo chcete.
@Observable
class Katalog {
var produkty: [Produkt] = []
func pridajProdukt(_ produkt: Produkt) {
produkty.append(produkt) // Spustí prekresľovanie views čítajúcich pole
}
}
@Observable
class Produkt {
var nazov: String
var cena: Double
var jeNaSklade: Bool
init(nazov: String, cena: Double, jeNaSklade: Bool = true) {
self.nazov = nazov
self.cena = cena
self.jeNaSklade = jeNaSklade
}
}
Pokročilé vzory: withObservationTracking mimo SwiftUI
Observation framework nie je limitovaný na SwiftUI — a to je fajn. Funkcia withObservationTracking umožňuje sledovať zmeny aj v iných kontextoch, napríklad v UIKit kontroléroch alebo v logike doménovej vrstvy:
func sledujZmeny(profil: UserProfile) {
withObservationTracking {
// Čítame vlastnosti, ktoré chceme sledovať
let meno = profil.meno
let vek = profil.vek
print("Aktuálne: \(meno), \(vek)")
} onChange: {
print("Niektorá zo sledovaných vlastností sa zmenila")
// DÔLEŽITÉ: Toto sa volá iba raz!
// Pre kontinuálne sledovanie musíte volanie zopakovať
}
}
Kľúčové obmedzenie: callback onChange sa volá iba pri prvej zmene. Ak potrebujete kontinuálne sledovanie, musíte withObservationTracking zavolať znova. Toto správanie je zámerné — zabraňuje náhodnému vytváraniu silných referenčných cyklov. Spočiatku to pôsobí zvláštne, ale v praxi to dáva zmysel.
Swift Evolution SE-0506: Pokročilé sledovanie
Komunita aktívne pracuje na návrhu SE-0506 (Advanced Observation Tracking), ktorý pridáva nové API vrátane withContinuousObservationTracking — variantu, ktorý automaticky obnovuje sledovanie po každej zmene. Toto API je navrhnuté hlavne pre infraštruktúrne účely ako middleware alebo widget systémy. Takže pokiaľ nerobíte niečo špecifické, bežný vývojár ho zatiaľ nebude potrebovať.
Bežné chyby a ako sa im vyhnúť
Za tie mesiace, čo pracujem s @Observable, som videl (a urobil) niekoľko opakujúcich sa chýb. Tu sú tie najčastejšie.
Chyba č. 1: Použitie @Observable na struct
Makro @Observable funguje iba s triedami. Ak ho aplikujete na struct, dostanete chybu pri kompilácii. Structs majú hodnotovú sémantiku a SwiftUI ich sleduje automaticky cez @State. Jednoducho povedané — structs to nepotrebujú.
Chyba č. 2: Vedľajšie efekty v init()
Toto je záludné. S @StateObject bol inicializátor volaný iba raz vďaka @autoclosure. S @State a @Observable sa inicializátor môže volať viackrát, keď SwiftUI rekonštruuje hierarchiu views. Preto:
// ❌ Nebezpečné
@Observable
class DataManager {
var data: [String] = []
init() {
// Toto sa môže volať viackrát!
NotificationCenter.default.addObserver(...)
loadDataFromNetwork()
}
}
// ✅ Bezpečné — presúňte vedľajšie efekty
@Observable
class DataManager {
var data: [String] = []
func nacitajData() {
// Volajte explicitne cez .task {} vo view
loadDataFromNetwork()
}
}
Presúňte vedľajšie efekty do samostatných metód a volajte ich cez .task {} vo view. Ušetríte si hodiny ladenia.
Chyba č. 3: Miešanie ObservableObject a @Observable
Nikdy nekombinujte oba prístupy v jednej triede. Trieda má byť buď @Observable, alebo ObservableObject — nie oboje. Oba prístupy však môžu pokojne koexistovať v jednom projekte, čo umožňuje postupnú migráciu.
Chyba č. 4: Ignorovanie minimálneho iOS targetu
@Observable vyžaduje iOS 17 a novší. Ak ešte podporujete staršie verzie, musíte zatiaľ zostať pri ObservableObject alebo použiť podmienené kompilácie:
#if canImport(Observation)
import Observation
@Observable
class MojModel {
var hodnota = ""
}
#else
import Combine
class MojModel: ObservableObject {
@Published var hodnota = ""
}
#endif
Tento prístup funguje, ale pridáva komplexitu. Ak máte možnosť zvýšiť minimálny target na iOS 17 (a v roku 2026 by väčšina aplikácií už mala), jednoznačne to odporúčam.
Praktický príklad: MVVM s @Observable
Pozrime sa na kompletný príklad architektúry MVVM s Observation frameworkom. Toto je niečo, čo môžete priamo použiť vo svojom projekte:
// Model
struct Uloha: Identifiable {
let id = UUID()
var nazov: String
var jeSplnena: Bool = false
}
// ViewModel
@Observable
class UlohyViewModel {
var ulohy: [Uloha] = []
var novyNazov = ""
var zobrazujLenNesplnene = false
var filtrovaneUlohy: [Uloha] {
if zobrazujLenNesplnene {
return ulohy.filter { !$0.jeSplnena }
}
return ulohy
}
func pridajUlohu() {
guard !novyNazov.isEmpty else { return }
ulohy.append(Uloha(nazov: novyNazov))
novyNazov = ""
}
func prepniStav(uloha: Uloha) {
if let index = ulohy.firstIndex(where: { $0.id == uloha.id }) {
ulohy[index].jeSplnena.toggle()
}
}
}
// View
struct UlohyView: View {
@State private var viewModel = UlohyViewModel()
var body: some View {
NavigationStack {
List {
ForEach(viewModel.filtrovaneUlohy) { uloha in
HStack {
Image(systemName: uloha.jeSplnena
? "checkmark.circle.fill"
: "circle")
Text(uloha.nazov)
}
.onTapGesture {
viewModel.prepniStav(uloha: uloha)
}
}
}
.navigationTitle("Úlohy")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Toggle("Nesplnené",
isOn: $viewModel.zobrazujLenNesplnene)
}
}
.safeAreaInset(edge: .bottom) {
HStack {
TextField("Nová úloha",
text: $viewModel.novyNazov)
.textFieldStyle(.roundedBorder)
Button("Pridať") {
viewModel.pridajUlohu()
}
.buttonStyle(.borderedProminent)
}
.padding()
}
}
}
}
Všimnite si, aký je kód čistý. Žiadne @Published, žiadny @StateObject. ViewModel je jednoduchá trieda s makrom @Observable a view používa @State na riadenie životného cyklu. Bindable syntax $viewModel.novyNazov funguje automaticky vďaka @State.
Presne takto vyzerá moderný SwiftUI kód. Jednoduchý, čitateľný a bez zbytočného boilerplate.
Často kladené otázky
Môžem používať @Observable a ObservableObject v jednom projekte súčasne?
Áno, bez problémov. Oba prístupy môžu koexistovať v jednom projekte, čo umožňuje postupnú migráciu. Jediné pravidlo je, že jedna trieda má používať len jeden z týchto prístupov — nekombinujte ich v jednej triede.
Funguje @Observable so struct alebo len s class?
Makro @Observable funguje výhradne s triedami. Structs majú hodnotovú sémantiku a SwiftUI ich zmeny sleduje automaticky cez @State, takže Observation framework jednoducho nepotrebujú.
Aký je minimálny iOS target pre @Observable?
Observation framework vyžaduje iOS 17, iPadOS 17, macOS 14, tvOS 17 alebo watchOS 10 a novšie. Ak vaša aplikácia ešte podporuje staršie verzie, musíte zatiaľ zostať pri ObservableObject.
Nahradí @Observable úplne Combine?
Nie úplne, ale z veľkej časti. @Observable nahrádza potrebu Combine v kontexte reaktívneho spájania modelov so SwiftUI views. Combine však zostáva užitočný pre spracovanie asynchrónnych streamov, operácie ako debounce, throttle a sieťové volania. V praxi väčšina aplikácií bude Combine na UI úrovni potrebovať čoraz menej.
Prečo sa moje views stále zbytočne prekresľujú s @Observable?
Najčastejšia príčina je, že view číta viac vlastností, než skutočne potrebuje. Ak v body voláte funkciu modelu, ktorá interne pristupuje k viacerým vlastnostiam, SwiftUI zaregistruje všetky tieto prístupy. Riešenie? Rozdeľte veľké views na menšie podviews, z ktorých každý číta len relevantné vlastnosti. Tak maximalizujete výhody property-level trackingu.