Understanding the projectedValue in Swift Property Wrappers

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:

  • Binding Values: It is essential for two-way data binding, particularly useful in SwiftUI.
  • Expose Metadata: You may need to expose additional information like logs, diagnostics, or other tracking details related to a property.
  • Helper Objects: You can provide access to external resources or behaviours that manage the property’s state.
  • Advanced Workflows: Implement complex behaviours such as deferred loading, validation, or state tracking.

? When It’s Worth Using

  1. In SwiftUI: For Binding Support

  • One of the most common use cases of projectedValue is in SwiftUI. In this framework, property wrappers like @State and @Binding use projectedValue to enable two-way binding or data binding.

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:

  • @State stores the value of name.
  • $name provides a binding, enabling two-way data binding between the TextField and the name property. Changes to the TextField automatically update name, and vice versa.

?? Why use it? It allows the TextField to update and reflect the name value dynamically.

2. When You Want to Provide Metadata

  • Use projectedValue to expose additional information about the property.

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

  • Use projectedValue when you need extra behaviour (binding, metadata, helper object).
  • Skip it if your property wrapper only transforms or validates a value.

?? When You Don’t Need projectedValue

While projectedValue is powerful, you should avoid using it in the following cases:

  • Simple Value Transformations: If you only need basic value transformations (e.g., formatting a string), you may not need projectedValue.
  • No Additional Information to Expose: If there’s no need to expose any additional metadata or complex functionality, stick to just wrappedValue.
  • Complexity: If you want to keep your property wrapper simple and easy to understand, avoid introducing unnecessary complexity with projectedValue.

? Key Points:

  • wrappedValue is required in every property wrapper. It holds the primary value and is used for reading and modifying the property.
  • projectedValue is optional but must have this exact name if you want to use the $ syntax.
  • The type of projectedValue can differ from that of wrappedValue. It provides additional functionality or metadata.
  • Swift automatically maps $propertyName to projectedValue.

?? 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

要查看或添加评论,请登录

Rajesh Banka ?的更多文章

社区洞察

其他会员也浏览了