Initializing #StateObject with parameters [duplicate] - ios

This question already has answers here:
Initialize #StateObject with a parameter in SwiftUI
(12 answers)
Closed 1 year ago.
Suppose I have views like this:
struct Parent: View {
var data: Int
var body: some View {
Child(state: ChildState(data: data))
}
}
struct Child: View {
#StateObject var state: ChildState
var body: {
...
}
}
class ChildState: ObservableObject {
#Published var property: Int
...
init(data: Int) {
// ... heavy lifting
}
}
My understanding is that the init method for Child should not do any heavy computation, since it could be called over and over, by SwiftUI. So heavy lifting should be done in the ChildState class, which is marked with the #StateObject decorator, to ensure it persists through render cycles. The question is, if Parent.data changes, how do I propagate that down so that ChildState knows to update as well?
That is to say, I DO want a new instance of ChildState, or an update to ChildState if and only if Parent.data changes. Otherwise, ChildState should not change.

You need to make the parent state observable too. I'd do it like this:
class ParentState: ObservableObject {
#Published var parentData = 0
}
class ChildState: ObservableObject {
#Published var childData = 0
}
struct Parent: View {
#StateObject var parentState = ParentState()
#StateObject var childState = ChildState()
var body: some View {
ZStack {
Color.black
VStack {
Text("Parent state \(parentState.parentData) -- click me")
.onTapGesture {
parentState.parentData += 1
}
Child(parentState: parentState, childState: childState)
}
}
}
}
struct Child: View {
#ObservedObject var parentState: ParentState
#ObservedObject var childState: ChildState
var body: some View{
Text("Child state \(childState.childData) parentState \(parentState.parentData) -- click me")
.onTapGesture {
childState.childData += 1
}
}
}
struct ContentView: View {
var body: some View {
Parent()
}
}
Tested with Xcode 13.1
Of course, there's nothing wrong with passing the child state object to the initializer of the child view, the way you've shown it. It will work fine still, I just wanted to make the example as clear as possible.

Related

SwiftUI: Communication between different ViewModels

I have a parent view which contains two child views. Each child view gets passed a different EnvironmentObject from the parent view. As a representation for all kinds of different changes, the second child view contains a Button which can be used to call a function in its ViewModel which then is supposed to change a variable in the ViewModel of the first child view.
struct ParentView: View {
#StateObject var viewModel_1: ViewModel_1
#StateObject var viewModel_2: ViewModel_2
var body: some View {
ZStack {
ChildView_1()
.environmentObject(viewModel_1)
ChildView_2()
.environmentObject(viewModel_2)
}
}}
struct ChildView_1: View {
#EnvironmentObject var viewModel_1: ViewModel_1
var body: some View {
...
}}
struct ChildView_2: View {
#EnvironmentObject var viewModel_2: ViewModel_2
var body: some View {
Button(action: {
viewModel_2.changeValue_in_ViewModel_1(value: 1)
}, label: {
Text("Tap to change value")
})
}}
class ViewModel_1: ObservableObject {
#Published var someValue: Int = 0
func changeValue(value: Int) -> Void {
self.someValue = value
}
}
class ViewModel_2: ObservableObject {
func changeValue_in_ViewModel_1(value: Int) -> Void {
//something like viewModel_2.changeValue(value: value)
}
}
Is there a way to make those two ViewModels able to communicate with each other?
Thanks!
It would be solved simply by ViewModel_2 referencing ViewModel_1.
However, it is not necessary to refer to all ViewModel_1, so you can separate only the desired logic using protocol and let ViewModel_2 own it.
This is the sample code for the above explanation.
Searching for dependency injection can yield a lot of information about it.
struct ParentView: View {
#StateObject var viewModel_1: ViewModel_1
#StateObject var viewModel_2: ViewModel_2
init() {
let viewModel_1 = ViewModel_1()
_viewModel_1 = StateObject(wrappedValue: viewModel_1)
_viewModel_2 = StateObject(wrappedValue: ViewModel_2(changeValue: viewModel_1 as! ChangeValue))
}
var body: some View {
VStack {
ChildView_1()
.environmentObject(viewModel_1)
ChildView_2()
.environmentObject(viewModel_2)
}
}
}
struct ChildView_1: View {
#EnvironmentObject var viewModel_1: ViewModel_1
var body: some View {
Text("\(viewModel_1.someValue)")
}
}
struct ChildView_2: View {
#EnvironmentObject var viewModel_2: ViewModel_2
#State var count: Int = 0
var body: some View {
Button(action: {
count = count + 1
viewModel_2.changeValue_in_ViewModel_1(value: count)
}, label: {
Text("Tap to change value")
})
}
}
protocol ChangeValue {
func changeValue(value: Int)
}
class ViewModel_1: ObservableObject, ChangeValue {
#Published var someValue: Int = 0
func changeValue(value: Int) -> Void {
self.someValue = value
}
}
class ViewModel_2: ObservableObject {
private let changeValue: ChangeValue
init (changeValue: ChangeValue) {
self.changeValue = changeValue
}
func changeValue_in_ViewModel_1(value: Int) -> Void {
//something like viewModel_2.changeValue(value: value)
changeValue.changeValue(value: value)
}
}
We don't actually use view model objects in SwiftUI. The View struct is a view model already, being a value type it's more efficient and less error prone than an object but the property wrappers make it behave like an object, SwiftUI diffs the View struct and it creates/updates actual UIView/NSViews on screen for us. If you use actual view model objects you'll get bugs and face the problems that you are experiencing.
You can group related #State vars into their own struct and use mutating func for logic. That way it can be tested independently but the best thing is any chance to a property of the struct is detected by SwiftUI as a change to the whole struct which makes its dependency tracking super fast.
environmentObject is designed to hold a store object that contains the model structs (usually in arrays) in #Published properties. There isn't usually more than one environmentObject. This object is usually responsible for persisting or syncing the model data.

