DEV Community

ArshTechPro
ArshTechPro

Posted on

Custom Property Wrappers in SwiftUI

If you've used SwiftUI, you've definitely encountered property wrappers like @State, @Binding, and @ObservedObject. But have you ever wondered how they work, or how to create your own? In this article, we'll dive deep into property wrappers and learn how to make SwiftUI views react to our custom implementations.

What is a Property Wrapper?

A property wrapper is a Swift language feature that adds a layer of separation between code that manages how a property is stored and the code that defines a property. Introduced in Swift 5.1, property wrappers allow you to write reusable property management logic once and apply it to multiple properties.

Basic Property Wrapper Example

Here's a simple property wrapper that ensures a value stays within bounds:

@propertyWrapper
struct Clamped<Value: Comparable> {
    private var value: Value
    private let range: ClosedRange<Value>

    var wrappedValue: Value {
        get { value }
        set { value = min(max(range.lowerBound, newValue), range.upperBound) }
    }

    init(wrappedValue: Value, _ range: ClosedRange<Value>) {
        self.range = range
        self.value = min(max(range.lowerBound, wrappedValue), range.upperBound)
    }
}

// Usage
struct Settings {
    @Clamped(0...100) var volume: Int = 50
}
Enter fullscreen mode Exit fullscreen mode

Key Components of a Property Wrapper

  1. The @propertyWrapper attribute - Marks a type as a property wrapper
  2. wrappedValue property - The actual value being wrapped
  3. init(wrappedValue:) - Initializer that accepts the initial value
  4. projectedValue (optional) - Provides additional functionality, accessed with $ prefix

What is DynamicProperty?

DynamicProperty is a protocol in SwiftUI that allows property wrappers to participate in the view update cycle. This is the secret sauce that makes property wrappers like @State and @ObservedObject trigger view updates.

How DynamicProperty Works

When a property wrapper conforms to DynamicProperty, SwiftUI does the following:

  1. Tracks the property wrapper as part of the view's state
  2. Calls the update() method before each view render
  3. Automatically invalidates and re-renders the view when wrapped values change

Here's what the protocol looks like:

protocol DynamicProperty {
    mutating func update()
}
Enter fullscreen mode Exit fullscreen mode

The update() method is called by SwiftUI before rendering the view body. You can implement custom logic here, but often you don't need to provide an implementation as the default behavior is sufficient.

Creating a Custom Property Wrapper for SwiftUI

Now let's combine both concepts to create a property wrapper that makes SwiftUI views react to changes.

Step 1: Basic Structure

@propertyWrapper
struct MyState<Value>: DynamicProperty {
    @State private var value: Value

    var wrappedValue: Value {
        get { value }
        nonmutating set { value = newValue }
    }

    init(wrappedValue: Value) {
        _value = State(wrappedValue: wrappedValue)
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Understanding the Components

DynamicProperty conformance
By conforming to DynamicProperty, we tell SwiftUI to track this wrapper as part of the view's state system.

Internal @State property
We use @State internally because it already knows how to trigger view updates. This is composition at work.

nonmutating setter
The nonmutating keyword is crucial. SwiftUI views are structs, which are value types. Normally, you can't mutate properties of a struct without marking the method as mutating. However, @State and similar wrappers use nonmutating set to allow mutations without requiring the entire view to be marked as mutating.

Step 3: Using the Custom Property Wrapper

struct ContentView: View {
    @MyState var count = 0

    var body: some View {
        VStack(spacing: 20) {
            Text("Count: \(count)")
                .font(.largeTitle)

            Button("Increment") {
                count += 1
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

When you tap the button, the count changes, and SwiftUI automatically re-renders the view. This happens because:

  1. MyState conforms to DynamicProperty
  2. The internal @State property publishes changes
  3. SwiftUI detects the change and invalidates the view
  4. The view body is called again with the updated value

Advanced Example: Custom Property Wrapper with Binding

Let's create a more sophisticated wrapper that also provides a Binding:

@propertyWrapper
struct Validated<Value>: DynamicProperty {
    @State private var value: Value
    private let validator: (Value) -> Bool

    var wrappedValue: Value {
        get { value }
        nonmutating set {
            if validator(newValue) {
                value = newValue
            }
        }
    }

    var projectedValue: Binding<Value> {
        Binding(
            get: { value },
            set: { newValue in
                if validator(newValue) {
                    value = newValue
                }
            }
        )
    }

    init(wrappedValue: Value, validator: @escaping (Value) -> Bool) {
        self.validator = validator
        _value = State(wrappedValue: wrappedValue)
    }
}

// Usage
struct FormView: View {
    @Validated(validator: { $0.count >= 3 }) var username = ""

    var body: some View {
        VStack {
            TextField("Username (min 3 chars)", text: $username)
            Text("Username: \(username)")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Alternative Approaches

You can also use @StateObject with ObservableObject for more complex state management:

@propertyWrapper
struct Reactive<Value>: DynamicProperty {
    @StateObject private var storage: Storage<Value>

    var wrappedValue: Value {
        get { storage.value }
        nonmutating set { storage.value = newValue }
    }

    init(wrappedValue: Value) {
        _storage = StateObject(wrappedValue: Storage(value: wrappedValue))
    }

    private class Storage<T>: ObservableObject {
        @Published var value: T

        init(value: T) {
            self.value = value
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This approach is useful when you need more control over observation and publishing changes.

Key Takeaways

  1. Property wrappers encapsulate reusable property logic and are marked with @propertyWrapper

  2. DynamicProperty is the bridge between your property wrapper and SwiftUI's update system

  3. Composition over reimplementation - Use existing wrappers like @State internally rather than rebuilding from scratch

  4. nonmutating setters allow property modification without requiring mutable view contexts

  5. Projected values (accessed with $) provide additional functionality like bindings

Conclusion

While SwiftUI's built-in property wrappers cover most use cases, knowing how to create your own gives you the flexibility to build exactly what your app needs.

Top comments (1)

Collapse
 
arshtechpro profile image
ArshTechPro

Dynamic property
Tracks the property wrapper as part of the view's state
Calls the update() method before each view render
Automatically invalidates and re-renders the view when wrapped values change