Verschachtelte Observables


Verschachtelte Observables sind einer der Stolpersteine bei der Entwicklung mit SwiftUI. Obwohl das Datenmodell aktualisiert wurde, bleibt eine Änderung der View aus. Als Entwickler rätselt man dann oft sehr lange, wo der Fehler liegt. Die Antwort ist dabei einfach: Swift unterstützt verschachtelte Observables nicht. Zumindest nicht so, wie es zu erwarten wäre. Es gibt es zwei Wege um das Problem zu lösen.

Gegeben sei für das folgende Beispiel eine Struktur mit dem Namen Person. Sie verfügt über zwei Eigenschaften für den Vornamen und den Nachnamen. In dieser Form bietet die Klasse keine Überraschungen. Zusätzlich soll die Klasse das Identifiable Protokoll erfüllen. Dadurch wird jede Instanz leicht identifizierbar. Das erleichtert die Verwendung in SwiftUI, zum Beispiel bei der Ausgabe in einem Listen-Steuerelement.

import Foundation

struct Person  : Identifiable{
    var id = UUID()
    var firstName = ""
    var lastName = ""
}

Der folgende Code zeigt die Klasse Employees, mit der die Mitarbeiter eines Unternehmens verwaltet werden. Jeder Mitarbeiter ist eine Instanz der Struktur Person. Im Beispiel enthält die Klassen nur ein Array der Personen. In einer realen Anwendung könnte die Klasse viele Aufgaben übernehmen. Beispielsweise die Lohnabrechnung oder die Verwaltung der Urlaubstage. Solche Funktionen brauchen wir an dieser Stelle nicht. Im Beispiel erzeugt die Klasse drei Instanz von Person zu Testzwecken. Die Klasse erbt von ObservableObject und hat mit list eine @Published Eigenschaft.

import Foundation

class Employees : ObservableObject {
    
    @Published var list : [Person]
    
    init() {
        list = [
            Person(firstName: "John", lastName: "Doe"),
            Person(firstName: "Jennifer", lastName: "Green"),
            Person(firstName: "Zoe", lastName: "Miller"),
        ]
    }
}

Die Verwaltung der Mitarbeiter übernimmt in einem Unternehmen die Personalverwaltung. Im englischen oft bezeichnet als Human Resources. Die Personalverwaltung hat ebenfalls mehrere Aufgaben und das Management der Mitarbeiter ist nur eine von vielen Verantwortungen. Im Programm ist die Klasse Employees der Klasse HumanResouces untergeordnet. Sie erbt ebenfalls von ObservableObject und hat mit der Instanz von Employees einen @Published Eigenschaft.

import Foundation

class HumanResouces : ObservableObject {
    @Published var employees = Employees()
}

Die View, um die Liste der Mitarbeiter anzuzeigen ist einfach aufgebaut. Benötigt wird eine Instanz der HumanResouces-Klasse als @StateObject. Anschließend genügt eine List-Steuerelement, um alle Einträge der Mitarbeiter Liste anzuzeigen.

Zusätzlich einhält der View einen Button um der Liste der Mitarbeiter zusätzliche Kollegen mit dem Namen „Ted Tester“ hinzuzufügen. Beachtenswert ist die die print-Anweisung. Mit ihr wird die aktuelle Anzahl der Mitarbeiter ausgegeben. Ein Klick auf den Button erhöht die Anzahl. Die angezeigte Liste verändert sich aber nicht. Das ist an dieser Stelle unerwartet, den normalerweise sollten alle Eigenschaften, die mit @Publsihed ausgezeichnet sind, Veränderungen sofort an die grafische Oberfläche melden. Das scheint hier nicht zu funktionieren. Die Ursache liegt in der Verschachtelung der einzelnen Klassen. Die Liste der Mitarbeiter liegt in humanResources.employees.list. Änderungen dort werden nicht an die grafische Oberfläche transportiert.

import SwiftUI

struct NestedObservablesView: View {
    
    @StateObject var humanResources = HumanResouces()
    
    var body: some View {
        
        VStack{
            List(humanResources.employees.list) { employee in
                Text("\( employee.firstName) \(employee.lastName)" )
            }
            Button("Add Employee") {
                humanResources.employees.list.append(  Person(firstName: "Ted", lastName: "Tester") )
                print(humanResources.employees.list.count)
            }
        }
    }
}
Erste Lösungmöglichkeit

Ein Weg das Problem zu lösen ist, die grafische Oberfläche in mehrere Views aufzuteilen. Ein Steuerelement, das nur um die Liste der Mitarbeiter kümmern muss, hat keine Probleme Änderungen in dieser Liste zu erkennen. Mit dem nachfolgenden Code funktioniert die Aktualisierung der grafischen Oberfläche.

import SwiftUI

struct EmployeesListView : View {
    
   @ObservedObject   var employees : Employees
    
    var body: some View {
        List(employees.list) { employee in
            Text("\( employee.firstName) \(employee.lastName)" )
        }
    }
}


struct NestedObservablesView: View {
    
    @StateObject var humanResources = HumanResouces()
    
    var body: some View {
        
        VStack{
            EmployeesListView(employees: humanResources.employees)
            Button("Add Employee") {
                humanResources.employees.list.append(  Person(firstName: "Ted", lastName: "Tester") )
                print(humanResources.employees.list.count)
            }
        }
    }
}
Zweite Lösungsmöglichkeit

Die grafische Oberfläche in Teile zu zerlegen, ist immer eine gute Idee, aber in diesem Fall nicht die einzige Lösung. Zumindest dann nicht, wenn IOS17 oder neuer zum Einsatz kommt. Die Entwicklung wird erheblich leichter, wenn statt der Elternklasse ObservableObject das Observation-Framework und das Makro @Observable verwendet wird. Eine Auszeichnung der einzelnen Eigenschaften mit @Published ist denn ebenfalls nicht mehr notwendig. Im Code der grafischen Oberfläche wird das @StateObject zu einem @State.

import Foundation
import Observation

@Observable
class Employees {
    
    var list : [Person]
    
    init() {
        list = [
            Person(firstName: "John", lastName: "Doe"),
            Person(firstName: "Jennifer", lastName: "Green"),
            Person(firstName: "Zoe", lastName: "Miller"),
        ]
    }
}
import Foundation
import Observation

@Observable
class HumanResouces {
    var employees = Employees()
}
import SwiftUI

struct NestedObservablesView: View {
    
    @State var humanResources = HumanResouces()
    
    var body: some View {
        
        VStack{
            List(humanResources.employees.list) { employee in
                Text("\( employee.firstName) \(employee.lastName)" )
            }
            // EmployeesListView(employees: humanResources.employees)
            Button("Add Employee") {
                humanResources.employees.list.append(  Person(firstName: "Ted", lastName: "Tester") )
                print(humanResources.employees.list.count)
            }
        }
    }
}

Geschrieben am: 16.09.2024