paint-brush
5 Useful Property Wrappers in Swiftby@jemsberliner
658 reads
658 reads

5 Useful Property Wrappers in Swift

by Maksim ZhelezniakovNovember 7th, 2024
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

Property wrappers were introduced in Swift 5.1. They are a type that encapsulates a piece of logic applied to any properties in general or properties constrained to some specific type you choose. You can add your own initializers, additional methods, properties, etc. that may have any functionality you need. In this guide I’ll cover 5 basic use cases like that.
featured image - 5 Useful Property Wrappers in Swift
Maksim Zhelezniakov HackerNoon profile picture

Introduced in Swift 5.1, property wrappers quickly became one of the most heavily used features in all things Swift-related. All famous now SwiftUI framework could not have probably been the way it is now without its @State, @Binding, @StateObject and other black magic pieces you use every day. Or maybe you heard or even used one of the most popular object relational mapper frameworks (ORM) for a server side Swift’s Vapor called Fluent. They leave you with no choice to figure out how and when to use a dollar sign $ before a property neatly wrapped in yet another wrapper.


The truth is that property wrappers are not that hard to crack when you dive deep and try to use them on your own. In fact, you might probably have a bunch of places in your project where you can get things easier by bringing a wrapper to help. In this guide I’ll cover 5 basic use cases like that. But first, let’s refresh our memories on the subject itself.

What is a property wrapper?

A property wrapper is a specially annotated type that encapsulates a piece of logic applied to any properties in general or properties constrained to some specific type you choose. It can be a struct, enum or class that declared with a @propertyWrapper attribute and has at least a wrappedValue as a mandatory property inside:


@propertyWrapper
struct SomeWrapper<T> {
    var wrappedValue: T { // mandatory property
        get { value }
        set { value = newValue } 
    }

    private var value: T
    
    init(wrappedValue: T) {
        self.value = wrappedValue
    }
}


Inside both a getter and a setter of the wrapped value property you can add any logic you need and intercept, transform or use values in basically any way you want. Then you can use the wrapper like this:


class SomeService {

    @SomeWrapper fancyNumber: Int

    init(number: Int) {
        self._number = SomeWrapper(wrappedValue: number) // use "_" to initialize a property wrapper
    }

    func doSmth() {
        print(fancyNumber)
    }
}


What’s nice about property wrappers is that you can add your own initializers, additional methods, properties, etc. that may have any functionality you need. One of this is an optional projectedValue property that can return even a wrapper itself or any other type. Now, remember: when you want to access something that belongs to the wrapper itself, but not to its value, you need to use a notorious dollar sign:


@propertyWrapper
struct SomeWrapper<T> {
    var wrappedValue: T {
        get { value }
        set { value = newValue } 
    }

    private var value: T
    
    init(wrappedValue: T) {
        self.value = wrappedValue
    }

    func methodToDoSmth() { ... } // extra wrapper method
}

class SomeService {

    ...

    func doSmth() {
        $fancyNumber.methodToDoSmth() // here we access the logic of the wrapper itself
    }
}


Of course, all nonsensical examples above do not demonstrate true power of property wrappers enough, so let’s take a look at some of the most useful ways to make them shine.

1. Storage

Inspired by all the things Fluent can do, we can apply property wrappers to deal with UserDefaults instead of creating a separate service for it this way:


@propertyWrapper
struct DefaultsStorage<T: Codable> {
    private let key: String
    private let defaultValue: T
    
    init(key: String, defaultValue: T) {
        self.key = key
        self.defaultValue = defaultValue
    }
    
    var wrappedValue: T {
        get {
            // getting and decodind the data from UserDefaults
            guard let data = UserDefaults.standard.object(forKey: key) as? Data,
                  let value = try? JSONDecoder().decode(T.self, from: data) else {
                return defaultValue
            }
            return value
        }
        set {
            // encodind the data and saving to UserDefaults
            guard let encoded = try? JSONEncoder().encode(newValue) else {
                return
            }
            let defaults = UserDefaults.standard
            defaults.set(encoded, forKey: key)
        }
    }
}


As you can see, the wrapper above can be used with anything Codable


@DefaultsStorage(key: "upload_date", defaultValue: Date())
var uploadDate: Date


The implementation above can be tweaked and changed a lot depending on your needs. For example, you can make it truly versatile by accepting other types or allow to use suite-based UserDefaults instead of the from-the-box standard singleton.


Another thing is the data you save with it. If you want to store something securely, you may want to write a similar solution for Keychain.

