Lektion 3.4 · Stufe 3 – Fortgeschritten

Älteren Code lesen: ObservableObject und Co.

ca. 15 Minuten Lesezeit

Lernziel

Nach dieser Lektion kannst du älteren SwiftUI-Code lesen, der ObservableObject, @Published, @StateObject, @ObservedObject und @EnvironmentObject verwendet. Du weißt, wie die alten Muster den neuen entsprechen und wie du alten Code Schritt für Schritt auf @Observable umstellst.

Das Problem: Tutorials aus einer anderen Zeit

@Observable gibt es seit iOS 17. SwiftUI selbst gibt es aber schon seit iOS 13. In den Jahren dazwischen sind unzählige Tutorials, Bücher, Stack-Overflow-Antworten und ganze Apps entstanden, die einen anderen Weg verwenden. Früher oder später triffst du auf Code wie diesen:

Swift
class ProfileModel: ObservableObject {
    @Published var name = "Anna"
}

struct ProfileView: View {
    @StateObject private var model = ProfileModel()

    var body: some View {
        Text(model.name)
    }
}

Dieser Code ist nicht falsch. Er funktioniert bis heute. Aber er sieht anders aus als das, was du gelernt hast, und hat ein paar eigene Fallstricke. In dieser Lektion lernst du, ihn zu lesen und bei Bedarf umzustellen. Schreiben wirst du neuen Code weiterhin mit @Observable.

Die alte Welt im Überblick

Alt (bis iOS 16)Neu (ab iOS 17)Zweck
class X: ObservableObject@Observable class XKlasse beobachtbar machen
@Published var namevar nameEigenschaft beobachten
@StateObject private var x = X()@State private var x = X()Objekt besitzen
@ObservedObject var x: Xlet x: X bzw. @Bindable var x: XObjekt bekommen
.environmentObject(x).environment(x)Objekt in die Umgebung legen
@EnvironmentObject var x: X@Environment(X.self) var xObjekt aus der Umgebung holen

Fast jede Zeile hat also eine direkte Entsprechung. Gehen wir sie durch.

ObservableObject und @Published

Swift
class SettingsModel: ObservableObject {
    @Published var userName = ""
    @Published var notificationsOn = true
    var internalCounter = 0
}
  • ObservableObject ist ein Protokoll, kein Makro. Die Klasse erfüllt es mit einem Doppelpunkt.
  • @Published markiert jede Eigenschaft, deren Änderung die Oberfläche aktualisieren soll.
  • Eigenschaften ohne @Published wie internalCounter lösen keine Aktualisierung aus. Bei @Observable entspricht das @ObservationIgnored.

Fallstrick 1: @Published vergessen

Vergisst man bei ObservableObject ein @Published, ändert sich die Eigenschaft zwar, aber die Oberfläche merkt nichts davon. Kein Fehler, keine Warnung. Ein sehr häufiger Anfängerfehler, den es bei @Observable nicht mehr gibt.

Die drei Property Wrapper

In der alten Welt gab es drei Wrapper für Objekte, je nach Situation.

@StateObject: besitzen

Swift
struct SettingsView: View {
    @StateObject private var model = SettingsModel()
    // …
}

Entspricht @State bei @Observable: Die View erzeugt das Objekt und hält es am Leben.

@ObservedObject: bekommen

Swift
struct SettingsForm: View {
    @ObservedObject var model: SettingsModel

    var body: some View {
        TextField("Name", text: $model.userName)
    }
}

Die View bekommt das Objekt von außen und beobachtet es. $model.userName liefert ein Binding. In der neuen Welt entspricht das let zum Lesen oder @Bindable für Bindings.

Fallstrick 2: @ObservedObject mit Startwert

Swift
@ObservedObject var model = SettingsModel()   // ⚠️ gefährlich

Das kompiliert, aber @ObservedObject besitzt das Objekt nicht. Wird die View neu erzeugt, entsteht ein neues Objekt, und alle Daten sind weg. Das passiert scheinbar zufällig, etwa wenn sich etwas in der Eltern-View ändert. Dieser Fehler war jahrelang eine der häufigsten Fragen zu SwiftUI. Richtig ist @StateObject.

@EnvironmentObject: aus der Umgebung

Swift
// Bereitstellen
ContentView()
    .environmentObject(settings)

// Verwenden
struct DeepView: View {
    @EnvironmentObject var settings: SettingsModel
}

Entspricht .environment(settings) und @Environment(SettingsModel.self). Und genau wie in der neuen Welt stürzt die App ab, wenn das Objekt fehlt.

Der wichtigste Unterschied: Wie genau wird beobachtet?

Neben der Schreibweise gibt es einen inhaltlichen Unterschied, der in großen Apps spürbar wird.

