Question 7
Property wrappers: @State, @Published, custom wrappers
What problem do property wrappers solve? How would you implement a simple custom property wrapper, and what is the projected value ($)?
Answer outline
Property wrappers let you write a property behavior once in a wrapper type and apply it with an attribute wherever you need it. Typical uses are clamping a value to a range and exposing SwiftUI state and bindings.
SwiftUI uses @State for view-local state and @Binding to hand that state to a child view. Combine uses @Published on an ObservableObject: you conform a class to it, mark the changing properties @Published, and hold the object in a view with @StateObject or @ObservedObject. Changes fan out through objectWillChange.
The @Observable macro is the modern default when your deployment target allows it. It applies to a class and has the compiler synthesize observation for its stored properties. Plain properties are tracked, so you don't add @Published.
Every wrapper exposes a wrappedValue, which is what the property reads and writes. A wrapper may also define a projectedValue, reached with the $ prefix, which exposes wrapper-specific API such as a Binding in SwiftUI. An @Observable instance gets those same $ bindings through @Bindable.
Principles
wrappedValueis the value the property exposes, andprojectedValue($name) is the wrapper's extra API.@Observabletracks stored properties on its own, so don't add@Publishedto them.@Bindablegives you bindings from an@Observableinstance, the way$does for@State.- Keep a wrapper's rules in one place, so every wrapped property gets the same validation for free.
A wrapper is a type marked @propertyWrapper with a wrappedValue property:
@propertyWrapper
struct Clamped {
private var value: Int
let range: ClosedRange<Int>
init(wrappedValue: Int, _ range: ClosedRange<Int>) {
self.range = range
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
}
var wrappedValue: Int {
get { value }
set { value = min(max(newValue, range.lowerBound), range.upperBound) }
}
}
Once the wrapper is applied to a property, every assignment is clamped to the configured range:
struct Settings {
@Clamped(0...100) var volume = 50
}
var settings = Settings()
settings.volume = 120
print(settings.volume) // 100
settings.volume = -10
print(settings.volume) // 0
Plain stored properties are tracked with no @Published on each field, and the view holds the model with @State:
import Observation
@Observable
final class ProfileModel {
var name = ""
var isLoading = false
}
// SwiftUI: @State private var model = ProfileModel()



