@propertyWrapper is interesting because it demystifies an advanced Swift feature that helps encapsulate logic, reduce boilerplate, and improve code maintainability. Many developers may not fully utilize property wrappers, despite their practical applications in areas like UserDefaults management, data validation, and SwiftUI state handling (@State, @Published, etc.). By providing clear explanations, real-world examples, and best practices we will present a pair of examples where it could be interesting approach implementation by using @properyWrapper.

Custom @propertyWrapper

A custom property wrapper in Swift is a specialized type that allows you to add custom behavior or logic to properties without cluttering the main class or struct code. It’s a powerful feature introduced in Swift 5 that enables developers to encapsulate common property-related functionality, such as validation, transformation, or persistence, in a reusable manner.

Custom property wrappers are particularly useful for:

  1. Encapsulating repetitive code patterns.

  2. Adding validation or transformation logic to properties1.

  3. Implementing persistence mechanisms, such as UserDefaults storage.

  4. Creating SwiftUI-compatible state management solutions.

Clamped type example

First example is a clamped type, that means an int ranged value:

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

    init(wrappedValue: Value, _ range: ClosedRange<Value>) {
        self.range = range
        self.value = range.contains(wrappedValue) ? wrappedValue : range.lowerBound
    }

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

// Uso del Property Wrapper
struct Player {
    @Clamped(wrappedValue: 50, 0...100) var health: Int
}

var player = Player()
player.health = 120
print(player.health) // Output: 100 (se ajusta al máximo del rango)

The Clamped property wrapper ensures that a property’s value remains within a specified range. It takes a ClosedRange<Value> as a parameter and clamps the assigned value to stay within the defined bounds. When a new value is set, it uses min(max(newValue, range.lowerBound), range.upperBound) to ensure the value does not go below the lower bound or exceed the upper bound. If the initial value is outside the range, it is automatically set to the lower bound. This makes Clamped useful for maintaining constraints on variables that should not exceed predefined limits.

In the Player struct, the health property is wrapped with @Clamped(wrappedValue: 50, 0...100), meaning its value will always stay between 0 and 100. If we set player.health = 120, it gets clamped to 100 because 120 exceeds the maximum allowed value. When printed, player.health outputs 100, demonstrating that the wrapper effectively enforces the constraints. This approach is particularly useful in scenarios like game development (e.g., keeping player health within a valid range) or UI elements (e.g., ensuring opacity remains between 0.0 and 1.0).

UserDefaults example

Second example is a UserDefaults sample:

@propertyWrapper
struct UserDefault<T> {
    let key: String
    let defaultValue: T

    var wrappedValue: T {
        get {
            return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
        }
        set {
            UserDefaults.standard.set(newValue, forKey: key)
        }
    }
}

@MainActor
struct Settings {
    @UserDefault(key: "username", defaultValue: "Guest")
    static var username: String
}

Settings.username = "SwiftUser"
print(Settings.username) // Output: "SwiftUser"

This code defines a property wrapper, UserDefault<T>, which allows easy interaction with UserDefaults in Swift. The wrapper takes a generic type T, a key for storage, and a defaultValue to return if no value is found in UserDefaults. The wrappedValue property is used to get and set values in UserDefaults: when getting, it retrieves the stored value (if available) or falls back to the default; when setting, it updates UserDefaults with the new value.

The Settings struct defines a static property username using the @UserDefault wrapper. This means Settings.username reads from and writes to UserDefaults under the key "username". When Settings.username = "SwiftUser" is set, the value is stored in UserDefaults. The subsequent print(Settings.username) retrieves and prints "SwiftUser" since it was saved, demonstrating persistent storage across app launches.

Conclusions

By using custom property wrappers, you can significantly reduce boilerplate code and improve the modularity and reusability of your Swift projects

You can find source code used for writing this post in following repository

References

Copyright © 2024-2025 JaviOS. All rights reserved