I’m taking a moment out of my coursework to try and get my head around Property Wrappers.
When writing PHP code in the past, variables/objects were either local or global. If you used them within a function, then they were local to that function. If you wanted to use a variable from outside the function or have it available outside that function, you’d declare it with the global
keyword.
This was refered to as the ‘scope’ in which the variable was available. It started to get a little more complicated when I transitioned my code into OOP, but I didn’t get chance to complete my transition to OOP so my knowledge is limited.
With SwiftUI, we have “property wrappers” which appear to achieve the same ends, but my head is still hooked on the past. So I’m trying to get it clear in my head what we’re actually doing when we use property wrappers.
The main reason for the mental block in my head is because SwiftUI is centred around one or more ‘views’ that are subject to change. Being a web-based script, PHP code was generally there to create a static HTML page that didn’t change unless you refreshed the page and the entire PHP script got executed again. That’s not the case with an app where we need to act on user input or some other change instantly.
“Property Wrappers” are needed because a SwiftUI view is “immutable” (i.e., cannot be changed), like the aforementioned page generated by a PHP script. But a view can contain “mutable” data (data that can be changed). If the data is changed, the view needs to be “recreated” to reflect that change, otherwise the view won’t change.
A “property wrapper” is a way of letting SwiftUI know that data has changed, and so the view needs to be recreated.
As I understand it, the immutable View exists within a mutable State. The State can contain data and, if that data changes so does the State which then prompts the View to be recreated.
The Property Wrappers I’m going to attempt to summarise here are @State
, @StateObject
, @ObservedObject
, and @EnvironmentObject
, and only in the context in which we’ve used them so far in the coursework. I’m sure there is much more to learn about them.
@State
Local state value.
@State var score = 1
@State
is for simple values (Int
, String
, Bool
, etc), not for complex reference types (such as class
, struct
, etc). If the variable score
changes within the local scope (such as from an action in a view within the state), then the body
will be recreated because the State has changed.
Although @State
is not for complex reference types, the following will not cause an error:
@State var obj = classObject()
However, it will also not cause the body
to be recreated. This is because classObject()
is not part of the State, only the variable obj
is and the variable doesn’t see the change to the object, which means the State hasn’t change and so it does not trigger the body to be recreaed.
@StateObject
Local state object.
@StateObject var userSettings = classObject()
@StateObject
is for an observable object rather than a simple value. The object classObject()
has to conform to the ObservableObject
protocol and the properties that should trigger a state change should be marked with a @Published
property wrapper.
class classObject: ObservableObject {
@Published var userEmail: String "a@b.com"
}
The view can update the state object, thus:
userSettings.userEmail = "c@d.com"
The view will be recreated because the state knows that the StateObject
has changed because userEmail
is @Published
within an ObservableObject
.
@ObservedObject
Child view accessing the properties of a parent’s state object.
@ObservedObject var obsObj: classObject
To access the properties of a StateObject
in a child view (a view shown after a button tap, for example), the StateObject
needs to be passed to that child view because the object has already been instantiated.
Apparently, @ObservedObject
was previously known as @BindableObject
(but I don’t know anything about that).
The main view with the StateObject
may contain a link:
NavigationLink("To child", destination: ChildView(obsObj: userSettings))
The child view accesses the properties of the object via an @ObservedObject
wrapper, which accepts an object of type classObject
as previously declared.
The child view may then update a property of the observed object, thus:
obsObj.userEmail = "e@f.com"
This updates the property of the StateObject
that was instantiated and is used by the parent view.
@EnvironmentObject
All sub-views accessing a global object.
@EnvironmentObject var envObj: classObject
The environment encompasses all local states (views) within it. An environment object can therefore be used by all states it encompasses, but the object needs to be declared in the top-most view in order to be accessible.
Within @main
we would append the .environmentObject
modifier to the view that calls all sub-views, thus:
@main
struct TestApp: App {
@StateObject var envObj = classObject()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(envObj)
}
}
}
To then use the object in ContentView()
and any view within ContentView()
, we’d use this:
@EnvironmentObject var envObj: classObject
And to update the object, we’d use:
envObj.userEmail = "g@h.com"
This is just like @ObservedObject
except that we’re expecting the object to be in the Environment, rather than being passed from the parent view. If there are a lot of objects that may be passed to many views and sub-views, having them available from the Environment would be better than passing them all as @ObservedObject
.
@Binding
On trying to sort the above out in my head, I’ve come across other wrappers including @Binding
. Although we have used “two-way binding” using the string/dollar ($) sign, we haven’t yet used the @Binding
property wrapper so I won’t try to detail it here at this time.
The above is for my own reference, and I make no apologies for any inaccuracies. I will no doubt update things as I learn more.