SwiftUI state over multiple views

Viewed 1773

In SwiftUI handling the state of an app first seemed to be quite simple. There are essentially two property wrappers: @EnvironmentObject and @ObservedObject (and @State). But it turned out that it isn't that simple at all.

I posted several questions here. Most of them have been answered very well: SwiftUI onDelete List with Toggle, SwiftUI ForEach with .indices() does not update after onDelete, SwiftUI: Index out of range when deleting cells with toggle

But every time I go a step further, some principal problems stay. These are

  1. Hand over a bindable variable via NavigationLink in a List or a ForEach to a SubView and reflecting changes back to the MainView. (I tried several solutions like working with .indices on the array and make a bindable on the fly (SwiftUI onDelete List with Toggle)). To reflect changes back I also tried to work with .onReceive() which also didn't work.
  2. I always run into index-problems when I try to work with this formula: someArray.firstIndex(where {condition})!. You can find this in the Apple tutorials for SwiftUI. I also run into problems, when I use indexes in a ForEach-closure and then try to swipe-delete with .onDelete(). The error message is:
Thread 1: Fatal error: Index out of range
  1. How can I bind variables to an @EnvironmentObject-model that are two or more layers behind the mainView? Working with the formula in 2. produces very long variable-names. This doesn't seem to be right.

Here is one example, which nearly works. The only thing that does not work is updating the MainView when I change values in the SubViews. But it doesn't feel right for me to use several ObservedObjects in my app.

class Model: ObservableObject {
    @Published var items: [Item]
    
    init(name: String, items: [Item]) {
        self.items = items
    }   
}

class Item: Identifiable, ObservableObject {
    var id = UUID()
    var itemName: String
    @Published var subItems: [SubItem]
    
    init(itemName: String, payments: [SubItem]) {
        self.itemName = itemName
        self.subItems = payments
    }
}

class SubItem: Identifiable, ObservableObject {
    var id = UUID()
    var isOn: Bool
    
    init(isOn: Bool) {
        self.isOn = isOn
    }
}


struct MainView: View {
    @EnvironmentObject var model: Model
    
    var body: some View {
        NavigationView {
            List {
                ForEach(model.items) {item in
                    NavigationLink(destination: SubView1(item: item)) {
                        HStack{
                            Text(item.itemName)
                            Text(item.subItems[0].isOn ? "True":"False") // I know: bad, but only for quick debugging reasons
                            Text(item.subItems[1].isOn ? "True":"False")
                        }
                    }.onReceive(item.objectWillChange, perform: {self.model.objectWillChange.send()})
                }.onDelete(perform: delete)
            }
        }
    }
    
    func delete(at offsets: IndexSet) {
        self.model.items.remove(atOffsets: offsets)
    }
    
}

struct SubView1: View {
    @ObservedObject var item: Item
    var body: some View {
        List {
            ForEach(item.subItems) {subItem in
                NavigationLink(destination: SubView2(subItem: subItem)) {
                    ToggleView(subItem: subItem)
                }
                
            }.onDelete(perform: delete)
        }
    }
    
    func delete(at offsets: IndexSet) {
           self.item.subItems.remove(atOffsets: offsets)
       }
}

struct SubView2: View {
    @ObservedObject var subItem: SubItem
    var body: some View {
        Toggle(isOn: $subItem.isOn) {
            Text("Toggle-Text")
        }
    }
}


struct ToggleView: View {
    @ObservedObject var subItem: SubItem
    var body: some View {
        Toggle(isOn: $subItem.isOn) {
            Text("Toggle-Text")
        }
    }
}
1 Answers

What you can do is to access the binding of an item and use it in the ToggleView:

struct SubView1: View {
    @ObservedObject var item: Item
    var body: some View {
        List {
            ForEach(0..<item.subItems.count, id: \.self) { index in
                NavigationLink(destination: SubView2(subItem: self.$item.subItems[index])) { // <- pass the binding
                    ToggleView(subItem: self.$item.subItems[index]) // <- pass the binding
                }
            }.onDelete(perform: delete)
        }
    }
    ...
}
struct SubView2: View {
    @Binding var subItem: SubItem // <- receive the binding
    var body: some View {
        Toggle(isOn: $subItem.isOn) {
            Text("Toggle-Text")
        }
    }
}
struct ToggleView: View {
    @Binding var subItem: SubItem // <- receive the binding
    var body: some View {
        Toggle(isOn: $subItem.isOn) {
            Text("Toggle-Text")
        }
    }
}
Related