In Xcode 11 Beta 1, the name of the modifier used in Swift was @PropertyDelegate, now it is @PropertyWrapper.

0258-property-wrappers

Property Wrappers are commonly used in SwiftUI, such as @state, @binding, @environment, @environmentobject, etc.

The Property Wrappers feature makes the code more concise and readable, reducing template code and giving users flexibility in customizations.

We saw the future of Swift, and it was full of @s.

Analysis of the

The implementation of the @ Lazy

Prior to Swift 5.1, lazy initialization of an attribute required adding the lazy keyword to the attribute, and lazy processing was hardcoded into a fixed pattern at compile time, with a range of support.

Use lazy to declare a lazily initialized property

lazy var foo = 1738
Copy the code

Without language level support, you would need to write a lot of boilerplate code like this to get the same effect:

struct Foo {
  private var _foo: Int?
  var foo: Int {
    get {
      if let value = _foo { return value }
      let initialValue = 1738
      _foo = initialValue
      return initialValue
    }
    set {
      _foo = newValue
    }
  }
}
Copy the code

With Property wrappers, you can simply declare as

@Lazy var foo = 1738
Copy the code

The use of @lazy is similar to that of Lazy, both simple and straightforward.

So how is the @Lazy property wrapper type implemented?

@propertyWrapper
enum Lazy<Value> {
  case uninitialized(() -> Value)
  case initialized(Value)

  init(wrappedValue: @autoclosure @escaping() - >Value) {
    self = .uninitialized(wrappedValue)
  }

  var wrappedValue: Value {
    mutating get {
      switch self {
      case .uninitialized(let initializer):
        let value = initializer()
        self = .initialized(value)
        return value
      case .initialized(let value):
        return value
      }
    }
    set {
      self = .initialized(newValue)
    }
  }
}
Copy the code

The property wrapper type provides storage for properties that are used as wrappers. The wrappedValue computed attribute provides the true implementation of the wrapper.

@Lazy var foo = 1738
Copy the code

Will be converted to:

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

We can provide a reset(_:) operation on Lazy to set it to the new value:

extension Lazy {
  mutating func reset(_ newValue:  @autoclosure @escaping() - >Value) {
    self = .uninitialized(newValue)
  }
}

_foo.reset(42)
Copy the code

We can add an initialization method.

extension Lazy {
  init(body: @escaping() - >Value) {
    self = .uninitialized(body)
  }
}

func createAString(a) -> String { . }
@Lazy var bar: String  // not initialized yet
_bar = Lazy(body: createAString)
Copy the code

The above code can be declared as a single statement:

@Lazy(body: createAString) var bar: String
Copy the code

At this point @lazy is already richer and more flexible than Lazy.

So is this the only thing the property wrapper can do? To better understand its use, analyze an @file.

@ the analysis of the Field

@propertyWrapper
public struct Field<Value: DatabaseValue> {
  public let name: String
  private var record: DatabaseRecord?
  private var cachedValue: Value?
  
  public init(name: String) {
    self.name = name
  }
  
  public func configure(record: DatabaseRecord) {
    self.record = record
  }
  
  public var wrappedValue: Value {
    mutating get {
      if cachedValue = = nil { fetch() }
      return cachedValue!
    }
    set {
      cachedValue = newValue
    }
  }
  
  public func flush(a) {
    if let value = cachedValue {
      record!.flush(fieldName: name, value)
    }
  }
  
  public mutating func fetch(a) {
    cachedValue = record!.fetch(fieldName: name, type: Value.self)}}Copy the code

We can define our model based on the Field attribute wrapper:

public struct Person: DatabaseModel {
  @Field(name: "first_name") public var firstName: String
  @Field(name: "last_name") public var lastName: String
  @Field(name: "date_of_birth") public var birthdate: Date
}
Copy the code

File allows us to refresh the existing value, get the new value, and retrieve the name of the corresponding field in the database.

@Field(name: "first_name") public var firstName: String
Copy the code

After expansion:

private var _firstName: Field<String> = Field(name: "first_name")

public var firstName: String {
  get { _firstName.wrappedValue }
  set { _firstName.wrappedValue = newValue }
}
Copy the code

Since _firstName is private, this property is not externally accessible and can not be used to provide methods.

@propertyWrapper
public struct Field<Value: DatabaseValue> {
  // ... API as before ...
  / / new
  public var projectedValue: Self {
    get { self }
    set { self = newValue }
  }
}
Copy the code

After adding projectedValue,

@Field(name: "first_name") public var firstName: String
Copy the code

It’s going to expand to

private var _firstName: Field<String> = Field(name: "first_name")

