Search code examples
swiftswiftuiobservedobject

SWIFTUI: Why adding @State to property doesn't update data model


having a problem with updating data in ObservedObject here's the data MRE:

struct Property: Identifiable, Codable, Hashable {
    var id = UUID()
    var name : String = ""
    var meters : [Meter] = [Meter(name: "electricity"), Meter(name: "water")]
}
struct Meter: Identifiable, Hashable, Codable {
    var id = UUID()
    var name : String = ""
}

class PropertyData: ObservableObject {
    @Published var properties: [Property]
    
    
    func save() {
        let encoder = JSONEncoder()
        if let encoded = try? encoder.encode(properties) {
            UserDefaults.standard.set(encoded, forKey: "PropertyData")
        }
    }
    
    init() {
        if let properties = UserDefaults.standard.data(forKey: "PropertyData") {
            let decoder = JSONDecoder()
            if let decoded = try? decoder.decode([Property].self, from: properties) {
                self.properties = decoded
                return
            }
        }
        self.properties = [
            Property(name: "Saks /1", meters: [Meter(name: "electricity")]),
            Property(name: "Saks /2", meters: [Meter(name: "electricity"), Meter(name: "water")]),
        ]
    }
}


import SwiftUI

struct ContentView: View {
    @ObservedObject var data = PropertyData()
    
    var body: some View {
        NavigationView {
            List {
                ForEach(data.properties, id:\.self) {property in
                    NavigationLink(destination: NavigationLinkView(data: data, property: property)) {
                        Text(property.name)
                    }
                }
            }
        }
    }
}

struct NavigationLinkView: View {
    @ObservedObject var data : PropertyData
    var property:Property
    
    var body: some View {
        TabView {
            MetersView(data: data, property: property)
                .tabItem{
                    VStack {
                        Image(systemName: "scroll")
                        Text("Utitity")
                    }
                }
        }
    }
}

struct MetersView: View {
    @ObservedObject var data: PropertyData
    @State private var metersPage = 0
    @State var property : Property
    @State private var addMeters = false
    
    var body: some View {
        VStack {
            HStack {
                Picker("Meters", selection: $metersPage) {
                    ForEach (0 ..< property.meters.count, id:\.self) {index in
                        Text(property.meters[index].name)
                    }
                }
                .pickerStyle(SegmentedPickerStyle())
                Button{
                    print(property.meters)
                    addMeters.toggle()
                } label: {
                    Image(systemName: "gear")
                }
            }.padding()
            Spacer()
        }.sheet(isPresented: $addMeters){AddMetersView(data: data, property: $property)}
    }
}

struct AddMetersView: View {
    @ObservedObject var data : PropertyData
    @Binding var property : Property
    @State var newMeter: String = ""
    @Environment(\.presentationMode) var presentationMode
    
    var body: some View {
        VStack {
            Form{
                Section {
                    TextField("Add another meter", text: $newMeter)
                        .autocapitalization(.none)
                    Button{
                        if newMeter != "" {
                            property.meters.append(Meter(name: newMeter))
                            data.save()
                            print(property.meters.count)
                        }                    } label: {
                            Text("Add a meter")
                        }
                }
                ForEach(0..<property.meters.count, id:\ .self) {index in
                    Text(property.meters[index].name)
                }
                Section() {
                    Button("That's enough"){
                        print(property.meters)
                        presentationMode.wrappedValue.dismiss()}
                }
            }
        }
    }
}

I cannot understand, why the meters, that I add on AddMetersView, do update the Meters page, but then just goes away as soon as I go to the ContentView. Plus, in my app, if I add a property, it does stay, and it persists, but not the Meters


Solution

  • This is similar to your previous question. Change @State var property : Property to

    It is all about the connections. You break them in a few places along the way I made some changes and put comments along the line.

    import SwiftUI
    
    struct MetersParentView: View {
        //Change to StateObject
        @StateObject var data = PropertyData()
        
        var body: some View {
            NavigationView {
                List {
                    ForEach($data.properties, id:\.id) {$property in
                        NavigationLink(destination: NavigationLinkView(data: data, property: $property)) {
                            Text(property.name)
                        }
                    }
                }
            }
        }
    }
    
    struct NavigationLinkView: View {
        @ObservedObject var data : PropertyData
        //Make Binding there is no connection without it
        @Binding var property : Property
        
        var body: some View {
            TabView {
                MetersView(data: data, property: $property)
                    .tabItem{
                        VStack {
                            Image(systemName: "scroll")
                            Text("Utitity")
                        }
                    }
            }
        }
    }
    
    struct MetersView: View {
        @ObservedObject var data: PropertyData
        @State var selectedMeter: Meter = .init()
        //Change to Binding
        //State is a source of truth, it breaks the connection
        @Binding var property : Property
        @State private var addMeters = false
        
        var body: some View {
            VStack {
                HStack {
                    Picker("Meters", selection: $selectedMeter) {
                        //Use object not index
                        ForEach(property.meters, id:\.id) {meter in
                            //Tag adjustment
                            Text(meter.name).tag(meter as Meter)
                        }
                    }.onAppear(perform: {
                        selectedMeter = property.meters.first ?? Meter()
                    })
                    .pickerStyle(SegmentedPickerStyle())
                    Button{
                        print(property.meters)
                        addMeters.toggle()
                    } label: {
                        Image(systemName: "gear")
                    }
                }.padding()
                Spacer()
            }.sheet(isPresented: $addMeters){
                AddMetersView(data: data, property: $property)
                
            }
        }
    }
    
    struct AddMetersView: View {
        @ObservedObject var data : PropertyData
        @Binding var property : Property
        @State var newMeter: String = ""
        @Environment(\.presentationMode) var presentationMode
        
        var body: some View {
            VStack {
                Form{
                    Section {
                        TextField("Add another meter", text: $newMeter)
                            .autocapitalization(.none)
                        Button{
                            if newMeter != "" {
                                print(property.meters.count)
                                property.meters.append(Meter(name: newMeter))
                                print(property.meters.count)
                                data.save()
                                print(property.meters.count)
                            }
                            
                        } label: {
                            Text("Add a meter")
                        }
                    }
                    //Dont use index
                    ForEach(property.meters, id:\ .id) {meter in
                        Text(meter.name)
                    }
                    Section() {
                        Button("That's enough"){
                            print(property.meters)
                            presentationMode.wrappedValue.dismiss()}
                    }
                }
            }
        }
    }
    struct MetersParentView_Previews: PreviewProvider {
        static var previews: some View {
            MetersParentView()
        }
    }