By the way, Apple already has a similar wrapper called @AppStorage, but it was designed to be used with SwiftUI which you might not necessarily want.

2. Logging

In cases where you need to monitor a specific value, the following property wrapper can be useful for you:


@propertyWrapper
struct Logged<Value> {
    private var value: Value
    private let label: String // label that will be used in the log event

    init(wrappedValue: Value, label: String) {
        self.value = wrappedValue
        self.label = label
        print("\(label): Initial value set to \(value)")
    }

    var wrappedValue: Value {
        get { value }
        set {
            // prints the set event to a console
            print("\(label): Value changed from \(value) to \(newValue)")
            value = newValue
        }
    }
}


@Logged(label: "Total Price") var price = Double(12.5)


This property wrapper might become even more useful with additional analytics service injected. It can be something local or even remote. If using the latter, remember to filter at the call site of such service to avoid unnecessary calls.

3. Validation

We often need to validate user’s input. There are so many possible input types, like email, a phone number, birth date and so on. All of these require specific logic piece applied to original data. You can solve this by using a property wrapper for every input:


@propertyWrapper
struct ValidatedEmail<Value: StringProtocol> {
    var value: Value?

    var wrappedValue: Value? {
        get {
            return validate(email: value) ? value : nil
        }
        set {
            value = newValue
        }
    }

    init(wrappedValue value: Value?) {
        self.value = value
    }
    
    private func validate(email: Value?) -> Bool {
        guard let email else { 
            return false 
        }
        let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
        let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx)
        return emailPred.evaluate(with: email)
    }
}


Or you can even try one-size-fits-all solution like this:


@propertyWrapper
struct Validated<Value> {
    // define all the possible inputs
    enum InputType {
        case email
        case phoneNumber 
        ...
    }
    
    let inputType: InputType
    var value: Value?

    var wrappedValue: Value? {
        get {
            // return value if validation succeeds or nil if not
            return validate(value) ? value : nil
        }
        set {
            value = newValue
        }
    }

    init(inputType: InputType, wrappedValue value: Value?) {
        self.inputType = inputType
        self.value = value
    }
    
    private func validate(_ value: Value?) -> Bool {
        // include validation logic for every input type
        switch inputType {
        case .email:
            ...
        case .phoneNumber:
            ...
        }
    }
}


Similar to validating things, you can format dates with property wrappers by defining a set of prefixed formats you’d use inside the wrapper.

4. Clamped

Sometimes you need to limit a value in a certain range, for example having percentage spanning from 0 to 100 or a fraction value from 0 to 1 and so on. You can encapsulate this logic in a property wrapper, too:


@propertyWrapper
struct Clamped<Value: Comparable> {
    private var value: Value
    private let range: ClosedRange<Value>

    init(wrappedValue: Value, _ range: ClosedRange<Value>) {
        self.range = range
        self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
    }

    var wrappedValue: Value {
        get { value }
        set { value = min(max(newValue, range.lowerBound), range.upperBound) }
    }
}


You can use it like this:


struct ClampedExample {
    @Clamped(0...10) var number: Int

    init(number: Int) {
        self._number = Clamped(wrappedValue: number, 0...10)
    }

    func clampTest() {
        number = 20
        print(example.number) // 10, because 20 is clamped to the upper bound 10
        number = -5
        print(example.number) // 0, because -5 is clamped to the lower bound 0
    }
}


5. Base64Encoding

Another use case for property wrappers is to help us out with all things Codable. Custom encoding and decoding logic implementation can be safely put inside a wrapper if needed. You can also automatically encode a String into base64 with this simple solution:


@propertyWrapper
struct Base64Encoding {
    private var value = ""
    
    var wrappedValue: String {
        get { Data(value.utf8).base64EncodedString() }
        set { value = newValue }
    }
}


Using the wrapper above, whenever you access the text transformed by it, it will always return a base64 encoded string.


Misuse of property wrappers

However, when dealing with property wrappers, do not forget that they are same base Swift types we’ve known for years, but with added extra feature on top. One of many popular ideas on how to use a property wrapper back in the past was to try to thread-safe properties with it. You can find multiple implementations of @Atomic online, but it turned out that most of them are quite problematic. Even though fixes also can be found for it, I don’t recommend using @Atomic anymore, especially with actors now available to serve this purpose.

Conclusion

I hope that you learned a lot or at least refreshed your knowledge of property wrappers in Swift. With this refresher take a look at your current projects: maybe you have some pieces that can leverage the power of this feature!