0

I am facing the issue that changes to the array items are not propagated to the parent view. The text in the inner view does get updated. However, changes to the same item are not displayed in the parent.

This is an extremely reduced version of my issue. Removing the array or manually sending objectWillChange is, in my opinion, not a good solution as It is quite likely to forget it. Changing the ViewModel to a struct does not work for me as I am updating the ViewModel from multiple places.

import SwiftUI

class ViewModel: Identifiable, ObservableObject {
    var label: String = ""
    @Published var big: Bool

    init(label: String, big: Bool) {
        self.big = big
        self.label = label
    }
}

class ArrayViewModel: ObservableObject {
    @Published var items: [ViewModel]

    init(items: [ViewModel] = [ViewModel]()) {
        self.items = items
    }

    init() {
        self.items = [ViewModel]()
    }
}

struct ContentView: View {
    @StateObject var items = ArrayViewModel(items: [ViewModel(label: "view ex", big: true)])
    var body: some View {
        VStack {
            ForEach(0 ..< items.items.count, id: \.self) { index in
                Text(items.items[index].big ? "big" : "small")
                Button("outer toggle") {
                    items.items[index].big.toggle()
                }
                InnerCard()
                    .environmentObject(items.items[index])
            }
        }
    }
}

struct InnerCard: View {
    @EnvironmentObject var currentItem: ViewModel
    var body: some View {
        Text(currentItem.big ? "big" : "small")
        Button("inner toggle") {
            currentItem.big.toggle()
        }
    }
}

#Preview {
    ContentView()
}

enter image description here

8
  • 1
    Why do you think objectWillChange is not a good solution? An alternative would be to use @Observable, but it’d be even better to just not use classes. Use structs instead. Commented Dec 26, 2023 at 15:57
  • 1
    Indices are not safe to use with ForEach Commented Dec 26, 2023 at 16:00
  • @Sweeper Because there are many different places from which I want to update the ViewModel, the probability is quite high that I will forget it somewhere. Structs can't be updated. Commented Dec 26, 2023 at 19:56
  • Structs can definitely be updated. In fact, SwiftUI is arguably much better at detecting changes in structs, since you don’t even need a property wrapper like @Published. Commented Dec 26, 2023 at 20:04
  • 1
    Also, having a “view model” at all, is overcomplicating things. SwiftUI doesn’t need an extra view model. The View struct that you write is literally the view model. SwiftUI creates the actual views for you, based on your View structs. Commented Dec 26, 2023 at 20:09

2 Answers 2

2

In my opinion using multiple observable objects and a view model class instead of a model struct is not a good solution.

I recommend this: ViewModel becomes Model, a struct which refreshes the view when a member changes, and ArrayViewModel becomes ViewModel, the one and only source of truth.

The child view is connected with a simple standard @Binding

struct Model: Identifiable {
    let id = UUID()
    var label: String
    var big: Bool
    
    var state : String {
        big ? "big" : "small"
    }
}

class ViewModel: ObservableObject {
    @Published var items: [Model]

    init(items: [Model] = []) {
        self.items = items
    }
}

struct ContentView1: View {
    @StateObject var viewModel = ViewModel(items: [Model(label: "view ex", big: true)])
    
    var body: some View {
        VStack {
            ForEach($viewModel.items) { $item in
                Text(item.state)
                Button("outer toggle") {
                    item.big.toggle()
                }
                InnerCard(currentItem: $item)
            }
        }
    }
}

struct InnerCard: View {
    @Binding var currentItem: Model

    var body: some View {
        Text(currentItem.state)
        Button("inner toggle") {
            currentItem.big.toggle()
        }
    }
}
Sign up to request clarification or add additional context in comments.

3 Comments

Using structs is not an option for me. I edited my question.
It's not clear at all why it's not an option.
After a lot of refactoring, I ended up with your solution. I can only support: If someone is facing a similar issue, you are doing something wrong.
0

The parent view is not being updated because the published property is a collection of classes. Classes are reference values; as long as the same classes are present in the array, the array (and therefore the containing ViewModel) technically did not change. Structs (among other types) are value types, and would propagate the behaviour as you expect.

Since there are many ways to reach the wished behaviour which depend on your use case, I would leave the rest to you. :)

1 Comment

Using structs is not an option for me. I edited my question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.