Using the new #StateObject to pass model to multiple views? - swiftui

If you wanted to pass a model between multiple views can you use #StateObject multiple times (see example below) so that each view can modify the object? I appreciate that this would be better done using #EnvironmentObject but was just trying to understand how this new property wrapper works in this situation.
// MARK: - MODEL
class Model: ObservableObject {
#Published var temperature = 27.5
}
.
// MARK: - APP.SWIFT
#main
struct SwiftUI_DataFlow_005App: App {
#ObservedObject var model = Model()
var body: some Scene {
WindowGroup {
ContentView(model: model)
}
}
}
.
// MARK: - CONTENT VIEW
struct ContentView: View {
#StateObject var model: Model
var body: some View {
VStack {
Text("\(model.temperature)")
AnotherView(model: model)
}
}
}
// MARK: - ANOTHER VIEW
struct AnotherView: View {
#StateObject var model: Model
var body: some View {
Text("\(model.temperature)")
}
}

#StateObject is source of truth, so it must be created in one place, so (schematically as you provided screenshot, but not a code to modify)
in App >
#StateObject private var model = Model()
...
ContentView().environmentObject(model)
...
in ContentView >
#EnvironmentObject var model: Model
...
in AnotherView >
#EnvironmentObject var model: Model

A small expansion based on the accepted answer so future readers can gain a little more insight.
1 - #ObservedObject
APPLE: Managing Model Data in Your App
It would seem from the Apple docs that one solution would be to use #ObservedObject on all three views.
// MARK: - CONTENT VIEW
struct ContentView: View {
#ObservedObject var model: Model
var body: some View {
VStack {
Text("\(model.temperature)")
AnotherView(model: model)
}
}
}
.
// MARK: - ANOTHER VIEW
struct AnotherView: View {
#ObservedObject var model: Model
var body: some View {
Text("\(model.temperature)")
}
}
2 - #EnvironmentObject
Another solution #asperi and by far the simplest is to create the model in app.swift as #StateObject and then inject the object into the environment. This then means you don't have to pass the model through the hierarchy, but rather access it as needed using #EnviromentObject.
// MARK: - APP.SWIFT
#main
struct SwiftUI_DataFlow_005App: App {
#StateObject var model = Model()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(model)
}
}
}
.
// MARK: - CONTENT VIEW
struct ContentView: View {
#EnvironmentObject var model: Model
var body: some View {
VStack {
Text("\(model.temperature)")
AnotherView()
}
}
}
.
// MARK: - ANOTHER VIEW
struct AnotherView: View {
#EnvironmentObject var model: Model
var body: some View {
Text("\(model.temperature)")
}
}

Related

Infinite loop when setting a #Published property

I have a simple view that shows some photos, through a list. Clicking on any row should display a detailed view of that photo. I'm using the MVVM pattern. However, an infinite loop occurs when I try to set the “selectedPhoto” property of the view model. Is there any way to avoid this loop without having to create a property in the detailed view itself?
Here is the Photo struct:
struct Photo: Identifiable {
var id = UUID()
var name: String
}
Here is the ContentView with an extension (the “updatePhoto” method is causing the infinite loop):
struct ContentView: View {
#StateObject private var viewModel = ViewModel()
var body: some View {
NavigationView {
List {
ForEach(viewModel.photos) { selectedPhoto in
showDetailView(with: selectedPhoto)
}
}
.navigationTitle("Favorite Photo")
}
}
}
extension ContentView {
func showDetailView(with selectedPhoto: Photo?) -> some View {
if let selectedPhoto = selectedPhoto {
viewModel.updatePhoto(selectedPhoto)
}
return DetailView(viewModel: viewModel)
}
}
Here is the view model:
class ViewModel: ObservableObject {
#Published var photos = [
Photo(name: "Photo 1"),
Photo(name: "Photo 2"),
Photo(name: "Photo 3")
]
#Published var selectedPhoto: Photo?
func updatePhoto(_ selectedPhoto: Photo?) {
self.selectedPhoto = selectedPhoto
}
}
And here is the DetailView:
struct DetailView: View {
#ObservedObject private var viewModel: ViewModel
init(viewModel: ViewModel) {
self.viewModel = viewModel
}
var body: some View {
Text(viewModel.selectedPhoto?.name ?? "Unknown photo name")
}
}
Try this approach, using a NavigationLink to present the DetailView,
and passing the selectedPhoto to it using #State var selectedPhoto: Photo.
struct Photo: Identifiable {
let id = UUID()
var name: String
}
class ViewModel: ObservableObject {
#Published var photos = [Photo(name: "Photo 1"),Photo(name: "Photo 2"),Photo(name: "Photo 3")]
}
struct DetailView: View {
#State var selectedPhoto: Photo
var body: some View {
Text(selectedPhoto.name)
}
}
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
NavigationView {
List {
ForEach(viewModel.photos) { selectedPhoto in
NavigationLink(selectedPhoto.name, destination: DetailView(selectedPhoto: selectedPhoto))
}
}
.navigationTitle("Favorite Photo")
}
}
}
Note that NavigationView is being deprecated and you will have to use NavigationStack instead.

