I have 3 swift classes, ContentView, LogIn and SignUP.
I can t make a boolean var which, let s say, is changing it s value in login.swift, to also announce contentview and signup about the new value.
Bassically, I have account = true, when account became false, i want all 3 classes contentview, login and signup to know that he s value is false now.
Any idea how can I do this? An example with simple 3 views will help me.
Thanks a lot!
You have 3 view right? ContentView, LogIn and SignUP. For all of those view you need to pass like
SignUP().environmentObject(account)
...
Login().environmentObject(account)
and in Views
struct LoginView: View {
#EnvironmentObject var account: Bool
...
}
struct SignUP: View {
#EnvironmentObject var account: Bool
...
}
Related
I have an app where there are multiple screens with textfields to create some new object. When the user selects "Create" on the last screen, an API call is performed which creates the new object.
From there I want to push the detail page of the newly created object, and (when the view is no longer visible) remove all the screens with textfields (as that is no longer relevant, and would only cause confusion. Luckily there is only one screen that should remain before the detailpage.
In UIKit, this would be performed by doing a push on the navigationController, and then editing the viewControllers array of the navigationController in the viewDidLoad of the new screen.
If I am correct, there is no way to edit the views in a SwiftUI NavigationView, so how can I perform this action in SwiftUI?
I solved it by adding an id to the NavigationView, and then setting this to another id in the viewmodel when it should reset.
Like this:
struct MyView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
NavigationView {
// ...
}
.id(viewModel.id)
}
}
In the viewmodel:
class ViewModel: ObservableObject {
#Published var id = UUID().uuidString
func reset() {
id = UUID().uuidString
}
}
This resets the NavigationView as Swift thinks it's a different view because the id changed.
I'm testing #ObservedObject to test how SwiftUI handles the lifecycle of the model.
As I understand things now, from WWDC videos and documentation, is that the View that creates the model object (a class adopting ObservableObject) should use #StateObject.
From WWDC videos they clearly state that #ObservedObject does not own the observed instance lifecycle.
So I created this simple setup:
struct TestParentView: View {
#ObservedObject var model:TestModel
var body: some View {
VStack {
Text("TEST TEXT. \(model.name)")
TestChildView(model: model)
}
}
}
struct TestParentView_Previews: PreviewProvider {
static var previews: some View {
TestParentView(model: TestModel())
}
}
struct TestChildView: View {
#ObservedObject var model:TestModel
var body: some View {
Button(action: {
print("change name")
model.changeName()
}, label: {
Text("Change Name")
})
}
}
struct TestChildView_Previews: PreviewProvider {
static var previews: some View {
TestChildView(model: TestModel())
}
}
The model I used here is this:
class TestModel:ObservableObject {
#Published var name:String = ""
let id = UUID()
func changeName() {
name = "\(UUID())"
}
deinit {
print("TestModel DEINIT. \(id)")
}
}
When I run this app, I was expecting the TestModel instance created in TestParentView to be de-initialized at least at some point since #ObservedObject does not own the lifecycle of the model.
When I tap on the button to trigger a name change all works, but the TestModel DEINIT never gets called.
From all of this, it looks like TestParentView has a strong reference to the TestModel and it never lets it go.
So, what do they mean then when they say that #ObservedObject does not manage the lifecycle of model in this case?
Why is the TestModel DEINIT never called if #ObservedObject does not manage the model's lifecycle?
I'm obviously missing something here.
Assume this View:
struct Foo: View {
#ObservedObject var model = TestModel()
var body: some View {
Text(model.name)
}
}
Every time this view is created, it instantiates a new instance of TestModel.
SwiftUI views are really more like view descriptions which are created and destroyed a lot during your app lifecycle. Therefore it's important that the structs are lightweight. The Foo view isn't very lightweight because every rendering pass it instantiates a new model.
If you instead used #StateObject, the framework will only instantiate a TestModel the first time. After that it will reuse that same instance. This makes it way more performant.
Rule of thumb:
If the view creates its own model (such as the Foo view), use #StateObject.
If the model is passed in from the outside, use #ObservedObject.
To answer your question: "Why is the TestModel DEINIT never called".
You state that: "When I run this app, I was expecting the TestModel instance created in TestParentView to be de-initialized at least at some point".
The subtle detail is that the TestModel is not created in the TestParentView, it's merely passed in.
It is created in the TestParentView_Previews. And since the body of the TestParentView_Previews is only executed once, the TestModel will also only be initialised once, and thus never deallocated.
I have created a simple example of the problem I'm facing. I have two views, ListView and EditView. ListView is observing the UsersViewModel (plural) which contain a list of user names.
Using NavigationLink I want to present a form where I can edit the user name and this is where I'm getting confused. I have created a UserViewModel (singular) which I have the EditView observing, but when I try to call the EditView passing the value from the ForEach loop, I get a type mismatch error as I am not passing a UserViewModel.
Maybe I am misunderstanding the observable object. I thought I could change the user name on the edit form, navigate back to the list view and I would see the change in the list.
struct ListView: View {
// Observe the Users View Model
#ObservedObject var usersViewModel = UsersViewModel()
var body: some View {
NavigationView {
List() {
ForEach(usersViewModel.users) { user in
// FAILS with cannot converted "user" to expected type userViewModel
NavigationLink (destination: EditView(userViewModel: user)) {
Text("Hello \(user.name)")
}
}
}
.navigationBarTitle("User List", displayMode: .inline)
.onAppear(){
usersViewModel.loadUsers()
}
The edit view
struct EditView: View {
#ObservedObject var userViewModel: UserViewModel
var body: some View {
TextField("User Name", text: $userViewModel.user)
cannot converted "user" to expected type userViewModel
You probably want to use the same UserViewModel in both views (assuming the user is a struct).
Change your EditView to expect a usersViewModel parameter in init:
struct EditView: View {
#ObservedObject var usersViewModel: UsersViewModel
and pass it in the parent view:
NavigationLink (destination: EditView(usersViewModel: usersViewModel))
Otherwise, as the user is probably a struct, you will modify the different copy of the user in the child view.
I have got around the issue by passing the usersViewModel to the editView rather than userViewModel as pawello2222 suggested, but obviously you need to know which element you want to edit.
So I have done this;
let users = usersViewModel.users.enumerated().map({ $0 })
NavigationView {
List() {
ForEach(users, id: \.element.id ) { index, user in
NavigationLink (destination: EditView(userViewModel: usersViewModel, index: index)) {
This seems long winded, surely having to enumerate the viewModel to provide the EditView with an index of which element to edit is not the correct way?
It works, but I would like to know the proper way
I understand how to use #EnvironmentObject to make global data available to any view class, but I can't find a way to pass the variable to a non-view class.
My situation, which seems like a common issue, is:
Login, returns an access token. This token is required to be used in all subsequent api calls
Store access token in a UserSettings class
Place the UserSettings in an Environment bucket
let contentView = ContentView()
.environmentObject(UserSettings())
Each view will display data based on the data returned in an api call
struct HomeView: View {
#EnvironmentObject var user: UserSettings <=== ACCESS TO GLOBAL DATA
#ObservedObject var categories = Categories() <=== GET DATA FOR THIS VIEW
var body: some View {
...
}
Categories() is a non-view Swift class that will retrieve the data, but requires the access token which is stored in UserSettings
class Categories : ObservableObject {
#Published var allCategories = CategoriesModel()
#EnvironmentObject var user: UserSettings <==== CANNOT ACCESS THIS DATA!
init() {
fetchCategories(page: 0, pageSize: 20)
}
UserSettings is empty because this class is not a View struct and doesn't seem to behave like a View class would. Since the access token I need is stored in the UserSettings class, how can I get access to this? Is there an alternative way to store/access this data?
Use a Singleton Pattern. Call UserSettings.sharedInstance from Categories & let contentView = ContentView().environmentObject(UserSettings.sharedInstance)
https://developer.apple.com/documentation/swift/cocoa_design_patterns/managing_a_shared_resource_using_a_singleton
In a SwiftUI app, I have an ObservableObject that keeps track of user settings:
class UserSettings: ObservableObject {
#Published var setting: String?
}
I have a view model to control the state for my view:
class TestViewModel: ObservableObject {
#Published var state: String = ""
}
And I have my view. When the user setting changes, I want to get the view model to update the state of the view:
struct HomeView: View {
#EnvironmentObject var userSettings: UserSettings
#ObservedObject var viewModel = TestViewModel()
var body: some View {
Text(viewModel.state)
.onReceive(userSettings.$setting) { setting in
self.viewModel.state = setting
}
}
}
When the UserSettings.setting is changed in another view it causes onReceive on my view to get called in an infinite loop, and I don't understand why. I saw this question, and that loop makes sense to me because the state of the ObservableObject being observed is being changed on observation.
However, in my case I'm not changing the observed object (environment object) state. I'm observing the environment object and changing the view model state which redraws the view.
Is the view redrawing what's causing the issue here? Does onReceive get called everytime the view is redrawn?
Is there a better way of accomplishing what I'm trying to do?
EDIT: this is a greatly simplified version of my problem. In my app, the view model takes care of executing a network request based on the user's settings and updating the view's state such as displaying an error message or loading indicator.
Whenever you have an onReceive with an #ObservedObject that sets another (or the same) published value of the #ObservedObject you risk creating an infinite loop if those published attributes are being displayed somehow.
Make your onReceive verify that the received value is actually updating a value, and not merely setting the same value, otherwise it will be setting/redrawing infinitely. In this case, e.g.,:
.onReceive(userSettings.$setting) { setting in
if setting != self.viewModel.state {
self.viewModel.state = setting
}
}
From described scenario I don't see the reason to duplicate setting in view model. You can show the value directly from userSettings, as in
struct HomeView: View {
#EnvironmentObject var userSettings: UserSettings
#ObservedObject var viewModel = TestViewModel()
var body: some View {
Text(userSettings.setting)
}
}
You might be able to prevent infinite re-rendering of the view body by switching your #ObservedObject to #StateObject.