Property wrappers add a layer of separation between code that manages how a property is stored and the code that defines a property. A property wrapper is a type that wraps a property, adding additional behavior or validation. They were introduced in Swift 5.1.
Key Components
- @propertyWrapper Attribute: Marks a type as a property wrapper
- wrappedValue: The underlying value being wrapped
- projectedValue (optional): Additional functionality exposed via $ prefix
Common Use Cases
Property wrappers are commonly used for:
- Value validation
- Thread safety
- Type conversion
- Persistence
- Notification on changes
- Lazy initialization
- Access control
Examples in the Code
Let’s look at the property wrappers defined below:
1. @Debounced
Delays setting a value until a time interval has passed without new values being set.
@propertyWrapper
class Debounced<Value> {
var backingValue: Value
private var timer: Timer?
var wrappedValue: Value {
get { backingValue }
set { setValue(newValue) }
}
private func setValue(_ newValue: Value) {
timer?.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [newValue, weak self] _ in
self?.backingValue = newValue
}
}
init(wrappedValue: Value, timer: Timer? = nil) {
self.backingValue = wrappedValue
self.timer = timer
}
}
2. @DateFormatted
Automatically formats a Date value as a string using a specified format.
@propertyWrapper
struct DateFormatted {
var date: Date
var dateFormatter: DateFormatter
var wrappedValue: String {
return dateFormatter.string(from: date)
}
init(wrappedValue: Date, format: String) {
self.date = wrappedValue
self.dateFormatter = DateFormatter()
self.dateFormatter.dateFormat = format
}
}
3. @UpperCased
Ensures a string is always stored in uppercase.
@propertyWrapper
struct UpperCased {
var backingString: String
var wrappedValue: String {
get { backingString }
set { backingString = newValue.uppercased() }
}
init(wrappedValue: String) {
self.backingString = wrappedValue.uppercased()
}
}
4. @NonNegative
Ensures an integer value is never negative.
@propertyWrapper
struct NonNegative {
var backingValue: Int
var wrappedValue: Int {
get { backingValue }
set {
if newValue > -1 {
backingValue = newValue
} else {
backingValue = 0
}
}
}
init(wrappedValue: Int) {
self.backingValue = max(wrappedValue, 0)
}
}
Usage Example
struct Item {
@NonNegative var itemStock: Int = 10
@Trimmed var name: String = " Krishna Y "
@Clamped(range: 0...1) var goodRange: Double = -1.5
@Logged var changingWeather: Int = 66
@UpperCased var shoutingText: String = "Lets go!"
@DateFormatted(format: "dd/MM/yyyy") var dateBought = Date()
}
Benefits
- Reusability: Write the property wrapper once, use it many times
- Encapsulation: Property access logic is contained in one place
- Clean Code: Reduces boilerplate in property declarations
- Type Safety: Enforces constraints at compile time
- Maintainability: Easier to modify behavior in one place
Best Practices
- Keep property wrappers focused on a single responsibility
- Document the behavior clearly
- Consider performance implications
- Use meaningful names that describe the behavior
- Handle edge cases appropriately
- Consider thread safety when needed
@propertyWrapper
class Debounced<Value> {
var backingValue: Value
private var timer: Timer?
var wrappedValue: Value {
get {
backingValue
}
set {
setValue(newValue)
}
}
private func setValue(_ newValue: Value) {
timer?.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [newValue, weak self] _ in
self?.backingValue = newValue
}
}
init(wrappedValue: Value, timer: Timer? = nil) {
self.backingValue = wrappedValue
self.timer = timer
}
}
@propertyWrapper
struct DateFormatted {
var date: Date
var dateFormatter: DateFormatter
var wrappedValue: String {
return dateFormatter.string(from: date)
}
init(wrappedValue: Date, format: String) {
self.date = wrappedValue
self.dateFormatter = DateFormatter()
self.dateFormatter.dateFormat = format
}
}
@propertyWrapper
struct UpperCased {
var backingString: String
var wrappedValue: String {
get {
backingString
}
set {
backingString = newValue.uppercased()
}
}
init(wrappedValue: String) {
self.backingString = wrappedValue.uppercased()
}
}
struct Item {
@NonNegative var itemStock: Int = 10
@Trimmed var name: String = " Krishna Y "
@Clamped(
wrappedValue: -1.5,
range: 0...1
) var goodRange: Double
@Logged var changingWeather: Int = 66
@UpperCased var shoutingText: String = "Lets go!"
@DateFormatted(format: "dd/MM/yyyy") var dateBought = Date()
var searchString: String = "hello"
}
@propertyWrapper
struct Logged<Value> {
var backingValue: Value
var wrappedValue: Value {
get {
backingValue
}
set {
print("Changed from \(backingValue) to \(newValue)")
backingValue = newValue
}
}
init(wrappedValue: Value) {
print("init with \(wrappedValue)")
self.backingValue = wrappedValue
}
}
@propertyWrapper
struct Clamped {
var backingDouble: Double
var range: ClosedRange<Double>
var wrappedValue: Double {
get {
backingDouble
}
set {
backingDouble = min(max(newValue,range.lowerBound), range.upperBound)
}
}
init(wrappedValue: Double, range: ClosedRange<Double>) {
self.range = range
self.backingDouble = min(max(wrappedValue,range.lowerBound), range.upperBound)
}
}
@propertyWrapper
struct Trimmed {
var backingString: String
var wrappedValue: String {
get {
backingString
}
set {
backingString = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
}
}
init(wrappedValue: String) {
self.backingString = wrappedValue.trimmingCharacters(in: .whitespacesAndNewlines)
}
}
@propertyWrapper
struct NonNegative {
var backingValue: Int
var wrappedValue: Int {
get {
backingValue
}
set {
if newValue > -1 {
backingValue = newValue
} else {
backingValue = 0
}
}
}
init(wrappedValue: Int) {
self.backingValue = max(wrappedValue, 0)
}
}
class VM: ObservableObject {
@Published var result: Int
var stockCount: Int
init(result: Int) {
self.result = result
self.stockCount = result
}
func doSomething() {
stockCount -= 1
result = stockCount
var item = Item(itemStock: stockCount)
print(item.itemStock)
print(item.name)
print(item.goodRange)
item.changingWeather = Int.random(in: 60...75)
print(item.changingWeather)
print(item.shoutingText)
print(item.dateBought)
item.searchString = "TEST"
}
}
struct ContentView: View {
@State var vm: VM = VM(result: 10)
@State private var timer: Timer?
var body: some View {
Text("Hello, World!")
Text("\(vm.result)")
.onAppear {
startTimer()
}
}
func startTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: true) { timer in
vm.doSomething()
}
}
private func stopTimer() {
timer?.invalidate()
timer = nil
}
}