SwiftUI incorrect navigation behavior in iOS 14.2

The view navigation hierarchy of my code is as follows:
ColorsView
WarmColorsView
RedView
CoolColorsView
Which is ColorsView can navigate directly to WarmColorsView and CoolColorView, and WarmColorsView can navigate directly to RedView.
Here is code (very simple):
import SwiftUI
class Model: ObservableObject {
#Published var tagNavToWarmOrCool: String?
#Published var isNavToRed = false
}
struct RedView: View {
#EnvironmentObject var model: Model
var body: some View {
VStack {
Button("to cool colors"){
model.isNavToRed = false
model.tagNavToWarmOrCool = "cool"
}
}
}
}
struct CoolColorsView: View {
var body: some View {
VStack {
}
.navigationTitle("Cool Colors")
}
}
struct WarmColorsView: View {
#EnvironmentObject var model: Model
var body: some View {
NavigationLink("red", destination: RedView(), isActive: $model.isNavToRed)
.navigationTitle("Warm Colors")
}
}
struct ColorsView: View {
#EnvironmentObject var model: Model
var body: some View {
VStack {
NavigationLink("to warm colors", destination: WarmColorsView(), tag: "warm", selection: $model.tagNavToWarmOrCool)
NavigationLink("to cool colors", destination: CoolColorsView(), tag: "cool", selection: $model.tagNavToWarmOrCool)
}
.navigationTitle("Colors")
}
}
struct ContentView: View {
#StateObject var model = Model()
var body: some View {
NavigationView {
ColorsView()
}
.navigationViewStyle(StackNavigationViewStyle())
.environmentObject(model)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(Model())
}
}
My intention is to go to the RedView, and then click the button to navigate to the CoolColorsView.
Running in iOS 14.2, however it ends up navigating to the ColorsView, I tried to change NavigationView's style to default, but it didn't work.
There is no such problem in iOS 15.4.1!
So how to navigate from RedView to CoolColorsView by click button in RedView in iOS 14.2? Thanks a lot! :)

How to use #FocusState with view models?

I'm using view models for my SwiftUI app and would like to have the focus state also in the view model as the form is quite complex.
This implementation using #FocusState in the view is working as expected, but not want I want:
import Combine
import SwiftUI
struct ContentView: View {
#ObservedObject private var viewModel = ViewModel()
#FocusState private var hasFocus: Bool
var body: some View {
Form {
TextField("Text", text: $viewModel.textField)
.focused($hasFocus)
Button("Set Focus") {
hasFocus = true
}
}
}
}
class ViewModel: ObservableObject {
#Published var textField: String = ""
}
How can I put the #FocusState into the view model?
Assuming you have in ViewModel as well
class ViewModel: ObservableObject {
#Published var hasFocus: Bool = false
...
}
you can use it like
struct ContentView: View {
#ObservedObject private var viewModel = ViewModel()
#FocusState private var hasFocus: Bool
var body: some View {
Form {
TextField("Text", text: $viewModel.textField)
.focused($hasFocus)
}
.onChange(of: hasFocus) {
viewModel.hasFocus = $0 // << write !!
}
.onAppear {
self.hasFocus = viewModel.hasFocus // << read !!
}
}
}
as well as the same from Button if any needed.
I faced the same problem and ended up writing an extension that can be reused to sync both values. This way the focus can also be set from the view model side if needed.
class ViewModel: ObservableObject {
#Published var hasFocus: Bool = false
}
struct ContentView: View {
#ObservedObject private var viewModel = ViewModel()
#FocusState private var hasFocus: Bool
var body: some View {
Form {
TextField("Text", text: $viewModel.textField)
.focused($hasFocus)
}
.sync($viewModel.hasFocus, with: _hasFocus)
}
}
extension View {
func sync<T: Equatable>(_ binding: Binding<T>, with focusState: FocusState<T>) -> some View {
self
.onChange(of: binding.wrappedValue) {
focusState.wrappedValue = $0
}
.onChange(of: focusState.wrappedValue) {
binding.wrappedValue = $0
}
}
}

