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

  1. @propertyWrapper Attribute: Marks a type as a property wrapper
  2. wrappedValue: The underlying value being wrapped
  3. 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

  1. Reusability: Write the property wrapper once, use it many times
  2. Encapsulation: Property access logic is contained in one place
  3. Clean Code: Reduces boilerplate in property declarations
  4. Type Safety: Enforces constraints at compile time
  5. Maintainability: Easier to modify behavior in one place

Best Practices

  1. Keep property wrappers focused on a single responsibility
  2. Document the behavior clearly
  3. Consider performance implications
  4. Use meaningful names that describe the behavior
  5. Handle edge cases appropriately
  6. 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
	}
}