Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI state over multiple views

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")
        }
    }
}
like image 159
pawello2222 Avatar answered Jul 30 '26 09:07

pawello2222



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!