← All topics/Swift language features

Practical interview questions

Scenario-style prompts with sample answer outlines. Focus is on how you would design and reason in real codebases.

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 solve the problem of repeating the same property-related behavior over and over. They let that behavior be defined once and reused declaratively on many properties. Common examples are clamping values or exposing binding/state behavior in SwiftUI.

SwiftUI uses @State, @Binding, and the Observation stack.

Combine uses @Published on ObservableObject properties.

ObservableObject is a class-only protocol from Combine: you conform a class, mark changing properties with @Published, and views hold it with @StateObject / @ObservedObject. Changes fan out through objectWillChange (and per-@Published emissions).

The @Observable macro applies to a class and has the compiler synthesize observation for stored properties. That is a different mechanism from Combine’s ObservableObject: @Published is what marks properties for change publishing on ObservableObject; on an @Observable type you typically use plain stored properties and let the macro track them (don’t layer @Published on top for the same fields).

Use @Bindable when you need bindings from an @Observable instance. @Observable is the modern default for new SwiftUI models when the OS allows it; it and ObservableObject both solve notifying views when the model changes, with less boilerplate in the macro-based approach.

projectedValue ($name) exposes wrapper-specific APIs—for example a Binding in SwiftUI.

Principles

  • The wrapper must respect the access level and mutation rules of the wrapped value.
Minimal custom wrapper
@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) }
    }
}

Apply the wrapper to a property; assignments are automatically clamped to the configured range.

Using the wrapper
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 for SwiftUI, no @Published on each field. Hold the model with @State (or pass @Bindable where you need bindings).

`@Observable` model (Observation)
import Observation

@Observable
final class ProfileModel {
    var name = ""
    var isLoading = false
}

// SwiftUI: @State private var model = ProfileModel()