I would like to know if there is currently (at the time of asking, the first Xcode 12.0 Beta) a way to initialize a @StateObject
with a parameter coming from an
Like @Mark pointed out, you should not handle @StateObject
anywhere during initialization. That is because the @StateObject
gets initialized after the View.init() and slightly before/after the body gets called.
I tried a lot of different approaches on how to pass data from one view to another and came up with a solution that fits for simple and complex views / view models.
Version
Apple Swift version 5.3.1 (swiftlang-1200.0.41 clang-1200.0.32.8)
This solution works with iOS 14.0 upwards, because you need the .onChange()
view modifier. The example is written in Swift Playgrounds. If you need an onChange
like modifier for lower versions, you should write your own modifier.
Main View
The main view has a @StateObject viewModel
handling all of the views logic, like the button tap and the "data" (testingID: String)
-> Check the ViewModel
struct TestMainView: View {
@StateObject var viewModel: ViewModel = .init()
var body: some View {
VStack {
Button(action: { self.viewModel.didTapButton() }) {
Text("TAP")
}
Spacer()
SubView(text: $viewModel.testingID)
}.frame(width: 300, height: 400)
}
}
Main View Model (ViewModel)
The viewModel publishes a testID: String?
. This testID can be any kind of object (e.g. configuration object a.s.o, you name it), for this example it is just a string also needed in the sub view.
final class ViewModel: ObservableObject {
@Published var testingID: String?
func didTapButton() {
self.testingID = UUID().uuidString
}
}
So by tapping the button, our ViewModel
will update the testID
. We also want this testID
in our SubView
and if it changes, we also want our SubView
to recognize and handle these changes. Through the ViewModel @Published var testingID
we are able to publish changes to our view. Now let's take a look at our SubView and SubViewModel.
SubView
So the SubView
has its own @StateObject
to handle its own logic. It is completely separated from other views and ViewModels. In this example the SubView
only presents the testID from its MainView
. But remember, it can be any kind of object like presets and configurations for a database request.
struct SubView: View {
@StateObject var viewModel: SubviewModel = .init()
@Binding var test: String?
init(text: Binding) {
self._test = text
}
var body: some View {
Text(self.viewModel.subViewText ?? "no text")
.onChange(of: self.test) { (text) in
self.viewModel.updateText(text: text)
}
.onAppear(perform: { self.viewModel.updateText(text: test) })
}
}
To "connect" our testingID
published by our MainViewModel
we initialize our SubView
with a @Binding
. So now we have the same testingID
in our SubView
. But we don't want to use it in the view directly, instead we need to pass the data into our SubViewModel
, remember our SubViewModel is a @StateObject
to handle all the logic. And we can't pass the value into our @StateObject
during view initialization, like I wrote in the beginning. Also if the data (testingID: String
) changes in our MainViewModel
, our SubViewModel
should recognize and handle these changes.
Therefore we are using two ViewModifiers
.
onChange
.onChange(of: self.test) { (text) in
self.viewModel.updateText(text: text)
}
The onChange modifier subscribes to changes in our @Binding
property. So if it changes, these changes get passed to our SubViewModel
. Note that your property needs to be Equatable. If you pass a more complex object, like a Struct
, make sure to implement this protocol in your Struct
.
onAppear
We need onAppear
to handle the "first initial data" because onChange doesn't fire the first time your view gets initialized. It is only for changes.
.onAppear(perform: { self.viewModel.updateText(text: test) })
Ok and here is the SubViewModel, nothing more to explain to this one I guess.
class SubviewModel: ObservableObject {
@Published var subViewText: String?
func updateText(text: String?) {
self.subViewText = text
}
}
Now your data is in sync between your MainViewModel and SubViewModel and this approach works for large views with many subviews and subviews of these subviews and so on. It also keeps your views and corresponding viewModels enclosed with high reusability.
Working Example
Playground on GitHub: https://github.com/luca251117/PassingDataBetweenViewModels
Additional Notes
Why I use onAppear
and onChange
instead of only onReceive
: It appears that replacing these two modifiers with onReceive
leads to a continuous data stream firing the SubViewModel updateText
multiple times. If you need to stream data for presentation, it could be fine but if you want to handle network calls for example, this can lead to problems. That's why I prefer the "two modifier approach".
Personal Note: Please don't modify the stateObject outside the corresponding view's scope. Even if it is somehow possible, it is not what its meant for.