Skip to Content
Course content

41: Property Wrappers Explained

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

When I first encountered property wrappers in Swift, I fell into a trap that I see a lot of developers fall into today: I thought they were just a "shorthand" way of writing getters and setters. I figured, "Oh, it's just a fancy way to avoid writing set { ... } every time I want to validate a variable."

Property Wrappers are just glorified getters and setters

If you think of them as just a way to wrap a set block, you're missing the actual magic. Let's look at the "manual" way of handling a common task: ensuring a string is always trimmed of whitespace. You'd probably do something like this:

class UserProfile {
    private var _username: String = ""
    var username: String {
        get { _username }
        set { _username = newValue.trimmingCharacters(in: .whitespaces) }
    }
}

This works, but it's tedious. If you have ten different string properties that all need trimming, you're writing that same trimmingCharacters logic ten times. You might try to move it to a helper function, but you're still stuck writing the boilerplate get/set for every single property. The misconception is thinking the property is what's doing the work, when in reality, we want the logic to be a reusable component that can be dropped onto any property.

Decoupling Storage Logic from the Property

A property wrapper doesn't just "shortcut" the setter; it completely abstracts the storage logic into its own type. When you use the @ symbol, you're telling the Swift compiler: "Don't just store this value; use this specific object to manage how the value is stored and retrieved."

Here is how we actually solve the trimming problem using a property wrapper:

@propertyWrapper
struct Trimmed {
    private var value: String = ""

    var wrappedValue: String {
        get { value }
        set { value = newValue.trimmingCharacters(in: .whitespaces) }
    }

    init(wrappedValue: String) {
        self.wrappedValue = wrappedValue
    }
}

struct UserProfile {
    @Trimmed var username: String
    @Trimmed var bio: String
}

var profile = UserProfile(username: "  SwiftDev  ", bio: "  I love coding!  ")
print(profile.username) // "SwiftDev"
print(profile.bio)      // "I love coding!"

Notice what happened here. I didn't write a single get or set inside the UserProfile struct. The Trimmed struct handles the logic, and the UserProfile simply declares that its properties should behave that way. I've separated the intent (this string should be trimmed) from the implementation (how the trimming actually happens).

The Hidden Power of the Projected Value

Now, here is the part that usually trips people up: the projected value. You've probably seen the $ sign used with @State or @Published in SwiftUI. That isn't some magic SwiftUI syntax; it's a core feature of Swift property wrappers.

If you add a property named projectedValue to your wrapper, you can expose additional functionality that is separate from the wrappedValue itself. I find this incredibly useful for metadata or state tracking. Let's say we want to know if our trimmed string was actually modified during the trimming process:

@propertyWrapper
struct Trimmed {
    private var value: String = ""
    private(set) var wasTrimmed: Bool = false

    var wrappedValue: String {
        get { value }
        set {
            wasTrimmed = newValue != newValue.trimmingCharacters(in: .whitespaces)
            value = newValue.trimmingCharacters(in: .whitespaces)
        }
    }

    // This is what the '$' prefix accesses
    var projectedValue: Bool {
        return wasTrimmed
    }

    init(wrappedValue: String) {
        self.wrappedValue = wrappedValue
    }
}

struct UserProfile {
    @Trimmed var username: String
}

var profile = UserProfile(username: "  SwiftDev  ")
print(profile.username)  // "SwiftDev" (The wrapped value)
print(profile.$username) // true (The projected value)

By using $username, I'm not accessing the string; I'm accessing the projectedValue of the Trimmed wrapper. This allows you to attach a "side-channel" of information to your property without polluting the main value type.




📋 Practical Task

Build a Clamped Value Wrapper for a Game Character's Health

In game development, you often need to ensure a value stays within a specific range (e.g., health cannot go below 0 or above 100). Instead of writing if health > 100 { health = 100 } everywhere in your combat logic, you should use a property wrapper.

Your Task:

  • Create a property wrapper called @Clamped.
  • The wrapper should take two generic parameters in its initializer: minValue and maxValue.
  • The wrappedValue should ensure that any value set is clamped between these two bounds. (Hint: Use max(minValue, min(maxValue, newValue))).
  • Implement a projectedValue that returns a Bool indicating whether the last value set was actually clamped (i.e., the input was outside the bounds).
  • Create a Character struct with a @Clamped property for health (min: 0, max: 100).
  • Test it by setting the health to 150 and verify that the value is 100 and the projected value is true.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.