SwiftUIproperty wrapper

  • The motivation
  • What is Property Wrappers?
  • Usage scenarios
  • limitations

The motivation

There are many property implementation patterns that are repetitive, so we need a property mechanism that defines these repeated patterns and uses them in a way that is similar to Swift’s lazy and @nscopying. In addition, lazy and @nscopying are limited in scope and impractical in many cases.

Lazy is an important feature of Swift, and if we want to implement the immutable lazy load property, lazy cannot be implemented. @nscopying just like the copy keyword in OC, assigning a value to an attribute calls the nscopying.copy () method.

/// Swift
@NSCopying var employee: Person
// Equal to OC
@property (copy, nonatomic) Person *employee;
Copy the code

The @nscopying attribute modifies the code to look like this:

// @NSCopying var employee: Person
  var _employee: Person
  var employee: Person {
    get { return _employee }
    set { _employee = newValue.copy() as! Person }
  }
Copy the code

Copy is actually done in the set method, which leads to the problem that copy is not implemented in the initialization method

init( employee candidate: Person ) { /// ... Self. Employee = candidate // }Copy the code

OC can use _property and self.property to control whether to access member variables directly or setter methods. Swift, however, always accesses member variables directly when accessing properties in initialization methods. Grammar doesn’t help either. The setter method cannot be called, which directly results in the @nscopying copy method not being called for deep copy. The usual practice is to manually call the copy method in the initialization method, but this is error prone.

init( employee candidate: Person ) {
   // ...
   self.employee = candidate.copy() as! Person
   // ...
}
Copy the code

What is a Property Wrapper?

The property is wrapped in a Wrapper type. You can separate the property definition from the code that manages the property store. The managed code can be written once and used on multiple properties, leaving the properties to decide which Wrapper to use.

To define a custom wrapper, simply add the @propertyWrapper flag before the custom type:

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

Lazy marks other attributes:

struct UseLazy {
    @Lazy var foo: Int = 1738
}
Copy the code

The above code converts to:

struct UseLazy {
    private var _foo: Lazy<Int> = Lazy<Int>(wrappedValue: 1738)
    var foo: Int {
      get { return _foo.wrappedValue }
      set { _foo.wrappedValue = newValue }
    }
}
Copy the code

Foo will actually become _foo: A Lazy variable that generates the get and set methods of foo and accesses _foo.wrappedValue. So the key to custom wrappers is to implement the necessary logic in the GET and set methods of wrappedValue.

In addition, we can provide more apis for custom wrappers:

@propertyWrapper struct Lazy<T> { var wrappedValue: T func reset() -> Void { ... } // Add a method}Copy the code

Private var _foo: Lazy

= Lazy

(wrappedValue: 1738) Member variables are private, so the func reset() -> Void method can only be called inside the UseLazy structure:

struct UseLazy {
    func useReset() {
        _foo.reset()
    }
}
Copy the code

This is not possible if you want to call it from outside:

Func myfunction() {let u = UseLazy() private var _foo: u._foo.reset()}Copy the code

If you want to call the Wrapper API from outside, you need to use projectedValue. You need to implement the projectedValue property in a custom Wrapper type:

@propertyWrapper struct Lazy<T> { var wrappedValue: T public var projectedValue: Self { get { self } set { self = newValue } } func reset() { ... }}Copy the code

After declaring projectedValue, @lazy var foo: Int = 1738 converts to the following code, which generates more get and set methods for $foo

private var _foo: Lazy<Int> = Lazy<Int>(wrappedValue: 1738)
var foo: Int {
   get { return _foo.wrappedValue }
   set { _foo.wrappedValue = newValue }
}
public var $foo: Lazy<Int> {
   get { _foo.projectedValue }
   set { _foo.projectedValue = newValue }
}
Copy the code

Public var $foo: Lazy

is public, and $foo gets _foo.projectedValue. ProjectedValue get returns self, so calling U.$foo actually returns _foo: Lazy. This calls the reset() method from the outside world.

func myfunction() {
    let u = UseLazy()
    // u.$foo -> _foo.projectedValue -> _foo
    u.$foo.reset()
}
Copy the code

Usage scenarios

  • UserDefault
  • @NSCopyingThe problem
  • The Property Wrapper limits the scope of data
  • Record changes to data (Project Value)

UserDefault

If we wanted to store some values in UserDefault, such as whether it was the first startup, and font information, we might have done this previously:

struct GlobalSetting {
    static var isFirstLanch: Bool {
        get {
            return UserDefaults.standard.object(forKey: "isFirstLanch") as? Bool ?? false
        } set {
            UserDefaults.standard.set(newValue, forKey: "isFirstBoot")
        }
    }
    static var uiFontValue: Float {
        get {
            return UserDefaults.standard.object(forKey: "uiFontValue") as? Float ?? 14
        } set {
            UserDefaults.standard.set(newValue, forKey: "uiFontValue")
        }
    }
}
Copy the code

As you can see, the code above is duplicated, which would result in more duplicated code if you had to store more information. These problems can be easily solved with the Property Wrapper.

@propertyWrapper struct UserDefault<T> { let key: String let defaultValue: T var wrappedValue: T { get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue } set { UserDefaults.standard.set(newValue, forKey: key) } } } struct GlobalSetting { @UserDefault(key: "isFirstLaunch", defaultValue: true) static var isFirstLaunch: Bool @UserDefault(key: "uiFontValue", defaultValue: 12.0) static var uiFontValue: Float}Copy the code

Using @propertyWrapper to decorate the UserDefault structure, UserDefault can then modify other properties. IsFirstLaunch, uiFontValue, Its properties are stored by UserDefault. Launching isFirstLaunch is equivalent to:

struct GlobalSettings {
    static var $isFirstLanch = UserDefault<Bool>(key: "isFirstLanch", defaultValue: false)
    static var isFirstLanch: Bool {
        get {
            return $isFirstLanch.value
        }
        set {
            $isFirstLanch.value = newValue
        }
    }
}
Copy the code

@NSCopyingThe problem

Definition of the Person type

class Person: NSObject, NSCopying { var firstName: String var lastName: String var job: String? init( firstName: String, lastName: String, job: String? = nil ) { self.firstName = firstName self.lastName = lastName self.job = job super.init() } /// Conformance to <NSCopying> protocol func copy( with zone: NSZone? = nil ) -> Any { let theCopy = Person.init( firstName: firstName, lastName: lastName ) theCopy.job = job return theCopy } /// For convenience of debugging override var description: String { return "\(firstName) \(lastName)" + ( job ! = nil ? ", \(job!) ": "")}}Copy the code

Implement a more copy-capable wrapper:

@propertyWrapper struct Copying<Value: NSCopying> { private var _value: Value init(wrappedValue value: Value) { // Copy the value on initialization. self._value = value.copy() as! Value } var wrappedValue: Value { get { return _value } set { // Copy the value on reassignment. _value = newValue.copy() as! Value } } } class Sector: NSObject { @Copying var employee: Person init( employee candidate: Person ) { self.employee = candidate super.init() assert( self.employee ! == candidate ) } override var description: String { return "A Sector: [ ( \(employee) ) ]" } }Copy the code

The employee above is decorated with @copying for deep copy

let jack = Person(firstName: "Jack", lastName: "Laven", job: "CEO")
let sector = Sector(employee: jack)
jack.job = "Engineer"
    
print(sector.employee) // Jack Laven, CEO
print(jack) // Jack Laven, Engineer
Copy the code

The Property Wrapper limits the scope of data

You can also customize the wrapper constraint data range,

@propertyWrapper
struct ColorGrade {
    private var number: Int
    init() { self.number = 0 }
    var wrappedValue: Int {
        get { return number }
        set { number = max(0, min(newValue, 255)) }
    }
}
Copy the code

Above we define a wrapper that limits the range of color components and, in the set method, ensures that the assignment does not exceed 255. We can then define a color type with the @colorgrade modifier.

struct ColorType {
    @ColorGrade var red: Int
    @ColorGrade var green: Int
    @ColorGrade var blue: Int
    
    public func showColorInformation() {
        print("red:\(red) green:\(green) blue:\(blue)")
    }
}
Copy the code
var c = ColorType()
c.showColorInformation() // red:0 green:0 blue:0
c.red = 300
c.green = 12
c.blue = 100
c.showColorInformation() // red:255 green:12 blue:100
Copy the code

Struct ColorType = 0; struct ColorType = 0; struct ColorType = 0;

@propertyWrapper
struct ColorGradeWithMaximum {
    private var maximum: Int
    private var number: Int
    
    init() {
        self.number = 0
        self.maximum = 255
    }
    init(wrappedValue: Int) {
        maximum = 255
        number = min(wrappedValue, maximum)
    }
    init(wrappedValue: Int, maximum: Int) {
        self.maximum = maximum
        number = min(wrappedValue, maximum)
    }
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, maximum) }
    }
}
Copy the code

This allows you to specify an upper limit and an initial value when you declare the property

struct ColorTypeWithMaximum { @ColorGradeWithMaximum var red: Int // use init() // @ColorGradeWithMaximum var green: Int = 100 @colorgradeWithMaximum (wrappedValue: 100) var green: Int (wrappedValue: 100) 2) @ColorGradeWithMaximum(wrappedValue: 90, maximum: 255) var blue: Int // (wrappedValue: 90, maximum:255) public func showColorInformation() { print("red:\(red) green:\(green) blue:\(blue)") } }Copy the code
  • @ColorGradeWithMaximum var red: Int

Is to use init() as the default initialization method.

  • @ColorGradeWithMaximum var green: Int = 100@ColorGradeWithMaximum(wrappedValue: 100) var green: Int

The above two methods are equivalent and the initialization method (wrappedValue: 2) is called.

  • @ColorGradeWithMaximum(wrappedValue: 90, maximum: 255) var blue: Int

This is the initialization method of the call (wrappedValue: 90, maximum:255).

projectedValue

A higher function of the property wrapper is provided with a projected value, for example, to record changes in data. The property name of a projected value is the same as wrapped Value except that it is accessed using $.

@propertyWrapper
struct Versioned<Value> {
    private var value: Value
    private(set) var projectedValue: [(Date, Value)] = []
    
    var wrappedValue: Value {
        get { value }
        set {
            defer { projectedValue.append((Date(), value)) }
            value = newValue
        }
    }
    
    init(initalizeValue: Value) {
        self.value = initalizeValue
        projectedValue.append((Date(), value))
    }
}

class ExpenseReport {
    enum State { case submitted, received, approved, denied }
    @Versioned(initalizeValue: .submitted) var state: State
}
Copy the code

In Versioned, we use projectedValue to record data changes. You can use $state to access the projectedValue to see the history of data changes:

let report = ExpenseReport() // print: [(13:36:07 2020-11-08 + 0000, SwiftUIDemo.ExpenseReport.State.submitted)] print(report.$state) // `projectedValue` report.state = .received report.state = .approved // print:[ // (2020-11-08 13:36:07 +0000, SwiftUIDemo.ExpenseReport.State.submitted), // (2020-11-08 13:36:07 +0000, SwiftUIDemo.ExpenseReport.State.received), // (2020-11-08 13:36:07 +0000, SwiftUIDemo.ExpenseReport.State.approved) // ] print(report.$state)Copy the code

Limitations of the Property Wrapper

  • Cannot be used for properties in the protocol.
  • Can no longer be used in enum.
  • The Wrapper property does not define getter or setter methods.
  • Cannot be used in extension because extension does not have storage properties.
  • The Wrapper property in class cannot override other properties.
  • The Wrapper property cannot belazy,@NSCopying,@NSManaged,weak,or,unowned.

In this paper, the demo

References

  • How to use @ObservedObject to manage state from external objects
  • Swift UI programming Guide
  • SwiftUI data flow
  • Swift.org
  • the-swift-51-features-that-power-swiftuis-api
  • awesome-function-builders
  • create-your-first-function-builder-in-5-minutes
  • deep-dive-into-swift-function-builders
  • SwiftUI changes in Xcode 11 Beta 5
  • Property Wrappers