Ein ObservableObject hat nur ein Signal: „Ich habe mich geändert.“ Ändert sich irgendeine @Published-Eigenschaft, werden alle Views neu berechnet, die das Objekt beobachten, egal welche Eigenschaft sie lesen.

Swift
class Profile: ObservableObject {
    @Published var name = "Anna"
    @Published var followers = 0
}

struct NameView: View {
    @ObservedObject var profile: Profile
    var body: some View { Text(profile.name) }
}

Ändert sich followers, wird NameView trotzdem neu berechnet, obwohl sie followers gar nicht anzeigt.

Mit @Observable (Lektion 3.1) passiert das nicht. Dort merkt sich SwiftUI für jede View, welche Eigenschaften sie wirklich liest.

Alten Code umstellen

Hast du ein bestehendes Projekt mit ObservableObject, kannst du es Schritt für Schritt umstellen. Für eine Klasse:

Vorher:

Swift
class CartModel: ObservableObject {
    @Published var items: [Item] = []
    @Published var couponCode = ""
    private var cache: [String: Double] = [:]
}

struct ShopView: View {
    @StateObject private var cart = CartModel()
    var body: some View {
        CartView(cart: cart)
            .environmentObject(cart)
    }
}

struct CartView: View {
    @ObservedObject var cart: CartModel
    var body: some View {
        TextField("Gutschein", text: $cart.couponCode)
    }
}

struct CheckoutButton: View {
    @EnvironmentObject var cart: CartModel
    var body: some View {
        Text("\(cart.items.count) Artikel")
    }
}

Nachher:

Swift
@Observable
class CartModel {
    var items: [Item] = []
    var couponCode = ""
    @ObservationIgnored private var cache: [String: Double] = [:]
}

struct ShopView: View {
    @State private var cart = CartModel()
    var body: some View {
        CartView(cart: cart)
            .environment(cart)
    }
}

struct CartView: View {
    @Bindable var cart: CartModel
    var body: some View {
        TextField("Gutschein", text: $cart.couponCode)
    }
}

struct CheckoutButton: View {
    @Environment(CartModel.self) private var cart
    var body: some View {
        Text("\(cart.items.count) Artikel")
    }
}

Die Schritte:

  1. : ObservableObject entfernen, @Observable davor schreiben.
  2. Alle @Published entfernen. Eigenschaften, die nicht beobachtet werden sollen, bekommen @ObservationIgnored.
  3. @StateObject → @State.
  4. @ObservedObject → let, oder @Bindable, wenn $ verwendet wird.
  5. .environmentObject(x) → .environment(x).
  6. @EnvironmentObject var x: X → @Environment(X.self) private var x.

Xcode hilft dir dabei: Nach Schritt 1 und 2 melden sich alle Stellen, die noch die alten Wrapper verwenden.

Tipp

Du musst nicht alles auf einmal umstellen. Alte und neue Klassen können problemlos nebeneinander existieren. Stell eine Klasse nach der anderen um und teste dazwischen.

Wann du die alte Welt noch brauchst

Ein paar Situationen, in denen ObservableObject noch eine Rolle spielt:

  • Ältere iOS-Versionen: Muss deine App auf iOS 16 oder früher laufen, gibt es kein @Observable.
  • Combine: @Published erzeugt einen sogenannten Publisher aus dem Framework Combine. Code, der damit arbeitet, etwa $name.debounce(...), braucht ObservableObject. In neuem Code ersetzt man das meist durch async/await (Lektion 3.6).
  • Bestehende Bibliotheken, die ihre Objekte noch als ObservableObject anbieten.

Typische Fehler beim Lesen

$model in alter und neuer Welt

In beiden Welten liefert $model.name ein Binding. In der alten Welt bekommst du es von @ObservedObject und @StateObject, in der neuen von @State und @Bindable. Nur bei let gibt es kein $.

Gemischte Wrapper

Swift
@Observable class Model { }
@StateObject private var model = Model()

Fehlermeldung

Generic struct 'StateObject' requires that 'Model' conform to 'ObservableObject'

Neue Klassen mit neuen Wrappern, alte mit alten. Mischen geht nicht.

@Published in einer @Observable-Klasse

Das ergibt einen Fehler, weil das Makro die Eigenschaft selbst umschreibt. Entferne @Published.

FAQ

Anfänger

Was bedeutet das überhaupt?

Muss ich die alte Welt können, um eine App zu bauen? Nein. Für neue Apps reicht @Observable. Diese Lektion ist dafür da, dass du nicht verwirrt bist, wenn du Code aus dem Internet liest, und dass du ihn übersetzen kannst.

Woran erkenne ich auf einen Blick, ob Code alt oder neu ist? Steht irgendwo ObservableObject, @Published, @StateObject, @ObservedObject oder @EnvironmentObject, ist es die alte Welt. Steht @Observable über einer Klasse, ist es die neue.