Updating EnvironmentObject from within the View Model

In SwiftUI, I want to pass an environment object to a view model so I can change/update it. The EnvironmentObject is a simple AppState which consists of a single property counter.
class AppState: ObservableObject {
#Published var counter: Int = 0
}
The view model "CounterViewModel" updates the environment object as shown below:
class CounterViewModel: ObservableObject {
var appState: AppState
init(appState: AppState) {
self.appState = appState
}
var counter: Int {
appState.counter
}
func increment() {
appState.counter += 1
}
}
The ContentView displays the value:
struct ContentView: View {
#ObservedObject var counterVM: CounterViewModel
init(counterVM: CounterViewModel) {
self.counterVM = counterVM
}
var body: some View {
VStack {
Text("\(counterVM.counter)")
Button("Increment") {
counterVM.increment()
}
}
}
}
I am also injecting the state as shown below:
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
NavigationStack {
let appState = AppState()
ContentView(counterVM: CounterViewModel(appState: appState))
.environmentObject(appState)
}
}
}
The problem is that when I click the increment button, the counterVM.counter never returns the updated value. What am I missing?
Your class CounterViewModel is an ObservableObject, but it has no #Published properties – so no changes will be published automatically to the views.
But you can manually publish changes by using objectWillChange.send():
func increment() {
objectWillChange.send()
appState.counter += 1
}
We actually don't use view model objects in SwiftUI for view data. We use an #State struct and if we need to mutate it in a subview we pass in a binding, e.g.
struct Counter {
var counter: Int = 0
mutating func increment() {
counter += 1
}
}
struct ContentView: View {
#State var counter = Counter()
var body: some View {
ContentView2(counter: $counter)
}
}
struct ContentView2: View {
#Binding var counter: Counter // if we don't need to mutate it then just use let and body will still be called when the value changes.
var body: some View {
VStack {
Text(counter.counter, format: .number) // the formatting must be done in body so that SwiftUI will update the label automatically if the region settings change.
Button("Increment") {
counter.increment()
}
}
}
}
I'm not sure why both the CounterViewModel and the AppState need to be observable objects, since you are using a view model to format the content of your models. I would consider AppState to be a model and I could therefore define it as a struct. The CounterViewModel will then be the ObservableObject and it published the AppState. In this way your code is clean and works.
Code for AppState:
import Foundation
struct AppState {
var counter: Int = 0
}
Code for CounterViewModel:
import SwiftUI
class CounterViewModel: ObservableObject {
#Published var appState: AppState
init(appState: AppState) {
self.appState = appState
}
var counter: Int {
appState.counter
}
func increment() {
appState.counter += 1
}
}
Code for the ContentView:
import SwiftUI
struct ContentView: View {
#StateObject var counterVM = CounterViewModel(appState: AppState())
var body: some View {
VStack {
Text("\(counterVM.counter)")
Button("Increment") {
counterVM.increment()
}
}
}
}
Do remind, that in the View where you first define an ObservableObject, you define it with #StateObject. In all the views that will also use that object, you use #ObservedObject.
This code will work.
Did you check your xxxxApp.swift (used to be the AppDelegate) file ?
Sometimes Xcode would do it for you automatically, sometimes won't you have to add it manually and add your environment object. * It has to be the view that contains all the view you want to share the object to.
var body: some Scene {
WindowGroup {
VStack {
ContentView()
.environmentObject(YourViewModel())
}
}
}