update View when view model's, publish object's, property is updated

How to update view, when view models publish var's (user's) , name property is updated. I do know why its happening but what is the best way to update the view in this case.
class User {
var id = "123"
#Published var name = "jhon"
}
class ViewModel : ObservableObject {
#Published var user : User = User()
}
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
userNameView
}
var userNameView: some View {
Text(viewModel.user.name)
.background(Color.red)
.onTapGesture {
viewModel.user.name += "update"
print( viewModel.user.name)
}
}
}
so one way i do it, is by using onReceive like this,
var body: some View {
userNameView
.onReceive(viewModel.user.$name){ output in
let tmp = viewModel.user
viewModel.user = tmp
print("onTapGesture",output)
}
}
but it is not a good approach it will update all view using users properties.
should i make a #state var for the name?
or should i just make a ObservedObject for user as well?
Make you class conform to ObservableObject
class User: ObservableObject {
var id = "123"
#Published var name = "jhon"
}
But he catch with that is that you have to observe it directly you can't chain it in a ViewModel
Use #ObservedObject var user: User in a View
You should use struct:
import SwiftUI
struct User {
var id: String
var name: String
}
class ViewModel : ObservableObject {
#Published var user : User = User(id: "123", name: "Mike")
}
struct ContentView: View {
#ObservedObject var viewModel: ViewModel = ViewModel()
var body: some View {
userNameView
}
var userNameView: some View {
Text(viewModel.user.name)
.background(Color.red)
.onTapGesture {
viewModel.user.name += " update"
print( viewModel.user.name)
}
}
}

SwiftUI: Passing an environmentObject to a sheet causes update problems

If you want to pass an #EnvironmentObject to a View presented as a sheet, you'll notice that this sheet gets recreated every single time any #Published property in the #EnvironmentObject is updated.
Minimum example that demonstrates the problem:
import SwiftUI
class Store: ObservableObject {
#Published var name = "Kevin"
#Published var age = 38
}
struct ContentView: View {
#EnvironmentObject private var store: Store
#State private var showProfile = false
var body: some View {
VStack {
Text("Hello, \(store.name), you're \(store.age) years old")
Button("Edit profile") {
self.showProfile = true
}
}
.sheet(isPresented: $showProfile) {
ProfileView()
.environmentObject(self.store)
}
}
}
struct ProfileView: View {
#EnvironmentObject private var store: Store
#ObservedObject private var viewModel = ViewModel()
var body: some View {
VStack {
Text("Hello, \(store.name), you're \(store.age) years old")
Button("Change age") {
self.store.age += 1
}
}
}
}
class ViewModel: ObservableObject {
init() {
print("HERE")
}
}
If you run this code, you'll notice that "HERE" gets logged every single time you press the button in the sheet, meaning that the ViewModel got recreated. This can be a huge problem as you might imagine, I expect the ViewModel to not get recreated but retain its state. It's causing huge problems in my app.
As far as I am aware, what I am doing in my code is the normal way to pass the #EnvironmentObject to a sheet. Is there a way to prevent the ProfileView from getting recreated any time something in the Store changes?
This is because the view gets recreated when a state variable changes. And in your view you instantiate the viewModel as ViewModel().
Try passing the observed object as a param and it won't hit "HERE" anymore:
struct ContentView: View {
#EnvironmentObject private var store: Store
#State private var showProfile = false
#ObservedObject private var viewModel = ViewModel()
var body: some View {
VStack {
Text("Hello, \(store.name), you're \(store.age) years old")
Button("Edit profile") {
self.showProfile = true
}
}
.sheet(isPresented: $showProfile) {
ProfileView(viewModel: self.viewModel)
.environmentObject(self.store)
}
}
}
struct ProfileView: View {
#EnvironmentObject private var store: Store
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
Text("Hello, \(store.name), you're \(store.age) years old")
Button("Change age") {
self.store.age += 1
}
}
}
}
If your Deployment Target is iOS14 and above, have you tried replacing #ObservedObject with #StateObject in ProfileView? This will help in keeping the state, it will only be created once, even if the Model View instantiaton happens inside the View's body.
A very nice article about this issue can be found her.