Swift
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Optionals
-
Section 4: Object-Oriented and Value Types
-
Section 5: Memory Management
-
Section 6: Generics and Error Handling
-
Section 7: Concurrency
-
Section 8: Working with Collections
-
Section 9: Codable and Data Handling
-
Section 10: Protocol-Oriented Programming
-
Section 11: Testing and Tooling
-
Section 12: Practical Projects
-
Section 13: Interview Practice
-
Section 14: More Practice Exercises
-
Section 15: More Standard Library
-
Section 16: Advanced Concurrency
-
Section 17: Foundation Framework Deep Dive
-
Section 18: URLSession and Networking Deep Dive
-
Section 19: Combine Framework
-
Section 20: SwiftUI Fundamentals for Swift Developers
-
Section 21: Server-Side Swift with Vapor
-
Section 22: Swift Package Manager Deep Dive
-
Section 23: Swift Concurrency Deep Dive
-
Section 24: More Language Features
-
Section 25: Error Handling Deep Dive
-
Section 26: Testing Deep Dive
-
Section 27: Data Structures and Algorithms in Swift
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Swift Macros (Swift 5.9+)
-
Section 31: Property Wrappers Ecosystem
-
Section 32: Swift Interop Deep Dive
-
Section 33: iOS App Architecture Patterns
-
Section 34: Performance and Debugging
-
Section 35: App Distribution and CI/CD
-
Section 36: More Practical Projects
-
Section 37: SwiftData and Persistence
-
Section 38: More Design Patterns
-
Section 39: More Review and Practice
-
Section 40: More Foundation Deep Dive
-
Section 41: Advanced Collections in Swift
-
Section 42: Advanced Generics Practice
-
Section 43: UIKit for Legacy and Hybrid Apps
-
Section 44: watchOS and visionOS Development Basics
-
Section 45: More Networking Patterns
-
Section 46: More Testing Practice
-
Section 47: Accessibility in Swift Apps
-
Section 48: Localization
-
Section 49: More Practical Projects Round 2
-
Section 50: Swift Charts Framework
-
Section 51: More Interview and Algorithm Practice
-
Section 52: Final Practice and Mastery
-
Section 53: Swift Compiler and Build System
-
Section 54: More Concurrency Practice
-
Section 55: App Store Guidelines and Review
-
Section 56: More Design and Architecture
41: Property Wrappers Explained
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:
minValueandmaxValue. - The
wrappedValueshould ensure that any value set is clamped between these two bounds. (Hint: Usemax(minValue, min(maxValue, newValue))). - Implement a
projectedValuethat returns aBoolindicating whether the last value set was actually clamped (i.e., the input was outside the bounds). - Create a
Characterstruct with a@Clampedproperty forhealth(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.
There are no comments for now.