SwiftUI: Why onReceive run duplicate when binding a field inside ObservableObject?

This is my code and "print("run to onReceive (text)")" run twice when text change (like a image). Why? and thank you!
import SwiftUI
class ContentViewViewModel : ObservableObject {
#Published var text = ""
}
struct ContentView: View {
#StateObject private var viewModel = ContentViewViewModel()
var body: some View {
ZStack {
TextField("pla", text: $viewModel.text)
.padding()
}
.onReceive(viewModel.$text) { text in
print("run to onReceive \(text)")
}
}
}
I think it's because the view is automatically updated as your #Published property in your ViewModel changes and the .onReceive modifier updates the view yet again due to the 2 way binding created by viewModel.$text resulting in the view being updated twice each time.
If you want to print the text as it changes you can use the .onChange modifier instead.
class ContentViewViewModel: ObservableObject {
#Published var text = ""
}
struct ContentView: View {
#StateObject private var viewModel = ContentViewViewModel()
var body: some View {
ZStack {
TextField("pla", text: $viewModel.text)
.padding()
}.onChange(of: viewModel.text) { newValue in
print("run to onChange \(newValue)")
}
}
}
onChanged in SwiftUI
Because you have a #Published variable inside the #StateObject of your view, the changes in that variable will automatically update the view.
If you add the .onReceive() method, you will:
update the view because you have the #Published var
update it again when the .onReceive() method listens to the change
Just delete the .onReceive() completely and it will work:
class ContentViewViewModel : ObservableObject {
#Published var text = ""
}
struct ContentView: View {
#StateObject private var viewModel = ContentViewViewModel()
var body: some View {
ZStack {
TextField("pla", text: $viewModel.text)
.padding()
}
// It still works without this modifier
//.onReceive(viewModel.$text) { text in
// print("run to onReceive \(text)")
//}
}
}

SwiftUI / Combine Pass data between two models

I have question regarding how to pass data between two models.
struct SettingsCell: View {
#State var isOn: Bool
var body: some View {
Toggle(name, isOn: $isOn)
}
}
class SettingsModel: ObservableObject {
#Published var someValue: Bool = false
}
struct SettingsView: View {
#ObservedObject var model = SettingsModel()
var body: some View {
List {
SettingsCell(isOn: model.someValue)
}
}
}
So i want to pass isOn state from cell, to main model, and react there. Send requests for example.
You need to declare isOn as #Binding in SettingsCell.
#State should only be used for properties initialised inside the View itself and must always be private. If you want to pass in a value that should update the View whenever it changes, but the value is created outside the View, you need to use Binding.
Another really important thing to note is that #ObservedObjects must always be injected into Views, you must not initialise them inside the view itself. This is because whenever an #ObservedObject is updated, it updates the view itself, so if you initialised the object inside the view, whenever the object updates the view, the view would create a new #ObservedObject and hence your changes wouldn't be persisted from the view to the model.
If you are targeting iOS 14 and want to create the model inside the view, you can use #StateObject instead.
struct SettingsCell: View {
#Binding private var isOn: Bool
init(isOn: Binding<Bool>) {
self._isOn = isOn
}
var body: some View {
Toggle(name, isOn: $isOn)
}
}
class SettingsModel: ObservableObject {
#Published var someValue: Bool = false
}
struct SettingsView: View {
#ObservedObject private var model: SettingsModel
init(model: SettingsModel) {
self.model = model
}
var body: some View {
List {
SettingsCell(isOn: $model.someValue)
}
}
}
Binding is used in cases where the data is "owned" by a parent view - i.e. the parent holds the source of truth - and needs the child view to update it:
struct SettingsCell: View {
#Binding var isOn: Bool // change to Binding
var body: some View {
Toggle(name, isOn: $isOn)
}
}
struct SettingsView: View {
// unrelated, but better to use StateObject
#StateObject var model = SettingsModel()
var body: some View {
List {
// pass the binding by prefixing with $
SettingsCell(isOn: $model.someValue)
}
}
}