public var firstName: String {
  get { _firstName.wrappedValue }
  set { _firstName.wrappedValue = newValue }
}

public var $firstName: Field<String> {
  get { _firstName.projectedValue }
  set { _firstName.projectedValue = newValue }
}
Copy the code

Projection Properties

ProjectedValue is called Projection Properties, so firstName’s Projection property is $firstName, and it is visible wherever firstName is visible. The projection property is prefixed with $.

With the projection property, we can happily use the following operations

somePerson.firstName = "Taylor"
$somePerson.flush()
Copy the code

This feature has been extensively used in vapor/fluent-kit’s 1.0.0-alpha.3.

To learn more about property wrappers, let’s continue with a few examples.

For example,

Delayed Initialization

variable

@propertyWrapper
struct DelayedMutable<Value> {
  private var _value: Value? = nil
  var wrappedValue: Value {
    get {
      guard let value = _value else {
        fatalError("property accessed before being initialized")}return value
    }
    set {
      _value = newValue
    }
  }

  /// "Reset" the wrapper so it can be initialized again.
  mutating func reset(a) {
    _value = nil}}Copy the code

immutable

@propertyWrapper
struct DelayedImmutable<Value> {
  private var _value: Value? = nil

  var wrappedValue: Value {
    get {
      guard let value = _value else {
        fatalError("property accessed before being initialized")}return value
    }

    // Perform an initialization, trapping if the
    // value is already initialized.
    set {
      if _value ! = nil {
        fatalError("property initialized twice")
      }
      _value = newValue
    }
  }
}
Copy the code

NSCopying

@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}}}Copy the code

User defaults

@propertyWrapper
struct UserDefault<T> {
  let key: String
  let defaultValue: T
  
  var wrappedValue: T {
    get {
      return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
    }
    set {
      UserDefaults.standard.set(newValue, forKey: key)
    }
  }
}

enum GlobalSettings {
  @UserDefault(key: "FOO_FEATURE_ENABLED", defaultValue: false)
  static var isFooFeatureEnabled: Bool
  
  @UserDefault(key: "BAR_FEATURE_ENABLED", defaultValue: false)
  static var isBarFeatureEnabled: Bool
}
Copy the code

AtomicWrite

@propertyWrapper
public struct AtomicWrite<Value> {
    
    // TODO: Faster version with os_unfair_lock?
    
    let queue = DispatchQueue(label: "Atomic write access queue", attributes: .concurrent)
    var storage: Value
    
    public init(initialValue value: Value) {
        self.storage = value
    }
    
    public var wrappedValue: Value {
        get {
            return queue.sync { storage }
        }
        set {
            queue.sync(flags: .barrier) { storage = newValue }
        }
    }
    
    /// Atomically mutate the variable (read-modify-write).
    ///
    /// - parameter action: A closure executed with atomic in-out access to the wrapped property.
    public mutating func mutate(_ mutation: (inout Value) throws -> Void) rethrows {
        return try queue.sync(flags: .barrier) {
            try mutation(&storage)
        }
    }
}
Copy the code

Trimmed

public struct Trimmed {
    private var storage: String!
    private let characterSet: CharacterSet
    
    public var wrappedValue: String {
        get { storage }
        set { storage = newValue.trimmingCharacters(in: characterSet) }
    }
    
    public init(initialValue: String) {
        self.characterSet = .whitespacesAndNewlines
        wrappedValue = initialValue
    }
    
    public init(initialValue: String.characterSet: CharacterSet) {
        self.characterSet = characterSet
        wrappedValue = initialValue
    }
}
Copy the code
@Trimmed
var text = " \n Hello, World! \n\n    "

print(text) // "Hello, World!"

// By default trims white spaces and new lines, but it also supports any character set
@Trimmed(characterSet: .whitespaces)
var text = " \n Hello, World! \n\n    "
print(text) // "\n Hello, World! \n\n"
Copy the code

More strategy, which can be reference guillermomuntaner/Burritos

Property Wrappers have some restrictions

  • Properties Can’t Participate in Error Handling
  • Wrapped Properties Can’t Be Aliased
  • Property Wrappers Are Difficult To Compose
  • Property Wrappers Aren’t First-Class Dependent Types
  • Property Wrappers Are Difficult to Document
  • Property Wrappers Further Complicate Swift

From Swift Property Wrappers.

conclusion

Property wrappers certainly simplify the code. In terms of usage, we can customize various access policies, so there is more room for imagination. Because these policies are essentially constraints on the data store, the robustness and security of the code increases.

To read more, please follow OldBirds’ official wechat account