I understand that EnvironmentObject property wrapper can be used to pass around objects to views. I have a session object which I am passing around to my views. Now I have a requirement to pass this into one of my model classes (i.e., non-view). Ideally, this model (receiving the session object) is instantiated as a StateObject.
struct CreditDetailsView: View {
#EnvironmentObject var session: Session
#StateObject var transactionsModel = TransactionsModel(token: session.token)
The code above will not work (understandably) because:
cannot use instance member 'session' within property initializer; property initializers run before 'self' is available
Any suggestions on how I can pass in the session into TransactionsModel?
Try initializing the StateObject in an .onAppear() prop to a child view, like this:
struct CreditDetailsView: View {
#EnvironmentObject var session: Session
#StateObject var transactionsModel: TransactionModel?
var body: some View {
SomeChildView()
.onAppear(perform: {
transactionModel = TransactionModel(token: session.token)
})
}
}
This way, the variable is initialized when the view renders on the screen. It doesn't matter much which child view you add the onAppear prop to, as long as it is rendered as soon as the parent does.
The best way that I've found to do this (because you cannot have an optional StateObject) is:
struct CreditDetailsView: View {
#EnvironmentObject var session: Session
#StateObject var localModel = LocalModel()
var body: some View {
SomeChildView()
.onAppear {
localModel.transactionModel = TransactionModel(token: session.token)
}
}
class LocalModel: ObservableObject {
#Published transactionModel: TransactionModel?
}
}
This is an incorrect answer. Please check out the chosen answer above.
You can access session object in init. In this case, transactionsModel should be done to be already initialized in any ways.
#EnvironmentObject var session: Session
#StateObject var transactionsModel = TransitionalModel(token: "")
init() {
let token = self.session.token
_transactionsModel = StateObject(wrappedValue: TransitionalModel(token: token))
}
Although it's out of the question, I am not sure if it's good way to pass something between them who look like being in different levels in the level of View.
Related
I am trying to pass a value from my View LingView, to another View ContentView. I have a state variable in the login view as such:
#State var userName: String = ""
#State private var password: String = ""
I then pass the value from the Content View Constructor in two places:
This is in RootView
var body: some View {
ContentView(userName: LoginView().$userName)
.fullScreenCover(isPresented: $authenticator.needsAuthentication) {
LoginView()
.environmentObject(authenticator) // see note
}
}
This is in Content View:
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(userName: Login().$userName)
.environmentObject(Authenticator())
}
}
I want to use the variable to pass into the getPictures() function that a database file uses. I am kind of confused as to what I am suppose to do. The parent view in this case would be LoginView correct? I am unsure why I keep getting: Cannot find type 'userName' in scope.
#Binding var userName: String
print(userName)
var pictureURL = DbHelper().getPictures()
After running the following code. I understand that you should make state private to a View, but in this case how would I pass the state value to the content view? The LoginView does not call the ContentView directly. Maybe I don't understand Bindings and State, but I have read this article: https://learnappmaking.com/binding-swiftui-how-to/
You are initialising two separate login views. The username binding passed to ContentView is therefore a different binding to the one you have under .fullscreenCover
To make it work, you can declare a State variable in RootView,
#State private var userName: String = "" // In RootView
then pass its binding to both ContentView and LoginView.
#Binding var userName: String // In ContentView and LoginView
Simply put, State holds your actual username value, while Binding gives you a method of seeing and changing it from somewhere else.
i'm trying to do a simple login screen using swiftUI.
i put my isLogin into a class and set it as an EnviromentObject Variable.
How do I use it in a fullscreen cover?
My class
class AuthUser:ObservableObject {
#Published var isLogin:Bool = false
#Published var isCorrect:Bool = false
}
My View :
struct UIController: View {
#EnvironmentObject var userAuth : AuthUser
#State var showLogin:Bool = false
var body: some View {
homeLogin()
.fullScreenCover(isPresented: $showLogin, content: AfterLogin.init)
}
}
You can use binding directly to property of environment object, like
struct UIController: View {
#EnvironmentObject var userAuth : AuthUser
var body: some View {
homeLogin()
.fullScreenCover(isPresented: $userAuth.isLogin, content: AfterLogin.init)
}
}
The structure you're attempting to follow is called MVVM or Model-View-View-Model, which is nearly a requirement for swiftUI I believe the only thing that you're missing is the actual usage of that ViewModel, or in your case the AuthUser. So let's dig into that.
View Model
class AuthUser: ObservableObject {
#Published var isLogin = false
//Any other code, methods, constructors you want.
}
View
struct UIController: View {
#ObservedObject var userAuth = AuthUser()
var body: some View {
homeLogin()
.fullScreenCover(isPresented: $userAuth.isLogin, content: AfterLogin.init)
}
}
What did I change? I changed your #EnvironmentObject to a ObservedObject because in your case you're not likely to actually be using an environment object. What are those special tags?
ObservableObject, this means that it can be observed for state changes.
ObservedObject, an object that is watching for state changes.
Published, an object that has public availability.
EnvironmentObject, an object that is created for the environment and accessible wherever in the app. This particular object is not immediately disposed which is why we removed it in your example.
So what's the purpose of doing things this way? Well it has to do with abstraction and readability. MVVM frameworks provide a way to Bind a view to an object. That binding means that any changes on the view change the data, and any changes to the data change the view. Swift makes this concept simple enough. In your case we're binding the isPresented to the ObservedObject and checking that objects isLogin state. If it changes, the view is also changed.
In the code below (a stripped-down version of some code in a project) I'm using a MVVM pattern with two views:
ViewA - displays a value stored in an ObservableObject ViewModel;
ViewB - displays the same value and has a Slider that changes that value, which is passed to the view using Binding.
Inside of ViewModelA I have a computed property which serves both to avoid the View from accessing the Model directly and to perform some other operations when the value inside the model (the one being displayed) is changed.
I'm also passing that computed value to a ViewModelB, using Binding, which acts as a StateObject for ViewB. However, when dragging the Slider to change that value, the value changes on ViewA but doesn't change on ViewB and the slider itself doesn't slide. As expected, when debugging, the wrappedValue inside the Binding is not changing. But how is the change propagated upwards (through the Binding's setters, I imagine) but not downwards back to ViewB?? I imagine this can only happen if the variable is being duplicated somewhere and changed only in one place, but I can't seem to understand where or if that's what's actually happening.
Thanks in advance!
Views:
import SwiftUI
struct ContentView: View {
#StateObject var viewModelA = ViewModelA()
var body: some View {
VStack{
ViewA(value: viewModelA.value)
ViewB(value: $viewModelA.value)
}
}
}
struct ViewA: View {
let value: Double
var body: some View {
Text("\(value)").padding()
}
}
struct ViewB: View {
#StateObject var viewModelB: ViewModelB
init(value: Binding<Double>){
_viewModelB = StateObject(wrappedValue: ViewModelB(value: value))
}
var body: some View {
VStack{
Text("\(viewModelB.value)")
Slider(value: $viewModelB.value, in: 0...1)
}
}
}
ViewModels:
class ViewModelA: ObservableObject {
#Published var model = Model()
var value: Double {
get {
model.value
}
set {
model.value = newValue
// perform other checks and operations
}
}
}
class ViewModelB: ObservableObject {
#Binding var value: Double
init(value: Binding<Double>){
self._value = value
}
}
Model:
struct Model {
var value: Double = 0
}
If you only look where you can't go, you might just miss the riches below
Breaking single source of truth, and breaching local (private) property of #StateObjectby sharing it via Binding are two places where you can't go.
#EnvironmentObject or more generally the concept of "shared object" between views are the riches below.
This is an example of doing it without MVVM nonsense:
import SwiftUI
final class EnvState: ObservableObject {#Published var value: Double = 0 }
struct ContentView: View {
#EnvironmentObject var eos: EnvState
var body: some View {
VStack{
ViewA()
ViewB()
}
}
}
struct ViewA: View {
#EnvironmentObject var eos: EnvState
var body: some View {
Text("\(eos.value)").padding()
}
}
struct ViewB: View {
#EnvironmentObject var eos: EnvState
var body: some View {
VStack{
Text("\(eos.value)")
Slider(value: $eos.value, in: 0...1)
}
}
}
Isn't this easier to read, cleaner, less error-prone, with fewer overheads, and without serious violation of fundamental coding principles?
MVVM does not take value type into consideration. And the reason Swift introduces value type is so that you don't pass shared mutable references and create all kinds of bugs.
Yet the first thing MVVM devs do is to introduce shared mutable references for every view and pass references around via binding...
Now to your question:
the only options I see are either using only one ViewModel per Model, or having to pass the Model (or it's properties) between ViewModels through Binding
Another option is to drop MVVM, get rid of all view models, and use #EnvironmentObject instead.
Or if you don't want to drop MVVM, pass #ObservedObject (your view model being a reference type) instead of #Binding.
E.g.;
struct ContentView: View {
#ObservedObject var viewModelA = ViewModelA()
var body: some View {
VStack{
ViewA(value: viewModelA)
ViewB(value: viewModelA)
}
}
}
On a side note, what's the point of "don't access model directly from view"?
It makes zero sense when your model is value type.
Especially when you pass view model reference around like cookies in a party so everyone can have it.
Really it looks like broken single-source or truth concept. Instead the following just works (ViewModelB might probably be needed for something, but not for this case)
Tested with Xcode 12 / iOS 14
Only modified parts:
struct ContentView: View {
#StateObject var viewModelA = ViewModelA()
var body: some View {
VStack{
ViewA(value: viewModelA.value)
ViewB(value: $viewModelA.model.value)
}
}
}
struct ViewB: View {
#Binding var value: Double
var body: some View {
VStack{
Text("\(value)")
Slider(value: $value, in: 0...1)
}
}
}
If the entire views access the same model in an app, I think the Singleton pattern is enough. Am I right?
For example, if MainView and ChildView access the same model(e.g. AppSetting) like below, I cannot find any reason to use EnvironmentObject instead of the Singleton pattern. Is there any problem if I use like this? If it is okay, then when I should use EnvironmentObject instead of the Singleton pattern?
class AppSetting: ObservableObject {
static let shared = AppSetting()
private init() {}
#Published var userName: String = "StackOverflow"
}
struct MainView: View {
#ObservedObject private var appSetting = AppSetting.shared
var body: some View {
Text(appSetting.userName)
}
}
struct ChildView: View {
#ObservedObject private var appSetting = AppSetting.shared
var body: some View {
Text(appSetting.userName)
}
}
Thanks in advance.
You are correct there is no reason in this case to use an EnvironmentObject. Apple even encourages to make no excessive use of EnvironmentObjects.
Nevertheless an EnvironmentObject can be great too, if you use an object in many views, because then you don't have to pass it from View A to B, from B to C and so on.
Often you find yourself in a situation where even #State and #Binding will be enough to share and update data in a view and between two views.
I think when your app supports multiple windows via Scene (example Document based apps) maybe the singleton is not a solution and the EnvironmentObject is better.
Example you want to share selectedColor. When you use a singleton, the selectedColor will be same in entire app, in every scene (in every view in window). But if you want to use separated settings the EnvironmentObject is convenient.
I found singleton easier because you can use it in a class vs passing it in the class via .onAppear().
Singleton:
class ViewModel {
private let appSetting = AppSetting.shared
}
EnvironmentObject:
class SomeClass {
private var appSetting : AppSetting?
func addAppSetting(_ appSetting : AppSetting) {
self.appSetting = appSetting
}
}
struct MyView: View {
#EnvironmentObject var appSetting : AppSetting
#StateObject var someClass = SomeClass()
var body: some View {
ZStack {
}
.onAppear {
someClass.addAppSetting(appSetting)
}
}
}
I have a structure like this:
contentView {
navigationView {
foreach {
NavigationLink(ViewA(id: id))
}
}
}
/// where ViewA contains an request trigger when it appears
struct ViewA: View {
#State var filterString: String = ""
var id: String!
#ObservedObject var model: ListObj = ListObj()
init(id: String) {
self.id = id
}
var body: some View {
VStack {
SearchBarView(searchText: $filterString)
List {
ForEach(model.items.filter({ filterString.isEmpty || $0.id.contains(filterString) || $0.name.contains(filterString) }), id: \.id) { item in
NavigationLink(destination: ViewB(id: item.id)) {
VStack {
Text("\(item.name) ")
}
}
}
}
}
.onAppear {
self.model.getListObj(id: self.id) //api request, fill data and call objectWillChange.send()
}
}
}
ViewB has the same code as ViewA: It receives an id, stores and requests an API to collect data.
But the viewB list is not being refreshed.
I also noticed that viewB's model property
#ObservedObject var model: model = model()
was instantiated multiple times.
Debugging, I found that every navigationLink instantiates its destination even before it is triggered. That's not a problem usually, but in my case i feel like the ViewB model is being instantiated 2 times, and my onAppear call the wrong one, reason why self.objectWillChange.send() not refreshing my view.
There are two issues here:
SwiftUI uses value types that that get initialized over and over again each pass through body.
Related to #1, NavigationLink is not lazy.
#1
A new ListObj gets instantiated every time you call ViewA.init(...). ObservedObject does not work the same as #State where SwiftUI keeps careful track of it for you throughout the onscreen lifecycle. SwiftUI assumes that ultimate ownership of an #ObservedObject exists at some level above the View it's used in.
In other words, you should almost always avoid things like #ObservedObject var myObject = MyObservableObject().
(Note, even if you did #State var model = ListObj() it would be instantiated every time. But because it's #State SwiftUI will replace the new instance with the original before body gets called.)
#2
In addition to this, NavigationLink is not lazy. Each time you instantiate that NavigationLink you pass a newly instantiated ViewA, which instantiates your ListObj.
So for starters, one thing you can do is make a LazyView to delay instantiation until NavigationLink.destination.body actually gets called:
// Use this to delay instantiation when using `NavigationLink`, etc...
struct LazyView<Content: View>: View {
var content: () -> Content
var body: some View {
self.content()
}
}
Now you can do NavigationLink(destination: LazyView { ViewA() }) and instantiation of ViewA will be deferred until the destination is actually shown.
Simply using LazyView will fix your current problem as long as it's the top view in the hierarchy, like it is when you push it in a NavigationView or if you present it.
However, this is where #user3441734's comment comes in. What you really need to do is keep ownership of model somewhere outside of your View because of what was explained in #1.
If your #ObservedObject is being initialized multiple times, it is because the owner of the object is refreshed and recreated every time it has state changes. Try to use #StateObject if your app is iOS 14 and above. It prevents the object from being recreated when the view refreshes.
https://developer.apple.com/documentation/swiftui/stateobject
When a view creates its own #ObservedObject instance it is recreated
every time a view is discarded and redrawn. On the contrary a #State
variable will keep its value when a view is redrawn. A #StateObject is
a combination of #ObservedObject and #State - the instance of the
ViewModel will be kept and reused even after a view is discarded and
redrawn
What is the difference between ObservedObject and StateObject in SwiftUI