Fortgeschritten

Warum verhält es sich so?

Gibt es auch @State mit einem ObservableObject? Technisch ja, aber dann wird das Objekt nicht beobachtet. Die Oberfläche aktualisiert sich nicht. Das ist ein weiterer Grund, die Welten nicht zu mischen.

Was ist objectWillChange? Der Publisher, den jedes ObservableObject hat. @Published ruft automatisch objectWillChange.send() auf, kurz bevor sich eine Eigenschaft ändert. In altem Code siehst du manchmal einen manuellen Aufruf, wenn sich etwas ändert, das nicht @Published ist.

Profi

Was passiert unter der Haube?

Warum „will change“ und nicht „did change“? SwiftUI will vor der Änderung wissen, dass gleich eine kommt, um den alten Zustand für Animationen festzuhalten. Die eigentliche Neuberechnung erfolgt dann gesammelt beim nächsten Durchlauf der Oberfläche. Mehrere Änderungen hintereinander lösen so nur eine Neuberechnung aus.

Wie gehen @Observable und Combine zusammen? Gar nicht direkt: @Observable erzeugt keine Publisher. Brauchst du einen Datenstrom für Änderungen, gibt es seit neueren Swift-Versionen Observations, eine asynchrone Sequenz, die mit for await gelesen wird. Für die meisten Fälle reicht aber .onChange(of:) in der View.

Übungen

Anfänger

Übung 1: Übersetzen

Schreib die rechte Spalte der Tabelle aus dem Gedächtnis auf: Was ist das neue Gegenstück zu ObservableObject, @Published, @StateObject, @ObservedObject, .environmentObject und @EnvironmentObject?

Fortgeschritten

Übung 2: Umstellen

Stell diesen Code auf @Observable um:

Swift
class TimerModel: ObservableObject {
    @Published var seconds = 0
    @Published var isRunning = false
    var startDate: Date?
}

struct TimerScreen: View {
    @StateObject private var timer = TimerModel()
    var body: some View {
        VStack {
            Text("\(timer.seconds) s")
            TimerControls(timer: timer)
        }
    }
}

struct TimerControls: View {
    @ObservedObject var timer: TimerModel
    var body: some View {
        Toggle("Läuft", isOn: $timer.isRunning)
    }
}
Lösung Übung 2Erst selbst probieren!
Swift
@Observable
class TimerModel {
    var seconds = 0
    var isRunning = false
    @ObservationIgnored var startDate: Date?
}

struct TimerScreen: View {
    @State private var timer = TimerModel()
    var body: some View {
        VStack {
            Text("\(timer.seconds) s")
            TimerControls(timer: timer)
        }
    }
}

struct TimerControls: View {
    @Bindable var timer: TimerModel
    var body: some View {
        Toggle("Läuft", isOn: $timer.isRunning)
    }
}

startDate war nicht @Published und wird deshalb @ObservationIgnored. TimerControls braucht ein Binding, deshalb @Bindable statt let.

Profi

Übung 3: Fehler finden

Was ist an diesem alten Code problematisch, und wie würdest du ihn reparieren, einmal in der alten und einmal in der neuen Welt?

Swift
struct ListScreen: View {
    @ObservedObject var store = ItemStore()
    var body: some View { … }
}
Lösung Übung 3Erst selbst probieren!

@ObservedObject mit Startwert besitzt das Objekt nicht. Bei jeder Neuerzeugung der View entsteht ein neuer, leerer Store. In der alten Welt: @StateObject private var store = ItemStore(). In der neuen: ItemStore mit @Observable und @State private var store = ItemStore().

Zusammenfassung

  • Ältere SwiftUI-Codes verwenden ObservableObject mit @Published.
  • @StateObject besitzt, @ObservedObject bekommt, @EnvironmentObject holt aus der Umgebung.
  • Jedes alte Muster hat ein neues Gegenstück mit @Observable, @State, let/@Bindable und @Environment.
  • ObservableObject meldet nur „irgendetwas hat sich geändert“, @Observable verfolgt jede Eigenschaft einzeln.
  • Umstellen geht schrittweise, eine Klasse nach der anderen.
  • Neuen Code schreibst du mit @Observable.

Spickzettel

Swift
// Alt                                  // Neu
class M: ObservableObject {            @Observable class M {
    @Published var x = 0                   var x = 0
}                                      }
@StateObject private var m = M()       @State private var m = M()
@ObservedObject var m: M               let m: M  /  @Bindable var m: M
.environmentObject(m)                  .environment(m)
@EnvironmentObject var m: M            @Environment(M.self) private var m

Lektion geschafft?

Markiere sie als erledigt, damit du deinen Fortschritt siehst.