Understanding the projectedValue in Swift Property Wrappers
If you’re new to property wrappers in Swift, I recommend reading my previous blog on the basics of property wrappers to build a solid foundation before diving into the concept of projectedValue.
Property wrappers are a powerful feature in Swift that allow you to encapsulate common logic for managing properties. While the wrappedValue is the core aspect we interact with, Swift introduces an additional feature called projectedValue, which enhances the functionality of property wrappers.
?? What is projectedValue?
In a property wrapper, projectedValue is an optional computed property that provides additional capabilities related to the wrapped value. It can be accessed using the $ symbol followed by the property name $propertyName.
While wrappedValue provides the actual value of the property, projectedValue offers extended functionality. It can be used to expose useful metadata, enable bindings, or provide other advanced features.
?? Why Does projectedValue Exist in Swift Property Wrappers?
projectedValue is meant to expose functionality beyond just accessing the wrapped value. By using the $propertyName syntax, you can access the projection, which allows you to bind, transform, or track additional information about the property.
This feature is particularly useful when you need to work with more complex behaviour, such as two-way data binding, logging, or metadata about the property.
?? Why is projectedValue Required to Use $?
In Swift, the compiler looks for a computed property named projectedValue inside the property wrapper to provide access to the projection. Without defining projectedValue, using the $ symbol for the property will result in a compiler error.
?? Example: Without projectedValue
Let’s start by defining a simple property wrapper:
import Foundation
@propertyWrapper
struct SanitisedString {
private var value: String = ""
var wrappedValue: String {
get { value }
set { value = newValue.filter { $0.isLetter || $0.isNumber || $0.isWhitespace } }
}
init(wrappedValue: String) {
self.wrappedValue = wrappedValue
}
}
struct User {
@SanitizedString var name: String
}
var user = User(name: "T@ylor Sw!ft")
print(user.name) // Output: "Tylor Swft"
// This will throw an error because there is no projected value available:
// print(user.$name) // ? Error: No projected value available
In this case, we’ve created a property wrapper SanitisedString to sanitise the input string. However, since we haven’t defined projectedValue, trying to access user.$name will result in an error.
?? Example: With projectedValue
Now, let’s enhance the SanitisedString wrapper by adding a projectedValue property:
import Foundation
@propertyWrapper
struct SanitisedString {
private var value: String = ""
var wrappedValue: String {
get { value }
set { value = newValue.filter { $0.isLetter || $0.isNumber || $0.isWhitespace } }
}
init(wrappedValue: String) {
self.wrappedValue = wrappedValue
}
// This enables the use of $propertyName
var projectedValue: String {
value
}
}
struct User {
@SanitizedString var name: String
}
var user = User(name: "J@hn! D#oe")
print(user.name) // Output: "Jhn Doe"
print(user.$name) // Output: "Jhn Doe"
Here, we’ve added a projectedValue computed property to the SanitisedString property wrapper. Now, by using $user.name, we can access the sanitised version of the value as a projection.
? Why Use projectedValue?
You might want to use projectedValue in the following scenarios:
? When It’s Worth Using
Example:
import SwiftUI
struct ContentView: View {
@State private var name: String = "John"
var body: some View {
TextField("Enter name", text: $name) // Uses projectedValue (Binding)
}
}
In this example:
?? Why use it? It allows the TextField to update and reflect the name value dynamically.
2. When You Want to Provide Metadata
Example:
import Foundation
@propertyWrapper
struct Logged<T> {
private var value: T
private(set) var logs: [String] = []
init(wrappedValue: T) {
self.value = wrappedValue
self.logs.append("Initial value: \(wrappedValue)")
}
var wrappedValue: T {
get { value }
set {
logs.append("Value changed from \(value) to \(newValue)")
value = newValue
}
}
// Exposing the logs as a projected value
var projectedValue: [String] {
return logs
}
}
// Example usage
struct Example {
@Logged var name: String
}
var example = Example(name: "Taylor")
example.name = "Swift"
example.name = "Cook"
print(example.name) // Output: "Cook
print(example.$name) // Output: ["Initial value: Taylor", "Value changed from Taylor to Swift", "Value changed from Swift to Cook"]
?? Why use it? You can track, log, and inspect property changes.
?? Rule of Thumb
?? When You Don’t Need projectedValue
While projectedValue is powerful, you should avoid using it in the following cases:
? Key Points:
?? Conclusion
projectedValue is an optional but powerful feature of Swift property wrappers that can significantly enhance your code. It allows you to extend the functionality of properties by enabling advanced capabilities like two-way data binding, metadata tracking, and other complex behaviours.
By understanding how to implement and use projectedValue, you can write cleaner, more maintainable, and feature-rich code, especially when working with frameworks like SwiftUI.
?? Have you used property wrappers in your Swift projects? Share your thoughts or ask questions below!
Stay updated by joining my network on ???Medium.
#Swift #iOSDevelopment #PropertyWrappers #CleanCode #SwiftUI #CodeTips #Xcode #apple