Property Wrappers SwiftUI

Swift Properties: A Deep Dive into Property Wrappers vs. Property Observers

Demonstrating the use cases of Property Wrappers in SwiftUI

Kanagasabapathy Rajkumar

--

Before heading to the differences between Property Wrappers and Property Observers, we must understand the key concepts of properties available in Swift programming language.

After reading the Trozware article, I successfully created a project demonstrating each Property Wrapper in SwiftUI.

What are properties?

Properties associate values with a particular class, structure, or enumeration.

Stored Properties

Properties either be a constant (let keyword) or variables stored (var keyword).

struct Person {
let name: String
var age: Int
}

var person = Person(name: "Saba", age: 32)
person.age = 33

This is the default behaviour of the Stored properties with Structure (Value types). If the instance of structure is constant, then its properties will be constant.

In the case of reference-type - classes, we can modify their properties even if the instance is marked as constant.

We cannot use Stored property in Protocol extensions.

Computed Properties

We can define a computed property with classes, structures, and enums which don’t store a value. Instead, they provide a getter and an optional setter.

For example: I will add the computed properties such as getter and getter and setter to the struct — Person.

 var fullName: String {
return "\(name) \(initial)"
}
var isAdult: Bool {
get {
return age >= 18
}
set {
if newValue {
age = 18
} else {
age = 0
}
}
}

print(person.fullName) // Saba R
print(person.isAdult) // true
person.age = 17
print(person.isAdult) // false

Lazy Properties

Property whose initial value isn’t calculated until the first time it’s used.

In simple words, it is beneficial when you want to delay the initialization of the property until it is actually needed. It improves the performance.

For example: I will add the lazy stored property to the struct — Person.

lazy var lazyProperty: String = {
print("Performing Lazy Operation")
return "Result of lazyProperty"
}()

//

print(person.lazyProperty) // Performing Lazy Operation
// Result of lazyProperty
print(person.lazyProperty)
// Result of lazyProperty

Downsides of using lazy var:

  1. Working with structs requires using mutating functions, even though lazy vars are said to be mutating.
  2. Lazy variables are not thread-safe. This means they can be executed multiple times due to different thread accesses.

Property Wrappers

A property wrapper adds a layer of separation between the code that manages how a property is stored and the code that defines a property.

I would like to cover the property wrapper in Swift Programming.

@propertyWrapper
struct SomeStruct {
private var value = 2
var wrappedValue: Int {
get { return value }
set { value = min(newValue, 7)}
}
}
struct SomeCuboid {
@SomeStruct var height: Int
@SomeStruct var length: Int
@SomeStruct var width: Int
}
var cuboid = SomeCuboid()
print(cuboid.length) // 2
cuboid.length = 20
print(cuboid.length) // 7

A property wrapper can expose additional functionality by using $. Hereby I will add the following SomeStruct.

private(set) var projectedValue: Bool
init () {
self.value = 0
self.projectedValue = false
}

print(cuboid.$length)
print(cuboid.length)

SwiftUI

SwiftUI offers Declarative programming for User Interface design.

Model Data — SwiftUI

In SwiftUI, we have a list of property wrappers. I would like to cover some of the wrappers in this article.

Persistent Storage

  • AppStorage
  • SceneStorage
  • FetchRequest

State

A property wrapper type that can read and write a value managed by SwiftUI.

Use state as the single source of truth for a given value type that you store in a view hierarchy.

struct MusicPlayer: View { 
@State private var isPlaying: Bool = false
var body: some View {

}
}

When should we use @State?

  • Storing a value type like a structure, string or integer
  • Store a reference type that conforms to the Observable() protocol
  • Declare state as private to prevent setting it in memberwise initializer — Conflict with storage management.

Bindable

A property wrapper type that supports creating bindings to the mutable properties of observable objects.

Introduced in iOS 17.0

@dynamicMemberLookup @propertyWrapper
struct Bindable<Value>

When to use @Bindable?

  • Wrapping a class with @Observable protocol
  • Provide another view a binding to a property on your ViewModel object
Single Source of Truth

Binding

A property wrapper type that can read and write a value owned by a source of truth.

Create a two-way connection between a property that stores data and a view that displays and changes data.

We can share access to the state with bindings.

struct PlayMusicButton: View { 
@Binding var isPlaying: Bool = false
var body: some View {
Button(action: {
self.isPlaying.toggle()
}) {
Image(systemName: isPlaying ? "pause.circle" : "play.circle")
}
}
}

Please find the Migrating from the Observable Object protocol to the Observable macro. Observable macro has been introduced in iOS 17. Using the Observable macro eliminates the need for the ObservedObject and Published property wrappers for observable properties.

Binding vs Bindable:

To create bindings to properties of a type that conforms to the Observable protocol, use the Bindable property wrapper.

If your property doesn’t require observation, use the ObservationIgnored() macro.

Bindable works only with class whereas Binding works well with the value types.

@ObservationIgnored var donotTrack = false

StateObject

