SwiftUI: Property wrappers explained in simplest way

Manisha Roy
Globant
Published in
5 min readJun 29, 2022

Note: Do checkout this article for briefed introduction to SwiftUI

The main goal of introducing property wrappers is wrapping properties with logic which can be extracted into the separated struct or class to reuse it across the codebase. We know that all our views are structs, which means they can’t be changed but since property wrappers are managed by SwiftUI, whenever the property wrapper value changes, the view invalidates its appearance and recomputes the body.

SwiftUI gives us a vast list of property wrappers, some of them are State, Binding, ObservedObject, EnvironmentObject, and Environment Property Wrappers. So let’s try to understand the differences between them and when, why and which one we have to use.

@State

  • @State is great for simple properties that belong to a specific view and never get used outside that view, so as a result it’s important to mark those properties as being private to reinforce the idea that such a state is specifically designed never to escape its view.
  • when we use @State to declare a property, we hand control over it to SwiftUI so that it remains persistent in memory for as long as the view exists.
struct StateEgView: View {
var propertyToUpdate: Int = 0
var body: some View {
VStack {
Text(“StateEgView”)
Text(“parentUpdate is \(self.propertyToUpdate)”)
.onTapGesture {
self.propertyToUpdate += 1
}
}
}
}

We want to update property propertyToUpdate on some tap gesture but here we will get error “Left side of mutating operator isn’t mutable: ‘self’ is immutable”

To make it mutable, we need to use SwiftUI property wrappers over here. As propertyToUpdate needs to be updated in it’s owner view so we can use @State property wrapper like this and everything will be working as expected.

@State var propertyToUpdate: Int = 0

If we pass propertyToUpdate to children then receiving property will automatically adapt the State behaviour.

@Binding

  • @Binding provides reference like access for a value type. Sometimes we need to make the state of our View accessible for its children. But we can’t simply pass that value because it is a value type and Swift will pass the copy of that value. And this is where we can use @Binding Property Wrapper.
  • We also use $ to pass a binding reference, because without $ Swift will pass a copy of the value instead of passing bindable reference

Binding does not support value at the time of declaration.

struct ParentView: View {
@State
var propertyToUpdate: Int = 0
var body: some View {
VStack {
Text(“StateEgView”)
Text(“parentUpdate is \(
self.propertyToUpdate)”)
Spacer()
BindingEgView(childUpdate: $propertyToUpdate)
}
}
}
struct BindingEgView: View {
@Binding
var childUpdate: Int
var body: some View {
VStack {
Text(“BindingEgView”)
Text(“childUpdate is \(
self.childUpdate)”)
.onTapGesture {
self.childUpdate += 1
}
}
}
}

here propertyToUpdate is declared as State inside ParentView and initialed to 0 also BindingEgView is having childUpdate property as Binding. Now when BindingEgView is added as child of ParentView it requires value to be passed in it constructor so we pass $propertyToUpdate. Now we have made the relationship like whenever childUpdate is modified it send the acknowledgement to the parent view and hence parent view update it’s appearance as well.

@ObservedObject

  • ObservedObject is similar to State but when you have a custom type you want to use that might have multiple properties and methods, or might be shared across multiple views — you should use @ObservedObject instead.
  • Whatever type you use with @ObservedObject should conform to the ObservableObject protocol. When you create properties on observable objects you get to decide whether changes to each property should force the view to refresh or not. You usually will, but it’s not required.
  • There are several ways for an observed object to notify views that important data has changed, but the easiest is using the @Published property wrapper.

ObservedObject can be used with classes only since ObservableObject protocol supports only class

Here we can see that CountObserver is having two published property wrapper parentUpdate and childUpdate who will update the view whenever their value changes(automatic action) whereas total is a plain property. calculateTotal() is called on a button action which calculated the total taps performed by user. Now this function is having objectWillChange.send() which sends manual signal to the view indicating that total is been calculated and they can update the view.

@StateObject

  • StateObject property wrappers works similar to ObservedObject but they difference in their life-cycle. Lets first understand the similarity by modifying the above eg like this
struct StateObjectEgView: View {
@State var grandParentUpdate: Int = 0
var body: some View {
VStack {
Text(“grandParentUpdate is \(self.grandParentUpdate)”)
.onTapGesture {
self.grandParentUpdate += 1
}.padding(30)
ObservedObjectEgView()
}
}
}
struct ObservedObjectEgView: View {
@ObservedObject var counter = CountObserver()
var body: some View {
……
  • Here we have made ObservedObjectEgView as child of StateObjectEgView and hence whenever the property of StateObjectEgView force view to update it’s appearance then counter get reset.

ObservedObject property get destroyed once their containing view struct redraws whereas StateObject don’t get destroyed. Just look at the below code snippet where we have only replaced ObservedObject with StateObject and counter dosn’t get reset.

@EnvironmentObject

  • Suppose there is multiple hierarchy view as lowerView is child of middleView who is child of topView. If we want to update lowerView whenever there is update on propertyA from topView and vice-versa. This can be achieved with both State and ObservedObject but the only problem is we have to declare propertyA to topView and bind it to both lower views and send propertyA in their constructor. Even though middleView not accessing propertyA then also it have to have it. In solution to this we have next property wrapper that is EnvironmentObject.
  • EnvironmentObject is created with and stored at application level. it’s shared data that every view can read/write if they want to
  • Since all views point to the same model, if one view changes the model all views immediately update — there’s no risk of getting different parts of your app out of sync.

@Environment

  • By marking our properties with @Environment Property Wrapper, we access and subscribe to changes of system-wide settings
  • @Environment is great for reading out things like a Core Data managed object context, whether the device is in dark mode or light mode, what size class your view is being rendered with, and more — fixed properties that come from the system

It’s hard to list all Property wrappers here and keep them up to date. The best way to find them is by diving into the documentation topics.

If you liked this article then please appreciate it with claps and comments. This will really encourage me to write more!!!!

--

--

Manisha Roy
Globant
Writer for

An enthusiastic iOS Developer. Keep learning!!