Tak schválně — proč vlastně? Původní mechanismus ObservableObject měl jeden zásadní výkonnostní problém. Jakákoliv změna libovolné @Published vlastnosti vyvolala signál objectWillChange a SwiftUI překreslil všechny pohledy, které danou instanci pozorovaly — i když konkrétní pohled tuhle vlastnost vůbec nečetl.
U velkých modelových objektů s desítkami stavových polí to vedlo k masivnímu množství zbytečných překreslení. Pamatuji si projekt, kde profilování v Instruments ukazovalo 40 000 volání body za vteřinu při scrollování. Brutální.
Framework Observation zavádí sledování na úrovni jednotlivých vlastností. SwiftUI přesně ví, které vlastnosti váš pohled v body přečetl, a invalidovat bude pouze ty pohledy, které čtou skutečně změněnou vlastnost. Žádné další zbytečné překreslování.
Klíčové výhody Observation frameworku
- Granulární sledování změn — pohled se překreslí pouze tehdy, změní-li se vlastnost, kterou skutečně používá.
- Méně boilerplate kódu — odpadá protokol
ObservableObject i anotace @Published.
- Sjednocené property wrappery — místo trojice
@StateObject, @ObservedObject a @EnvironmentObject používáte standardní @State, @Bindable a @Environment.
- Sledování kolekcí a optionálů — Observation umí sledovat to, co
ObservableObject nezvládal.
- Lepší ergonomie pro rozsáhlé seznamy — změna jednoho prvku v seznamu desítek tisíc položek překreslí pouze daný řádek.
Před a po: srovnání minimálního příkladu
Začněme typickým ObservableObject z jednoduché aplikace pro správu úkolů (taková ta věc, kterou si každý napíše jako tutoriál):
// Starý přístup s ObservableObject
import Combine
final class TaskStore: ObservableObject {
@Published var tasks: [Task] = []
@Published var filter: Filter = .all
@Published var isLoading: Bool = false
func addTask(_ title: String) {
tasks.append(Task(title: title))
}
}
struct TaskListView: View {
@StateObject private var store = TaskStore()
var body: some View {
List(store.tasks) { task in
TaskRow(task: task)
}
}
}
A teď se podívejte, jak to vypadá po migraci:
// Nový přístup s @Observable
import Observation
@Observable
final class TaskStore {
var tasks: [Task] = []
var filter: Filter = .all
var isLoading: Bool = false
func addTask(_ title: String) {
tasks.append(Task(title: title))
}
}
struct TaskListView: View {
@State private var store = TaskStore()
var body: some View {
List(store.tasks) { task in
TaskRow(task: task)
}
}
}
Všimněte si tří důležitých změn: protokol ObservableObject nahradilo makro @Observable, anotace @Published zmizely (sláva!) a @StateObject se proměnil v obyčejné @State. Vypadá to nevinně, jenže právě tahle poslední změna je zdrojem nejhorších chyb. K tomu se ještě dostaneme.
Krok za krokem: migrace existujícího projektu
1. Přidejte makro a odstraňte konformitu
// Předtím
final class UserSession: ObservableObject {
@Published var currentUser: User?
@Published var isAuthenticated = false
}
// Po
@Observable
final class UserSession {
var currentUser: User?
var isAuthenticated = false
}
2. Označte vlastnosti, které nemají být sledovány
Po aplikaci makra @Observable jsou všechny uložené vlastnosti automaticky sledovány. To je v drtivé většině případů přesně to, co chcete — ale ne vždy. Pokud potřebujete vlastnost ze sledování vyloučit (například pomocné cache, přihlašovací tokeny nebo injectované závislosti), použijte @ObservationIgnored:
@Observable
final class FeedViewModel {
var posts: [Post] = []
var isRefreshing = false
@ObservationIgnored
private var apiClient: APIClient
@ObservationIgnored
private var cancellables: Set<AnyCancellable> = []
init(apiClient: APIClient) {
self.apiClient = apiClient
}
}
3. Upravte property wrappery v pohledech
| Stará syntaxe (ObservableObject) | Nová syntaxe (@Observable) |
@StateObject var model = Model() | @State var model = Model() |
@ObservedObject var model: Model | let model: Model (pouze čtení) nebo @Bindable var model: Model (pro vazby) |
@EnvironmentObject var model: Model | @Environment(Model.self) var model |
.environmentObject(model) | .environment(model) |
4. Použijte @Bindable pro obousměrné vazby
Potřebujete obousměrnou vazbu (typicky TextField($model.name)) na model, který do pohledu pouze přichází zvenčí? Označte ho @Bindable:
struct ProfileEditor: View {
@Bindable var user: User
var body: some View {
Form {
TextField("Jméno", text: $user.name)
Toggle("Notifikace", isOn: $user.notificationsEnabled)
}
}
}
Pozor — a tohle je past, do které jsem osobně spadl: @Bindable deklarujte na úrovni celé struktury nebo úplně na začátku body, nikdy uvnitř vnořeného kontejneru jako VStack nebo if bloku. Způsobíte tím chybu linkeru Undefined symbol: unsafeMutableAddressor, která nevypadá nijak souvisle a ztrácel jsem nad ní jedno odpoledne.
5. Předávejte modely přes Environment
@main
struct MyApp: App {
@State private var session = UserSession()
var body: some Scene {
WindowGroup {
RootView()
.environment(session)
}
}
}
struct ProfileView: View {
@Environment(UserSession.self) private var session
var body: some View {
Text("Vítejte, \(session.currentUser?.name ?? "Hoste")")
}
}
Skryté úskalí: @State NENÍ drop-in náhrada za @StateObject
Tady je to nejdůležitější. Pokud si z celého článku odnesete jen jednu věc, ať je to právě tahle.
@StateObject přijímal @autoclosure, takže inicializace modelu se provedla pouze jednou za životní cyklus pohledu. Naproti tomu @State volá inicializátor při každém přepočítání pohledové hierarchie, byť SwiftUI nakonec použije pouze první vytvořenou instanci. Ostatní se zahodí — jenže ne vždy úplně čistě.
Důsledky bývají zákeřné:
- Pokud inicializátor dělá náročnou práci (síťové dotazy, dekódování velkých souborů, JSON parsing), spustí se opakovaně.
- Pokud se model přihlašuje k notifikacím (například
UIApplication.willTerminateNotification), všechny "zapomenuté" instance zůstanou zaregistrované a budou na notifikaci reagovat. Při ukončení aplikace pak může proběhnout uložení dat z náhodné instance — typický nedeterministický bug, který se občas projeví a občas ne.
- Pomocí Memory Graph Debuggeru lze ověřit, že tyhle instance v paměti přetrvávají déle, než byste čekali.
Jak se tomu vyhnout
// ŠPATNĚ: drahá práce v inicializátoru
@Observable
final class DocumentViewModel {
var pages: [Page] = []
init(url: URL) {
// POZOR: spustí se vícekrát!
self.pages = parseHugeDocument(at: url)
}
}
// SPRÁVNĚ: lehký inicializátor + .task pro načtení
@Observable
final class DocumentViewModel {
var pages: [Page] = []
var isLoading = false
@ObservationIgnored
let url: URL
init(url: URL) {
self.url = url
}
func load() async {
isLoading = true
defer { isLoading = false }
pages = await parseHugeDocument(at: url)
}
}
struct DocumentView: View {
@State private var viewModel: DocumentViewModel
init(url: URL) {
_viewModel = State(initialValue: DocumentViewModel(url: url))
}
var body: some View {
ContentList(pages: viewModel.pages)
.task { await viewModel.load() }
}
}
Jednoduché pravidlo na pamatování: v inicializátoru jen přiřaďte hodnoty, žádné parsování, žádné fetch volání. Vše ostatní patří do .task.
Časté chyby při migraci
Chyba 1: @State v podřízeném pohledu
@State používejte pouze v pohledu, který model vlastní. Podřízené pohledy přijímají model jako prostou vlastnost, žádný wrapper kolem toho být nemá:
// ŠPATNĚ — vytvoří kopii modelu
struct TaskRow: View {
@State var task: Task
var body: some View { Text(task.title) }
}
// SPRÁVNĚ — stejná instance jako v rodiči
struct TaskRow: View {
let task: Task
var body: some View { Text(task.title) }
}
Chyba 2: Míchání @Published s @Observable
Anotace @Published nemá v @Observable třídách žádný efekt — Xcode na ni ani neupozorní. Po migraci ji vždy odstraňte, jinak může zbytečně mást čtenáře kódu (typicky vás samotného za půl roku).
Chyba 3: Ztráta Combine publisherů
U @Observable nelze pomocí prefixu $ získat Combine publisher — operátor $ ve view nyní vytváří Binding. Pokud potřebujete debounce nebo throttle nad uživatelským vstupem, použijte modifikátor .onChange v kombinaci se Swift Concurrency:
struct SearchBar: View {
@Bindable var model: SearchModel
@State private var debounceTask: Task<Void, Never>?
var body: some View {
TextField("Hledat", text: $model.query)
.onChange(of: model.query) { _, newValue in
debounceTask?.cancel()
debounceTask = Task {
try? await Task.sleep(for: .milliseconds(300))
guard !Task.isCancelled else { return }
await model.performSearch(newValue)
}
}
}
}
Chyba 4: Použití @nonisolated(unsafe)
Hlásí vám Swift 6 varování ohledně izolace modelu? Neobcházejte je značkou @nonisolated(unsafe), prosím. Místo toho přidejte na třídu @MainActor, aby byla bezpečně přístupná z hlavního vlákna — což je stejně jediné místo, odkud by SwiftUI měl číst:
@MainActor
@Observable
final class AppState {
var theme: Theme = .system
var fontSize: CGFloat = 14
}
Měření výkonu: reálná čísla
Teoretizovat můžeme do nekonečna, ale čísla mluví jasněji. V testovacím projektu se seznamem 10 000 položek (každá obsahuje pět samostatných stavových polí) byly naměřeny tyhle rozdíly:
- Při změně jednoho pole položky
ObservableObject spustil přepočítání body celé sekce seznamu — průměrně 230 ms.
- Stejná operace s
@Observable invalidovala pouze konkrétní řádek — průměrně 4 ms.
- Spotřeba paměti zůstala srovnatelná, ale počet vyvolání
body klesl o více než 95 %.
Pro aplikace s rozsáhlými seznamy nebo komplexními formuláři je migrace investicí, která se vrátí prakticky okamžitě. (A i pokud máte menší aplikaci, méně boilerplate je méně boilerplate.)
Pokročilé techniky
Sdílení modelu mezi více scénami
Díky sjednocení s @Environment stačí model vložit jednou na vrcholu hierarchie a pak ho použít, kde potřebujete:
@main
struct InvoiceApp: App {
@State private var library = InvoiceLibrary()
var body: some Scene {
WindowGroup { MainView() }
.environment(library)
Settings { SettingsView() }
.environment(library)
}
}
Vnořené @Observable modely
Tohle je vlastně moje nejoblíbenější vlastnost: Observation funguje rekurzivně. Pokud má rodičovský model vlastnost typu jiného @Observable, sledování probíhá automaticky bez další konfigurace:
@Observable final class Address {
var street: String = ""
var city: String = ""
}
@Observable final class Customer {
var name: String = ""
var address: Address = Address()
}
// Pohled, který čte zákazníka.address.city, se aktualizuje
// pouze tehdy, změní-li se konkrétně město.
Testování modelů s @Observable
Modely označené @Observable se testují stejně jako jakákoliv jiná třída — bez Combine, bez nutnosti čekat na odeslání publisheru, bez expectation gymnastiky:
import Testing
@testable import MyApp
@Test func addTaskIncreasesCount() {
let store = TaskStore()
#expect(store.tasks.isEmpty)
store.addTask("Napsat článek")
#expect(store.tasks.count == 1)
#expect(store.tasks.first?.title == "Napsat článek")
}
Kdy NEMIGROVAT
Migrace nedává smysl ve dvou případech:
- Aplikace cílí na iOS 16 nebo starší. Framework Observation vyžaduje minimálně iOS 17.
- Silně využíváte Combine pipeline. Pokud váš model staví na
$publisher řetězech operátorů, přepis na async/await může být rozsáhlejší než samotná migrace anotací. V tom případě bych radil jít po jednotlivých modelech podle priority, ne plošně.
Ve všech ostatních případech je migrace doporučovaná i samotnou Apple dokumentací.
Často kladené otázky
Mohu používat @Observable a ObservableObject ve stejném projektu?
Ano. Migrace probíhá inkrementálně, soubor po souboru. Smíchané přístupy v rámci jedné aplikace nezpůsobují žádné problémy, dokud nemícháte oba mechanismy v jedné třídě.
Jaký je rozdíl mezi @Bindable a @Binding?
@Binding propojí konkrétní hodnotovou vlastnost (např. Bool nebo String) mezi rodičem a potomkem. @Bindable přidá schopnost vytvářet Binding z vlastností @Observable třídy přijaté zvenčí — tedy $model.property u modelu, který pohled nevlastní.
Funguje @Observable s SwiftData?
Ano, a velmi dobře. Třídy @Model v SwiftData implicitně získávají chování @Observable, takže můžete kombinovat @Query a @Bindable bez dalších úprav.
Proč se mi @Observable model "ztrácí" mezi překresleními?
Pravděpodobně používáte @State v podřízeném pohledu místo prostého let. @State v dítěti vytvoří novou instanci namísto reference na sdílenou. Odeberte @State a předávejte model přímo.
Musím přejít na Swift 6 kvůli @Observable?
Ne. Framework Observation je dostupný od Swiftu 5.9 a iOS 17. Swift 6.2 přináší doplňky pro koncepci souběžnosti, ale samotné @Observable funguje už od Xcode 15.
Závěr
Makro @Observable není kosmetickým vylepšením — je to nové paradigma sledování stavu v SwiftUI. Přináší výrazně lepší výkon, méně boilerplate kódu a sjednocené API. Současně je to past pro neopatrné: rozdílné chování inicializace mezi @StateObject a @State dokáže způsobit chyby, které se projeví až v produkci.
Můj doporučený postup? Postupujte inkrementálně, vždy ověřte, že náročné inicializátory nahrazujete asynchronním načítáním v .task, a důsledně používejte @ObservationIgnored pro vlastnosti, které do sledování opravdu nepatří. S těmihle pravidly získáte rychlejší, čitelnější a stabilnější SwiftUI aplikace připravené na rok 2026 a iOS 26.