Introduction
Swift property wrappers can expose a projected value (accessed with $property) by defining a projectedValue property. When a property wrapper does not define this, the $ prefix syntax produces a compiler error. This is common when creating custom property wrappers or when expecting a projected value from a third-party wrapper that does not provide one. The projected value is essential for patterns like @State providing Binding, @Published providing Publisher, and @AppStorage providing Binding.
Symptoms
Cannot find '$propertyName' in scopecompiler error- Custom property wrapper does not expose
$prefix access - Expected Binding from property wrapper but gets wrapped value instead
projectedValueproperty missing from wrapper type definition- Cannot pass
$propertyto child views expecting Binding
Error: ```swift @CustomStorage(key: "username") var username: String = ""
// Error: Value of type 'CustomStorage<String>' has no member 'projectedValue' TextField("Name", text: $username) ```
Common Causes
- Custom property wrapper missing
projectedValuedefinition - Using
$prefix on a wrapper that does not support it - Expecting projected value type to match SwiftUI's Binding pattern
- Wrapper defines
wrappedValuebut forgetsprojectedValue - Third-party wrapper version does not include projected value
Step-by-Step Fix
- 1.Add projectedValue to custom property wrapper:
- 2.```swift
- 3.@propertyWrapper
- 4.struct CustomStorage<T> {
- 5.private let key: String
- 6.private let defaultValue: T
var wrappedValue: T { get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue } set { UserDefaults.standard.set(newValue, forKey: key) } }
// WRONG - no projectedValue, so $username does not work
// CORRECT - add projectedValue var projectedValue: Binding<T> { Binding( get: { wrappedValue }, set: { wrappedValue = $0 } ) }
init(key: String, defaultValue: T) { self.key = key self.defaultValue = defaultValue } }
// Now this works: struct SettingsView: View { @CustomStorage(key: "username", defaultValue: "") var username
var body: some View { TextField("Username", text: $username) // $username is Binding<String> } } ```
- 1.Project a different type than the wrapped value:
- 2.```swift
- 3.@propertyWrapper
- 4.struct ValidatedText {
- 5.private var value: String
- 6.private let validator: (String) -> Bool
var wrappedValue: String { get { value } set { value = newValue } }
// Project a validation result instead of a Binding var projectedValue: Bool { validator(value) }
init(wrappedValue: String, validator: @escaping (String) -> Bool) { self.value = wrappedValue self.validator = validator } }
struct SignUpView: View { @ValidatedText(validator: { $0.contains("@") }) var email = ""
var body: some View { VStack { TextField("Email", text: $email) // $email is Binding<String> (default projected value from @State)
// But $email from @ValidatedText is Bool (our custom projected value) if !$email { // Uses projectedValue: Bool Text("Invalid email format").foregroundColor(.red) } } } } ```
- 1.Handle property wrapper composition correctly:
- 2.```swift
- 3.// When combining wrappers, each needs its own projectedValue
- 4.@propertyWrapper
- 5.struct LoggedValue<T> {
- 6.var wrappedValue: T {
- 7.didSet {
- 8.print("Value changed to \(wrappedValue)")
- 9.}
- 10.}
// Provide projectedValue as Binding if T is Equatable var projectedValue: Binding<T> { Binding(get: { wrappedValue }, set: { wrappedValue = $0 }) }
init(wrappedValue: T) { self.wrappedValue = wrappedValue } } ```
Prevention
- Always define
projectedValuewhen creating custom property wrappers - Project
Binding<T>for UI-related wrappers used with SwiftUI - Project
Publisherfor reactive wrappers - Document what
$propertyreturns for each custom wrapper - Test projected value access in unit tests
- Use
Bindingprojection for any wrapper that will drive SwiftUI views