Property Wrappers II
Custom property wrappers.
Property Wrappers II
Custom property wrappers.
0
0
Checkbox to mark video as read
Mark as read

In our first chapter on Property wrappers we saw some SwiftUI native examples that not only allow you to encapsulate functionality that's commonly associated with properties, like @State, but they also allow you to add custom behaviors to properties without duplicating code.

Today we are going to explore how to write our custom Property wrappers to improve readability and maintainability.

Defining A Property Wrapper

In Swift we define it using the @propertyWrapper attribute and can apply it to properties in a class, struct, or enum. The core requirement for a property wrapper is a wrappedValue property that provides the underlying value.

Let's start with a basic example: a property wrapper that logs changes to a property whenever its value is modified.

@propertyWrapper
struct Validator { // 1
    private var value: String
    
    init(wrappedValue: String) { // 2
        self.value = wrappedValue
    }
    
    var wrappedValue: String {
        get { value }
        set {
            print("Email changed from \"\(value)\" to \"\(newValue)\"") // 3
            value = newValue
        }
    }
}

struct User { // 4
    @Validator var email: String
}

var user = User(email: "test")
user.email = "test@educaswift.com"
// Prints: Value changed from "test" to "test@educaswift.com"

What we've done here?:

  1. Define the Validator property wrapper.
  2. The init(wrappedValue:) initializer lets you set an initial value for the property's value (In our case it is email).
  3. For the moment, anytime the property is set, some text is printed indicating the old and the new values.
  4. Finally, we create the User structure and assign this property wrapper to the email property.

If you test this, the end result will be that anytime you change the value of email, a new text is printed in console indicating the change.

Additional Properties In Wrappers

Property wrappers can also contain additional properties and functions to help expand the functionality. Let's extend our wrapper and define a new property inside to indicate the type of field we need to validate.

First, we create a new structure for the fields type. For this example, we will add only the email field:

enum FieldType: String {
    case email
}

Then, let's define a new type property for our wrapper:

private var value: String
private var type: FieldType // Add this line

Now we need to contemplate this new property in the initializer we already defined in the previous section:

init(wrappedValue: String, _ type: FieldType) { // 1
    self.type = type

    assertionFailure(Validator.isValid(wrappedValue, type), "Invalid email") // 2
    self.value = wrappedValue
}

A couple of things are happening here:

  1. Initialize our new type property.
  2. We have this assertion here (An assertion type that don't crash the app at runtime) in order for the app to fail in case we are introducing an invalid email.

Of course, we haven't defined the Validator.isValid() function, and we will do it in a moment, but first, we should define what it will happen when the user sets the value of the property again in the future. The logic behavior would be to check is the email is valid, right?. Let's do it:

var wrappedValue: String {
    get { value }
    set {
        assertionFailure(Validator.isValid(wrappedValue, type), "Invalid email")
        value = newValue
    }
}

We are removing the print we sat early and replace it with an identical behavior to the one in the initializer. Now let's define how this Validator.isValid() function looks like:

private static func isValid(_ value: String, _ type: FieldType) -> Bool { // 1
    switch type {
    case .email:
        return Validator.isValidEmail(value)
    }
}

private static func isValidEmail(_ email: String) -> Bool { // 2
    let regex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
    let predicate = NSPredicate(format: "SELF MATCHES %@", regex)
    return predicate.evaluate(with: email)
}

  1. First we switch over the cases in the type received and execute a function that will in turn be executed to validate whatever data we are trying to validate, in our case, the email entered by the user. This can be extended to work with multiple types of values and to add different validations.
  2. This is where we actually validate the email using a regular expression to match a good email format. The result is passed to the isValid function and then returned to the assertion to fail as needed.

Now we just need to use this new features in our applications:

struct User {
    @Validator(.email) var email: String = ""
}

var user = User(email: "asdfasdf") // Error: Invalid email
user.email = "test@educaswift.com"

As you can see, we now specify the type of data we want to validate. To the eyes of the compiler, email is just an String, but our use of @Validator(.email) is telling the compiler this is a special value.

Using Projected Values

To finish with this already long article, there is another useful thing we can add to our Validator property wrapper, and this is projected values.

Projected Values are nothing more than additional functionality you can expose via the $ syntax. Let's see an example by allowing the user to consult the values of the 2 parts of the entered email:

var projectedValue: (firstPart: String, hostPart: String) {
    (value.components(separatedBy: "@")[0], value.components(separatedBy: "@")[1])
}

We are just returning a tuple with the parts of the entered email separated by the @ symbol. A projected value can be of any kind, and it is designed to work separately from the properties you already defined inside the wrapper. The name of the projected value is the same that the wrapped property (var email: String = "") but with the $ symbol at the beginning.

Let's see how this can be used:

var user = User(email: "test@educaswift.com")
print(user.$email.firstPart)
print(user.$email.hostPart)
// Prints: test
// Prints: educaswift.com

Property wrappers in Swift give you a powerful way to create reusable, encapsulated property logic that can enhance readability and consistency.

0 Comments

Join the community to comment

Be the first to comment

Accept Cookies

We use cookies to collect and analyze information on site performance and usage, in order to provide you with better service.

Check our Privacy Policy