Skip to Content
Course content

140: Property Wrappers with Projected Values

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I remember reviewing a pull request from a developer who was building a complex registration form. They had created a property wrapper called @Validated to handle email and password checks. It worked great for cleaning up the data, but the developer had a problem: the UI needed to know if the field was currently invalid to show a red border, but the wrappedValue was just the string itself. To solve this, they were manually creating a second boolean property for every single form field—isEmailValid, isPasswordValid, isZipCodeValid—just to track the state. It was a mountain of boilerplate that completely defeated the purpose of using a property wrapper in the first place.

This is exactly where projected values come in. If the wrappedValue is the "meat" of your property, the projectedValue is the metadata or the "side-channel" that lets you expose extra information about that property without cluttering your object's main interface.

Beyond the Wrapped Value

You already know that when you apply a property wrapper, Swift generates a hidden storage property. But it also generates something else: a projected value, which you access by prefixing the property name with a dollar sign ($). By default, the projected value is simply the property wrapper instance itself. While that's occasionally useful, the real power comes when you explicitly define a projectedValue property inside your wrapper struct.

Think of it as a way to provide a different "view" of the same data. While the wrappedValue handles the primary data type, the projectedValue can return a completely different type—like a boolean indicating validity, a publisher for observing changes, or even a closure.

Exposing Metadata via the Projected Value

Let's look at how we could have fixed that registration form. Instead of creating separate booleans in the ViewModel, we can bake the validation state directly into the wrapper's projection.

@propertyWrapper
struct Validated {
    private var value: String = ""
    private let validation: (String) -> Bool

    var wrappedValue: String {
        get { value }
        set { value = newValue }
    }

    // This is the magic part. 
    // Accessing $property will now return this Boolean.
    var projectedValue: Bool {
        validation(value)
    }

    init(wrappedValue: String, validation: @escaping (String) -> Bool) {
        self.value = wrappedValue
        self.validation = validation
    }
}

Now, look at how much cleaner the implementation becomes in the actual class. I personally love how this separates the data from the state of the data:

struct RegistrationForm {
    @Validated(validation: { $0.contains("@") }) 
    var email: String = ""
}

var form = RegistrationForm()
form.email = "invalid-email"

print(form.email)  // "invalid-email" (wrappedValue)
print(form.$email) // false (projectedValue)

form.email = "dev@example.com"
print(form.$email) // true

When you call form.email, you're talking to the wrappedValue. When you call form.$email, you're talking to the projectedValue. It's a clean, elegant way to attach helper logic to a property without adding noise to your model's public API.




📋 Practical Task

Building a Change-Tracking History Wrapper

Your goal is to create a property wrapper called @Tracked. This wrapper should store a value of any type, but its projected value should return an array containing the history of every value that has been assigned to that property (including the initial value).

Requirements:

  • The wrappedValue should be a generic type T.
  • The projectedValue should be an array of type [T].
  • Every time the wrappedValue is set, the new value must be appended to the history list.

Test your implementation with the following code:

struct UserProfile {
    @Tracked var username: String = "Guest"
}

var profile = UserProfile()
profile.username = "Alice"
profile.username = "Bob"

print(profile.username) // Should print "Bob"
print(profile.$username) // Should print ["Guest", "Alice", "Bob"]
Rating
0 0

There are no comments for now.

to be the first to leave a comment.