The Property Wrapper is a new feature of the Swift language that allows us to customize the types that implement the functionality of get and set methods and use them everywhere. In this article, we’ll look at all things Property wrappers:

  • What problems did they solve?
  • How do I implement a property wrapper?
  • How do I access property wrappers, wrapped values, and projections?
  • How do I use property wrappers in my code?
  • What are the limitations of property wrappers?

To understand the Property Wrappers

To better understand property wrappers, let’s take an example of what problems they can solve. Suppose we want to add a logging capability to our app. Each time a property changes, we print its new value to the Xcode console. This is useful when tracking errors or tracking data streams. The immediate way to do this is to override the setter:

struct Bar {
    private var _x = 0
    
    var x: Int {
        get { _x }
        set {
            _x = newValue
            print("New value is \(newValue)")}}}var bar = Bar()
bar.x = 1 // Prints 'New value is 1'
Copy the code

If we keep recording more of these properties, the code will quickly become a mess. Instead of copying the same code over and over again for each new property, we declare a new type that will perform logging:

struct ConsoleLogged<Value> {
    private var value: Value
    
    init(wrappedValue: Value) {
        self.value = wrappedValue
    }

    var wrappedValue: Value {
        get { value }
        set { 
            value = newValue
            print("New value is \(newValue)")}}}Copy the code

Here’s how we override the Bar with the ConsoleLogged:

struct Bar {
    private var _x = ConsoleLogged<Int>(wrappedValue: 0)
    
    var x: Int {
        get { _x.wrappedValue }
        set { _x.wrappedValue = newValue }
    }
}

var bar = Bar()
bar.x = 1 // Prints 'New value is 1'
Copy the code

Swift provides linguistic support for this pattern. All we need to do is add the @propertyWrapper property to our ConsoleLogged type:

@propertyWrapper
struct ConsoleLogged<Value> {
    // The rest of the code is the same.
}
Copy the code

You can think of the Property Wrapper as a regular property that delegates get and set methods to other types.

Where the property is declared, we can specify which wrapper implements it:

struct Bar {@ConsoleLogged var x = 0
}

var bar = Bar()
bar.x = 1 // Prints 'New value is 1'
Copy the code

The attribute @consolelogged is a syntax sugar that translates to the previous version of our code.

Translator’s note: @propertyWrapper removes some duplication or similar code.

Use the Property Wrapper

For Property Wrapper types [1], there are two requirements:

  1. You must use attributes@propertyWrapperDefine.
  2. It has to havewrappedValueProperties.

Here is the simplest wrapper “Yang Zi” :

@propertyWrapper
struct Wrapper<T> {
   var wrappedValue: T
}
Copy the code

Now, we can use @wrapper:

struct HasWrapper {@Wrapper var x: Int
}

let a = HasWrapper(x: 0)
Copy the code

We can pass the default value to the wrapper in two ways:

struct HasWrapperWithInitialValue {@Wrapper var x = 0 / / 1
    @Wrapper(wrappedValue: 0) var y / / 2
}
Copy the code

There is a difference between the above two statements:

  1. The compiler implicitly calls init(wrappedValue:) to initialize x with 0.

  2. The initialization method is explicitly specified as part of the property.

Access the property wrapper

It is often useful to provide additional behavior in the property wrapper:

@propertyWrapper
struct Wrapper<T> {
    var wrappedValue: T

    func foo(a) { print("Foo")}}Copy the code

We can access the wrapper type by adding an underscore to the variable name:

struct HasWrapper {@Wrapper var x = 0

    func foo(a) { _x.foo() }
}
Copy the code

_x here is an instance of the wrapper, so we can call foo (). However, calling it from outside HasWrapper results in a compilation error:

let a = HasWrapper()
a._x.foo() // ❌ '_x' is inaccessible due to 'private' protection level
Copy the code

The reason is that synthetic wrappers have a private access level by default. We can use Projection to solve this problem.

The property wrapper exposes more API by defining the projectedValue property. There are no restrictions on the type of projectedValue.

@propertyWrapper
struct Wrapper<T> {
    var wrappedValue: T

    var projectedValue: Wrapper<T> { return self }

    func foo(a) { print("Foo")}}Copy the code

The $sign is a syntactic sugar to access wrapper properties:

let a = HasWrapper()
a.$x.foo() // Prints 'Foo'
Copy the code

In summary, there are three ways to access the wrapper:

struct HasWrapper {@Wrapper var x = 0
    
    func foo(a) {
        print(x) // `wrappedValue`
        print(_x) // wrapper type itself
        print($x) // `projectedValue`}}Copy the code

Use restrictions

Property wrappers are not without limits. They imposed many restrictions:

  • Attributes with wrappers cannot be overridden in subclasses.
  • Properties that have a wrapper cannot belazy.@NSCopying.@NSManaged.weakorunowned.
  • Properties that have wrappers cannot have customsetorgetMethods.
  • wrappedValue.Init (wrappedValue:)andprojectedValueYou must have the same level of access control as the wrapper type itself
  • You cannot declare a property with a wrapper in a protocol or extension.

Using the example

When attribute wrappers really come into play, they have many usage scenarios. Built into the SwiftUI framework: @state, @Published, @observedobject, @environmentobject and @Environment all use it. Others are widely used in the Swift community, such as:

  • www.vadimbulavin.com/swift-atomi…
  • Github.com/SvenTiigi/V…
  • www.avanderlee.com/swift/prope…
  • Github.com/marksands/B…

conclusion

Attribute wrappers are a powerful feature of Swift 5 that adds a layer of encapsulation between managing how attributes are stored and the code that defines them:

When deciding to use attribute wrappers, be sure to consider their disadvantages:

  • Property wrappers have multiple language restrictions, as described in the usage Restrictions section above.
  • Swift 5.1, Xcode 11 and iOS 13 are required for the properties wrapper.
  • Attribute wrappers add more syntactic sugar to Swift, which makes it harder to understand and adds a barrier to entry for newcomers.

Original text: Vadim Bulavin

Music Coding

Reference:

[1] : github.com/apple/swift…

[3] : docs.swift.org/swift-book/…

【 2 】 : www.jianshu.com/p/ff4c048f0…