SwiftUI MVVM: child view model re-initialized when parent view updated

I'm attempting to use MVVM in a SwiftUI app, however it appears that view models for child views (e.g. ones in a NavigationLink) are re-initialized whenever an ObservableObject that's observed by both the parent and child is updated. This causes the child's local state to be reset, network data to be reloaded, etc.
I'm guessing it's because this causes parent's body to be reevaluated, which contains a constructor to SubView's view model, but I haven't been able to find an alternative that lets me create view models that don't live beyond the life of the view. I need to be able to pass data to the child view model from the parent.
Here's a very simplified playground of what we're trying to accomplish, where incrementing EnvCounter.counter resets SubView.counter.
import SwiftUI
import PlaygroundSupport
class EnvCounter: ObservableObject {
#Published var counter = 0
}
struct ContentView: View {
#ObservedObject var envCounter = EnvCounter()
var body: some View {
VStack {
Text("Parent view")
Button(action: { self.envCounter.counter += 1 }) {
Text("EnvCounter is at \(self.envCounter.counter)")
}
.padding(.bottom, 40)
SubView(viewModel: .init())
}
.environmentObject(envCounter)
}
}
struct SubView: View {
class ViewModel: ObservableObject {
#Published var counter = 0
}
#EnvironmentObject var envCounter: EnvCounter
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
Text("Sub view")
Button(action: { self.viewModel.counter += 1 }) {
Text("SubView counter is at \(self.viewModel.counter)")
}
Button(action: { self.envCounter.counter += 1 }) {
Text("EnvCounter is at \(self.envCounter.counter)")
}
}
}
}
PlaygroundPage.current.setLiveView(ContentView())
A new property wrapper is added to SwiftUI in Xcode 12, #StateObject. You should be able to fix it by simply changing #ObservedObject for #StateObject as follows.
struct SubView: View {
class ViewModel: ObservableObject {
#Published var counter = 0
}
#EnvironmentObject var envCounter: EnvCounter
#StateObject var viewModel: ViewModel // change on this line
var body: some View {
// ...
}
}
To solve this problem I created a custom helper class called ViewModelProvider.
The provider takes a hash for your view, and a method that builds the ViewModel. It then either returns the ViewModel, or builds it if its the first time that it received that hash.
As long as you make sure the hash stays the same as long as you want the same ViewModel, this solves the problem.
class ViewModelProvider {
private static var viewModelStore = [String:Any]()
static func makeViewModel<VM>(forHash hash: String, usingBuilder builder: () -> VM) -> VM {
if let vm = viewModelStore[hash] as? VM {
return vm
} else {
let vm = builder()
viewModelStore[hash] = vm
return vm
}
}
}
Then in your View, you can use the ViewModel:
Struct MyView: View {
#ObservedObject var viewModel: MyViewModel
public init(thisParameterDoesntChangeVM: String, thisParameterChangesVM: String) {
self.viewModel = ViewModelProvider.makeViewModel(forHash: thisParameterChangesVM) {
MOFOnboardingFlowViewModel(
pages: pages,
baseStyleConfig: style,
buttonConfig: buttonConfig,
onFinish: onFinish
)
}
}
}
In this example, there are two parameters. Only thisParameterChangesVM is used in the hash. This means that even if thisParameterDoesntChangeVM changes and the View is rebuilt, the view model stays the same.
I was having the same problem, your guesses are right, SwiftUI computes all your parent body every time its state changes. The solution is moving the child ViewModel init to the parent's ViewModel, this is the code from your example:
class EnvCounter: ObservableObject {
#Published var counter = 0
#Published var subViewViewModel = SubView.ViewModel.init()
}
struct CounterView: View {
#ObservedObject var envCounter = EnvCounter()
var body: some View {
VStack {
Text("Parent view")
Button(action: { self.envCounter.counter += 1 }) {
Text("EnvCounter is at \(self.envCounter.counter)")
}
.padding(.bottom, 40)
SubView(viewModel: envCounter.subViewViewModel)
}
.environmentObject(envCounter)
}
}

Resources