Migrace z ObservableObject na @Observable v SwiftUI: Kompletní průvodce 2026

Kompletní průvodce migrací z ObservableObject na @Observable makro v SwiftUI. Reálná čísla výkonu, rozdíly mezi @State a @StateObject, použití @Bindable a typické chyby, které vám ušetří hodiny ladění v iOS 26.

@Observable SwiftUI 2026: Migrace průvodce
On this page
  1. Proč Apple zavedl @Observable makro
  2. Před a po: srovnání minimálního příkladu
  3. Krok za krokem: migrace existujícího projektu
  4. Skryté úskalí: @State NENÍ drop-in náhrada za @StateObject
  5. Časté chyby při migraci
  6. Měření výkonu: reálná čísla
  7. Pokročilé techniky
  8. Kdy NEMIGROVAT
  9. Často kladené otázky
  10. Závěr

Pokud udržujete iOS aplikaci napsanou v SwiftUI, pravděpodobně máte ve složce Models stále třídy implementující protokol ObservableObject s celou armádou anotací @Published. Tuhle bolest znám důvěrně — ještě loni jsem v jednom firemním projektu počítal, že jich máme přes dvě stě. Apple naštěstí od iOS 17 nabízí elegantnější řešení: makro @Observable postavené na novém frameworku Observation. A v roce 2026, s iOS 26 a Swift 6.2, se stalo de facto standardem. Starší ObservableObject je v nových projektech výslovně odrazován.

V tomhle průvodci vás provedu kompletní migrací. Ukážeme si rozdíly v chování, projdeme správné použití @State, @Bindable a @Environment, podíváme se na konkrétní zlepšení výkonu (s reálnými čísly) a hlavně si posvítíme na úskalí, která dokážou pěkně potrápit. Některé z nich totiž vyrobí nedeterministické chyby, na které v produkci přijdete až po pár týdnech.

Proč Apple zavedl @Observable makro

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: Modellet 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:

  1. Aplikace cílí na iOS 16 nebo starší. Framework Observation vyžaduje minimálně iOS 17.
  2. 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.