A property wrapper type that instantiates an observable object.

@frozen @propertyWrapper
struct StateObject<ObjectType> where ObjectType : ObservableObject

Single source of truth for a reference type that is stored in the view hierarchy. We can declare and provide an initial value that conforms to the ObserevableObject protocol.

When to use @StateObject?

  • Respond to changes or updates in ObserevableObject
  • Declare StateObject as private to prevent setting it in memberwise initializer — Conflict with storage management.
  • Share state objects with subviews
  • Initialize state objects with external data
  • Force Reinitialization by changing view identity — using id(_:) modifier

ObservedObject

A property wrapper type that subscribes to an observable object and invalidates a view whenever the observable object changes.

@propertyWrapper @frozen
struct ObservedObject<ObjectType> where ObjectType : ObservableObject

When to use @ObservedObject?

  • The view to update when the object’s published properties change.
  • Don’t wrap objects conforming to Observable protocol with ObservedObject. To wrap use the @Bindable property wrapper.

EnvironmentObject

A property wrapper type for an observable object that a parent or ancestor view supplies.

@frozen @propertyWrapper
struct EnvironmentObject<ObjectType> where ObjectType : ObservableObject

An environment object invalidates the current view whenever the observable object conforms to the ObservableObject changes.

environmentObject(_:) modifier

If the observable object conforms to the Observable protocol, use Environment instead of EnvironmentObject.

Environment

A property wrapper that reads a value from a view’s environment.

Environment
@frozen @propertyWrapper
struct Environment<Value>

Example Usage:

@Environment(\.colorScheme) var colorScheme: ColorScheme
struct NestedViews: View {
@Environment(Settings.self) private var settings
var body: some View {
ZStack {
Color.accentColor.ignoresSafeArea(.all)

}
}
}

In the WWDC23, Apple displayed the following flow chart — Discover Observation in SwiftUI

AppStorage

A property wrapper type that reflects a value from UserDefaults and invalidates a view on a change in value in that user default.

@frozen @propertyWrapper
struct AppStorage<Value>

Used to access UserDefaults from multiple views in a view hierarchy

struct AppStorageView: View {
@AppStorage("preferDark") var preferMode: Bool = false
var body: some View {
ZStack {
Color(preferMode ? .black : .white).ignoresSafeArea(edges: .all)
VStack {

Text("@AppStorage with @Binding")
.font(.title)
Text("Used to access UserDefaults from multiple views in a view hierachy")
.font(.subheadline)
ShapeView(preferMode: $preferMode)
SetView(preferMode: $preferMode)
}
}
.animation(.easeIn, value: preferMode)
}
}

SceneStorage

A property wrapper type that reads and writes to persisted, per-scene storage.

@frozen @propertyWrapper
struct SceneStorage<Value>

Use SceneStorage when you need automatic state restoration of the value. Similar to State.

Used to save data persistently for each scene.

struct SceneStorageView: View {
@SceneStorage("currentTime") var currentTime: Double?
var body: some View {
VStack {
Text("Button was clicked on \(dateString)")
Button("Click Here") {
currentTime = Date().timeIntervalSince1970
}
Spacer()
}
}
var dateString: String {
if let currentTimeStamp = currentTime {
return Date(timeIntervalSince1970: currentTimeStamp).formatted()
} else {
return "Never"
}
}
}

FetchRequest

A property wrapper type that retrieves entities from a Core Data persistent store.

@MainActor @propertyWrapper
struct FetchRequest<Result> where Result : NSFetchRequestResult

Use FetchRequest whenever want to fetch data by interacting with the CoreData store directly to the view.

Property Observers

Observe and respond to changes in a property’s value. Called every time a property’s value is set, even if the new value is the same as the current value.

Observers on a property are as follows:

  • willSet — called just before the value is stored.
  • didSet — called immediately after the new value is stored.
class Temperature {
var celsius: Double = 0.0 {
willSet(newCelsius) {
print("about to change to \(newCelsius) degrees")
}
didSet {
if celsius > oldValue {
print("Increased from \(celsius - oldValue) degrees")
} else {
print("Reduced from \(oldValue - celsius) degrees")
}
}
}
}
let temperature = Temperature()
temperature.celsius = 36.1
//About to change to 36.1 degrees
//Increased from 36.1 degrees
temperature.celsius = 38
//About to change to 38.0 degrees
//Increased from 1.8999999999999986 degrees
temperature.celsius = 37.2
//About to change to 37.2 degrees
//Reduced from 0.7999999999999972 degrees

References

SwiftUI Property Wrappers — https://swiftuipropertywrappers.com/

SwiftUI Data Flow — https://troz.net/post/2023/swiftui-data-flow-2023/

Grateful for your read, now let’s code on!

Interested in connecting? 
Feel free to connect with me on: LinkedIn and GitHub

Please take a look at my first-ever Article 👇

--

--

Kanagasabapathy Rajkumar

Swift Enthusiast | Building Seamless iOS Experiences 🚀 | Swift & Objective-C |