Pass up to date values to a StateObject through a view's init

Hi, I was taught that:

  • if my SwiftUI view changes data that are kept outside of the view, it should use @Binding
  • if my view keeps its state in some variables, the variables should be @State
  • if my view only receives data to present etc. then the fields should be normal fields (let xyz: String)

In my case I should use the last option because my view only presents the data, but my question/problem is how to pass the current, up to date data to the view's StateObject? Setting the values for the StateObject in onAppear is not the proper way to solve the problem. For example:

// --- Main view ---

class MainViewModel: ObservableObject {
    @Published var items = [Int]()

    // ... logic
}

struct MainView: View {
    @StateObject var mainViewModel = MainViewModel()

    var body: some View {
        // 1
        MyView(items: mainViewModel.items)
    }
}

// --- My view ---

struct MyView: View {
    let items: [Int]

    var body: some View {
        // 2
        MyViewSubview(items: items)
    }
}

// --- My view's subview

class MyViewSubviewModel: ObservableObject {
    @Published var itemsPresentation = ""

    func prepareItemsForPresentation(items: [Int]) {
        // ... logic
    }
}

struct MyViewSubview: View {
    let items: [Int]
    @StateObject private var myViewSubviewModel = MyViewSubviewModel()

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

    var body: some View {
        // 4
        Text(myViewSubviewModel.itemsPresentation)
            .onAppear {
                // 3
                myViewSubviewModel.prepareItemsForPresentation(items: items)
            }
    }
}

When mainViewModel.items changes (1) it will update and redraw MyView (👍). In MyView, updated items will go to MyViewSubview (2) and cause update and redraw (👍). The problem is happening here in MyViewSubview, we need to update the delivered data, the proper way is to do it in onAppear (3) BUT it will be called only once - when the view is presented the first time. The subsequent updates of items and redraws of the MyViewSubview will not call prepareItemsForPresentation and so it will not call the login in MyViewSubviewModel and it will not present the proper data (4).

How to refresh/update @StateObject whenever new data is passed through an initializer of a view??? onChange(of:perform:) doesn't work, because items is not a @State variable (and can't be, because I shouldn't set @State in an initializer).

What is your question ?

  • initialise State in init ?

You should use State(initialValue:) on the _var

        _someVar = State(initialValue: theValue)   

But you seem to need to update this value ?

A solution is

  • to declare a StateObject at the top level of views that will use it (could even be in @main)
class SomeObject: ObservableObject {  
    @Published var value = 0
}
  • and insert it to the environment in the body of the "top" view
.environmentObject(object)
  • in each view that need to use it:
    @EnvironmentObject var anObject: SomeObject      

In each view that uses, add in onAppear:

        .onAppear() {
            .environmentObject(anObject)   
       }

Thank you for your answer.

What is your question ?

My question is

How to refresh/update @StateObject whenever new data is passed through an initializer of a view???

You are correct, EnvironmentObject should help, but isn't the mechanism ugly? Views that use my MyViewSubview have no idea that the view needs an EnvironmentObject, I would like to pass the dependencies through an initializer, exactly how it works in case of Binding but like I mentioned the subview doesn't change the delivered data, so it shouldn't be Binding. Another disadvantage is that I have to build ObservableObject even if I wanted to pass one simple value type.

EnvironmentObject should help, but isn't the mechanism ugly

No, it is advised to use instead of passing Bindings between a lot of views. It is done for this.

.

have to build ObservableObject even if I wanted to pass one simple value type.

Yes, but you need to publish for others to know.

I have an additional question

You should use State(initialValue:) on the _var

_someVar = State(initialValue: theValue)

Is it good to use the State's initializer? The documentation says about the "twin" initializer (init(wrappedValue:)):

Don’t call this initializer directly. Instead, declare a property with the State attribute, and provide an initial value:

@State private var isPlaying: Bool = false
Pass up to date values to a StateObject through a view's init
 
 
Q