@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:
Encapsulating repetitive code patterns.
Adding validation or transformation logic to properties1.
Implementing persistence mechanisms, such as UserDefaults storage.
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
- What is Property Wrapper?
Ask WWDC