In the section titled
“Why @State doesn’t work here”
it says
“Every time your data triggers an update in a view, SwiftUI recreates that view from scratch. When this happens, all properties on the view are initialized again.”
This is not the behaviour I am seeing. (SwiftUI 2.0, Xcode 12).
If there is an update in the view (some kind of change in state) then the plain var properties of the view aren’t initialised again. They retain their values as long as the view is part of the hierarchy.
So I don’t think the entire View is created from scratch at each update. My understanding is that the Body function of the View is simply called again to render the View again. It doesn’t re-initialize the simple (non wrapped) var properties and reset them to their default value
When I create a new iOS app with Xcode 12 there are no files, AppDelegate.swift, SceneDelegate.swift, LaunchScreen.swift and maybe others. Apple seems to have made some significant changes to Xcode or Swift with version 12. I’m following along with the App template, starter files, but wondered if there was a description of the changes and how to adjust for them. Probably off topic for this tutorial but a point in the right direction would be appreciated. Thanks
Hi @dbourne, when creating a new SwiftUI iOS app in Xcode 12, you have the option of choosing between two different life cycles:
UIKit App Delegate
SwiftUI App
In Xcode 11, you only had UIKit App Delegate. The sample project for this chapter follows the UIKit App Delegate lifecycle, so choose that one when creating your project and you’ll get AppDelegate.swift and all of its familiar friends.
Great, thanks. And you still get ContentView.swift which SceneDelegate.swift points to. Also, a LaunchScreen.storyboard. Using the SwiftUI App approach seems to leave out some option or I haven’t found them Some searching suggested rather involved methods for LaunchScreen. I haven’t looked for environment variables. Thanks keeganrush.
In the “Working With Internal State” one sees a UIScrollView does not support multiple observers... error after build & run (Xcode 12.0.1, macOS 10.15.7). If the NavigationView is eliminated, so the body starts with a Form view instead, it still works and the error is eliminated. Reading elsewhere, I gather this is because we have a NavigationView already defined elsewhere.
Do ObservedObjects or EnvironmentalObjects need to be instantiated when they are declared? For example, the class UserStore is an ObservableObject and has no init but one variable, currentUserInfo, that is defined as an optional. Then UserStore is declared as an EnvironmentalObject in MovieList, AddMovie and UserView but is never instantiated. When is it required to be, or not? In other tutorials, I see examples both ways.
Do ObservedObjects or EnvironmentalObjects need to be instantiated when they are declared?
“Yes, but actually, no”. Every instance variable in a struct or class (including ObservableObjects and EnvironmentObjects) needs an initial value. However, for Optional types, the Swift compiler assigns an initial value of Optional<T>.none if you don’t set an initial value yourself.
This means that optional ObservableObjects and EnvironmentObjects have an initial value of nil even if you don’t assign a value yourself.
hi Thomas! I think I interpret your question a slightly different way.
Like StateObject, you create an EnvironmentObject exactly once. If a view uses an EnvironmentObject, you must create the model object by calling the environmentObject(_:) modifier on an ancestor view.
MovieList uses UserStore so it’s instantiated in SceneDelegate.swift:
let contentView = MovieList().environmentObject(UserStore())
This is the first code line in Using the Environment. It’s easy to miss. Maybe that’s why some tutorials don’t seem to create it.
Audrey,
Thanks for the additional clarification. I am currently working in WatchOS where there is no scene delegate so I did not see where UserStore was being instantiated. For WatchOs, it is instantiated in the App.
In addition, it is not clear to me why UserStore is using an EnvironmentObject while MovieStore is using a StateObject. And furthermore, why MovieList instantiates MovieStore while AddMovie does not.
Thanks,Tom
Just joined site the other day and your answer to this question hit the nail on the head for me. Thank you for confirming the only answer I could think of myself.
Hi Tom! I didn’t want to hijack Keegan’s forum but since I’m here to reply to the other Tom…
The latest Xcode replaced AppDelegate and SceneDelegate with App for iOS too, so that’s where you instantiate an environment object if you want it available to all views in the app.
I’ve written an app where ContentView just has a TabView, so I instantiate the environment object on the TabView. Whether you use StateObject or EnvironmentObject is a matter of convenience. Both are instantiated only once, so you can’t instantiate them again in subviews.
You have to pass a StateObject to the owning view’s subviews as a parameter, and the subviews declare it as an ObservedObject. Similar to State and Binding for values.
If you make an architectural design decision not to pass your ObservableObject to every subview that needs it (or sometimes, to a subview only so it can pass it to one of its subviews), then you “inject” it as an EnvironmentObject.
In this article, UserStore could’ve been a StateObject but I guess just about every view uses it, and Keegan needed a reason to show you how to use an EnvironmentObject ;].
I frequently see the @State property wrapper is defined in the ContentView. Does it have to be? Does the parent view need to contain the @State var or can child views declare @State as well?
For example, could the genre picker view declare the genre var as a @State variable and then have ContentView define genre as a @Binding?
On another topic, can the @Binding wrapper be used in multiple structs? Can it be used in a struct that is not a View?
In this specific scenario, @tomws, reversing the flow in that manner would create unnecessary headaches. Also, I don’t see how it would compile. In GenrePicker’s parent view (either AddMovie or UserView), you need to provide a value for genre on initialization. If you were to make genre a binding and not a state variable, what would you bind it to? GenrePicker is only instantiated when body is created, and it’s recreated whenever body refreshes.
You can use @Binding in a regular struct that isn’t a View.
@tomws If you’re looking to learn more about how SwiftUI’s data flow works, I recommend you read the SwiftUI by Tutorials book. There’s a whole chapter on data flow!
Keegan,
Thanks. This really helps. I was mistakenly thinking that the @State wrapper should be in the View that can modify the property, such as the Picker view. Now I understand the @State property needs to be in the Parent view, often the ContentView.