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

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.

O Autorovi Mei-Lin Chen

Mei-Lin joined Robinhood in 2020 as an iOS engineer on the Crypto team and stayed through the SwiftUI rewrite of the order-entry flow before leaving in 2025. She also did a two-year stint at Asana earlier in her career working on the iPad app and the Mac Catalyst port. She writes about the parts of Apple's frameworks that the WWDC talks gloss over - what Observable actually does to your view-update graph, why @Bindable bindings tear in some animation contexts, and the surprisingly deep rabbit hole of Swift macros for boilerplate elimination. She has shipped two indie apps to the App Store, one of which hit #4 in the Health & Fitness category for a week in 2023. Mei-Lin is based in Seattle and has been writing Swift for 8 years.