As a beginner to iOS and Swift programming, and programming in general, while I have a good understanding of the basics of programming syntax, choosing the best way to construct code, in terms of performance, memory usage, complexity, etc. is much more challenging!
Case in point, going through the 'Properties' section of the Swift guide(Properties — The Swift Programming Language (Swift 5.7)) I noticed that you can 'process' changing instance properties in different ways. You can either use property observers to "observe and respond to changes in a property’s value" (immediately before or after the change with willSet and didSet):
class StepCounter {
var totalSteps: Int = 0 {
willSet(newTotalSteps) {
print("About to set totalSteps to \(newTotalSteps)")
}
didSet {
if totalSteps > oldValue {
print("Added \(totalSteps - oldValue) steps")
}
}
}
}
or use property wrappers to "adds a layer of separation between code that manages how a property is stored and the code that defines a property" (during the change with wrappedValue):
@propertyWrapper
struct TwelveOrLess {
private var number: Int
init() { self.number = 0 }
var wrappedValue: Int {
get { return number }
set { number = min(newValue, 12) }
}
}
Both especially can be used to adjust changes in properties; I was able use property observers instead of wrappers in the audioChannel example at the end. How does one choose the subtle difference between processing right before or after a change or during a change, or even just using regular get and set methods?
Property wrappers and the observer blocks (willSet, didSet, etc.) serve different purposes, even if you can sometimes use one in the place of the other.
The observer blocks are for reacting to the setting of a property. Not much really going on there. There's an opportunity to tweak or replace the value that will be or was set, but it's generally used to trigger some kind of action in response to the value changing.
Property wrappers, on the other hand, are a tool for extracting boilerplate around managing a property into a reusable type that can be applied to any number of properties. You'll want to create wrappers when you find yourself implementing the same pattern in multiple places.
Property wrappers example
I'll illustrate using a case that came my way just a couple days ago. I have a small Reactive-like library that I wrote for my own use. It has a type called Observable which can be used to emit events, and an Observer type which be used to observe events. An Observable can also observe events.
I found it was common in my code to have a private Observable, and a public Observer. This is to prevent outside code using the Observable to issue events. Logic in the init method would tie the Observer to the Observable. I extracted this into the following property wrapper:
@propertyWrapper public struct PrivateObservable<Value> {
public let wrappedValue: Observer<Value>
public let projectedValue: Observable<Value>
private var observer: Observer<Value>?
public init(wrappedValue: Observer<Value>) {
self.wrappedValue = wrappedValue
projectedValue = Observable<Value>()
observer = projectedValue
.on(next: { [wrappedValue] in
wrappedValue.next($0)
})
}
}
And instead of repeating something like the following:
class C {
public let observer: Observer<Int>
private let observable = Observable<Int>()
init() {
observer.= observable.observer()
}
func f() {
observable.next(i)
}
}
I can now simply do this:
class C {
@PrivateObservable
public var observer = Observer<Int>()
func f() {
$observer.next(i)
}
}
So essentially property observers are more appropriate to specific uses to manage a property while property wrappers are more like a function or macro that can be across different classes, structs or enums to manage properties or even across multiple properties within a class, struct or enum. ie. if I need it once or twice, us an observer, if I need it multiple time across or within a class, etc. use a wrapper.