SwiftUI
– property 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
@NSCopying
The 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
@NSCopying
The 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 be
lazy
,@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