Home Articles

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

Demonstrating the use cases of Property Wrappers in SwiftUI. Let us take a deep dive into property wrappers in SwiftUI in this article:

Demo of Properties

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

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.

Property Wrapper

Persistent Storage

  1. AppStorage
  2. SceneStorage
  3. FetchRequest

Property Wrappers

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?

  1. Storing a value type like a structure, string or integer
  2. Store a reference type that conforms to the Observable() protocol
  3. 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?

  1. Wrapping a class with @Observable protocol
  2. 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.

For more details check this link


    @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?

  1. Respond to changes or updates in ObserevableObject
  2. Declare StateObject as private to prevent setting it in memberwise initializer — Conflict with storage management.
  3. Share state objects with subviews
  4. Initialize state objects with external data
  5. 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?

  1. The view to update when the object’s published properties change.
  2. 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

WWDC23 — Observation Framework

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:

  1. willSet — called just before the value is stored.
  2. 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
    

The entire project can be found here

References:

SwiftUI Property Wrappers

SwiftUI Data Flow

This is a free third party commenting service we are using for you, which needs you to sign in to post a comment, but the good bit is you can stay anonymous while commenting.