Updating EnvironmentObject from within the View Model - ios

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())
}
}
}

Related

How to pass by reference in Swift - number incrementing app using MVVM

I've just started learning swift and was going to build this number-incrementing sample app to understand MVVM. I don't understand why is my number on the view not updating upon clicking the button.
I tried to update the view everytime user clicks the button but the count stays at zero.
The View
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel = CounterViewModel()
var body: some View {
VStack {
Text("\(viewModel.model.count)")
Button(action: {
self.viewModel.increment()
}) {
Text("Increment")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The ViewModel
import SwiftUI
class CounterViewModel: ObservableObject {
#ObservedObject var model = Model()
func increment() {
self.model.count += 1
}
}
The Model
import Foundation
class Model : ObservableObject{
#Published var count = 0
}
Following should work:
import SwiftUI
struct Model {
var count = 0
}
class CounterViewModel: ObservableObject {
#Published var model = Model()
func increment() {
self.model.count += 1
}
}
struct ContentView: View {
#ObservedObject var viewModel = CounterViewModel()
var body: some View {
VStack {
Text("\(viewModel.model.count)")
Button(action: {
self.viewModel.increment()
}) {
Text("Increment")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Please note:
ObservableObject and #Published are designed to work together.
Only a value, that is in an observed object gets published and so the view updated.
A distinction between model and view model is not always necessary and the terms are somewhat misleading. You can just put the count var in the ViewModel. Like:
#Published var count = 1
It makes sense to have an own model struct (or class), when fx you fetch a record from a database or via a network request, than your Model would take the complete record.
Something like:
struct Adress {
let name: String
let street: String
let place: String
let email: String
}
Please also note the advantages (and disadvantages) of having immutable structs as a model. But this is another topic.
Hi it's a bad idea to use MVVM in SwiftUI because Swift is designed to take advantage of fast value types for view data like structs whereas MVVM uses slow objects for view data which leads to the kind of consistency bugs that SwiftUI's use of value types is designed to eliminate. It's a shame so many MVVM UIKit developers (and Harvard lecturers) have tried to push their MVVM garbage onto SwiftUI instead of learning it properly. Fortunately some of them are changing their ways.
When learning SwiftUI I believe it's best to learn value semantics first (where any value change to a struct is also a change to the struct itself), then the View struct (i.e. when body is called), then #Binding, then #State. e.g. have a play around with this:
// use a config struct like this for view data to group related vars
struct ContentViewConfig {
var count = 0 {
didSet {
// could do validation here, e.g. isValid = count < 10
}
}
// include other vars that are all related, e.g. you could have searchText and searchResults.
// use mutating func for logic that affects multiple vars
mutating func increment() {
count += 1
//othervar += 1
}
}
struct ContentView: View {
#State var config = ContentViewConfig() // normally structs are immutable, but #State makes it mutable like magic, so its like have a view model object right here, but better.
var body: some View {
VStack {
ContentView2(count: config.count)
ContentView3(config: $config)
}
}
}
// when designing a View first ask yourself what data does this need to do its job?
struct ContentView2: View {
let count: Int
// body is only called if count is different from the last time this was init.
var body: some View {
Text(count, format: .number)
}
}
struct ContentView3: View {
#Binding var config: ContentViewConfig
var body: some View {
Button(action: {
config.increment()
}) {
Text("Increment")
}
}
}
}
Then once you are comfortable with view data you can move on to model data which is when ObservableObject and singletons come into play, e.g.
struct Item: Identifiable {
let id = UUID()
var text = ""
}
class MyStore: ObservableObject {
#Published var items: [Item] = []
static var shared = MyStore()
static var preview = MyStore(preview: true)
init(preview: Bool = false) {
if preview {
items = [Item(text: "Test Item")]
}
}
}
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(MyStore.shared)
}
}
}
struct ContentView: View {
#EnvironmentObject store: MyStore
var body: some View {
List($store.items) { $item in
TextField("Item", $item.text)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(MyStore.preview)
}
}
Note we use singletons because it would be dangerous to use #StateObject for model data because its lifetime is tied to something on screen we could accidentally lose all our model data which should have lifetime tied to the app running. Best to think of #StateObject when you need a reference type in a #State, i.e. involving view data.
When it comes to async networking use the new .task modifier and you can avoid #StateObject.

Initializing #StateObject with parameters [duplicate]

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.

SwiftUI Using ObservableObject in View on New Sheet

I am struggling with figuring out how to use a value assigned to a variable in an ObservableObject class in another view on another sheet. I see that it gets updated, but when I access it in the new view on the new sheet it is reset to the initialized value. How do I get it to retain the new value so I can use it in a new view on a new sheet?
ContentData.swift
import SwiftUI
import Combine
class ContentData: ObservableObject {
#Published var text: String = "Yes"
}
ContentView.swift
import SwiftUI
struct ContentView: View {
#ObservedObject var contentData = ContentData()
#State private var inputText: String = ""
#State private var showNewView: Bool = false
var body: some View {
VStack {
TextField("Text", text: $inputText, onCommit: {
self.assignText()
})
Button(action: {
self.showNewView = true
}) {
Text("Go To New View")
}
.sheet(isPresented: $showNewView) {
NewView(contentData: ContentData())
}
}
}
func assignText() {
print(contentData.text)
contentData.text = inputText
print(contentData.text)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(contentData: ContentData())
}
}
NewView.swift
import SwiftUI
struct NewView: View {
#ObservedObject var contentData = ContentData()
var body: some View {
VStack {
Text(contentData.text)
}
}
}
struct NewView_Previews: PreviewProvider {
static var previews: some View {
NewView(contentData: ContentData())
}
}
I have tried many, many different methods I have seen from other examples. I tried doing it with #EnviromentObject but could not get that to work either. I also tried a different version of the NewView.swift where I initialized the value with:
init(contentData: ContentData) {
self.contentData = contentData
self._newText = State<String>(initialValue: contentData.text)
}
I think I am close, but I do not see what I am missing. Any help would be very much appreciated. Thanks.
#ObservedObject var contentData = ContentData()
ContentData() in the above line creates a new instance of the class ContentData.
You should pass the same instance from ContentView to NewView to retain the values. Like,
.sheet(isPresented: $showNewView) {
NewView(contentData: self.contentData)
}
Stop creating new instance of ContentData in NewView and add the ability to inject ContentData from outside,
struct NewView: View {
#ObservedObject var contentData: ContentData
...
}

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)
}
}

How do I update a text label in SwiftUI?

I have a SwiftUI text label and i want to write something in it after I press a button.
Here is my code:
Button(action: {
registerRequest() // here is where the variable message changes its value
}) {
Text("SignUp")
}
Text(message) // this is the label that I want to change
How do I do this?
With only the code you shared it is hard to say exactly how you should do it but here are a couple good ways:
The first way is to put the string in a #State variable so that it can be mutated and any change to it will cause an update to the view. Here is an example that you can test with Live Previews:
import SwiftUI
struct UpdateTextView: View {
#State var textToUpdate = "Update me!"
var body: some View {
VStack {
Button(action: {
self.textToUpdate = "I've been udpated!"
}) {
Text("SignUp")
}
Text(textToUpdate)
}
}
}
struct UpdateTextView_Previews: PreviewProvider {
static var previews: some View {
UpdateTextView()
}
}
If your string is stored in a class that is external to the view you can use implement the ObservableObject protocol on your class and make the string variable #Published so that any change to it will cause an update to the view. In the view you need to make your class variable an #ObservedObject to finish hooking it all up. Here is an example you can play with in Live Previews:
import SwiftUI
class ExternalModel: ObservableObject {
#Published var textToUpdate: String = "Update me!"
func registerRequest() {
// other functionality
textToUpdate = "I've been updated!"
}
}
struct UpdateTextViewExternal: View {
#ObservedObject var viewModel: ExternalModel
var body: some View {
VStack {
Button(action: {
self.viewModel.registerRequest()
}) {
Text("SignUp")
}
Text(self.viewModel.textToUpdate)
}
}
}
struct UpdateTextViewExternal_Previews: PreviewProvider {
static var previews: some View {
UpdateTextViewExternal(viewModel: ExternalModel())
}
}

Resources