SwiftUI
- property wrapper
- 设计动机
- 什么是 Property Wrappers?
- 使用场景
- 局限性
设计动机
有很多的属性实现模式都是重复的,所以我们需要一套属性机制能够定义这些重复的模式,并且使用体验能和 Swift 提供的属性模式类似,例如 Swift 提供的 lazy
和 @NSCopying
。另外,lazy
和 @NSCopying
的使用范围有限、很多情况下不实用。
lazy
是 Swift 的一个重要特性,如果我们希望实现不可变的懒加载属性,lazy
是不能实现的。
@NSCopying
和 OC 里面的 copy
关键字功能一样,给属性赋值会调用 NSCopying.copy()
方法。
/// Swift
@NSCopying var employee: Person
// Equal to OC
@property (copy, nonatomic) Person *employee;
@NSCopying
修饰属性,会将代码转为下面这样:
// @NSCopying var employee: Person
var _employee: Person
var employee: Person {
get { return _employee }
set { _employee = newValue.copy() as! Person }
}
其实是在 set 方法中进行 copy 操作,这会导致一个问题,在初始化方法中无法实现 copy 功能
init( employee candidate: Person ) {
/// ...
self.employee = candidate // 浅拷贝
/// ...
}
在 OC 里可以通过 _property
、self.property
控制是直接访问成员变量还是访问 setter 方法,但 Swift 里,在初始化方法中访问属性时总是直接访问成员变量,不能访问 setter 方法,即时用 self.
语法也没用。不能调用 setter 方法,这就直接导致不能调用 @NSCopying
的 copy
方法实现深拷贝。通常的做法是在初始化方法里面手动调用 copy 方法,但这很容易出错。
init( employee candidate: Person ) {
// ...
self.employee = candidate.copy() as! Person
// ...
}
什么是 Property Wrapper ?
就是将属性用 wrapper 类型包裹一层。可以将属性定义、管理属性存储的代码分开,管理的代码只要写一次,就可以用在多个属性上,让属性自己决定使用哪个 wrapper。
自定义 wrapper 只需在自定义类型前添加 @propertyWrapper
标记:
@propertyWrapper
struct Lazy<T> {
var wrappedValue: T
}
上面的 Lazy 就可以对其他属性进行标记:
struct UseLazy {
@Lazy var foo: Int = 1738
}
上面的代码会转换为:
struct UseLazy {
private var _foo: Lazy<Int> = Lazy<Int>(wrappedValue: 1738)
var foo: Int {
get { return _foo.wrappedValue }
set { _foo.wrappedValue = newValue }
}
}
foo 实际上会变成 _foo: Lazy 类型的变量,并且会生成 foo 的 get、set 方法,在get、set 方法中访问 _foo.wrappedValue,所以自定义 wrapper 的关键就是在 wrappedValue 的 get、set 方法中实现需要的逻辑。
另外,我们可以为自定义的 wrapper 提供更多 API:
@propertyWrapper
struct Lazy<T> {
var wrappedValue: T
func reset() -> Void { ... } // 添加一个方法
}
由于 Wrapper 生成的 private var _foo: Lazy<Int> = Lazy<Int>(wrappedValue: 1738)
成员变量是私有的,所以只能在 UseLazy
结构体内部调用 func reset() -> Void
方法:
struct UseLazy {
func useReset() {
_foo.reset()
}
}
如果想在外界这样调用则是不可以的:
func myfunction() {
let u = UseLazy()
// 不能直接访问 private var _foo:
u._foo.reset()
}
如果想在外界调用 wrapper 的API,需要借助 projectedValue
,需要在自定义的 wrapper 类型中实现 projectedValue
属性:
@propertyWrapper
struct Lazy<T> {
var wrappedValue: T
public var projectedValue: Self {
get { self }
set { self = newValue }
}
func reset() { ... }
}
声明 projectedValue
后,@Lazy var foo: Int = 1738
会转换成下面的代码,会多生成 $foo
的 get、set 方法
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 }
}
由于 public var $foo: Lazy<Int>
是 public 的,在外界可以通过 $foo
拿到 _foo.projectedValue
,projectedValue
的 get 返回 self,所以调用 u.$foo
实际返回的就是 _foo: Lazy
。这样在外界就能调用 reset()
方法了。
func myfunction() {
let u = UseLazy()
// u.$foo -> _foo.projectedValue -> _foo
u.$foo.reset()
}
使用场景
- UserDefault
@NSCopying
问题- Property Wrapper 限制数据范围
- 记录数据的变化(projectedValue)
UserDefault
如果想在 UserDefault 中存储一些值,例如存储是否为第一次启动、和字体信息,之前我们可能会这样做:
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")
}
}
}
可以看到上面的代码是重复的,如果要存储更多的信息,会导致更多重复的代码。 有了 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
}
使用 @propertyWrapper 修饰 UserDefault 结构体,UserDefault 就可以修饰其他属性了,isFirstLaunch、uiFontValue 用 @UserDefault
后,其属性的存储就由 UserDefault 来实现。将 isFirstLaunch 展开就是相当于:
struct GlobalSettings {
static var $isFirstLanch = UserDefault<Bool>(key: "isFirstLanch", defaultValue: false)
static var isFirstLanch: Bool {
get {
return $isFirstLanch.value
}
set {
$isFirstLanch.value = newValue
}
}
}
@NSCopying
问题
Person 类型的定义
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!)" : "" )
}
}
实现一个具有拷贝更能的 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) ) ]"
}
}
上面的 employee 用 @Copying 修饰后实现深拷贝
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
property wrapper 限制数据范围
还可以自定义 wrapper
约束数据的取值范围,
@propertyWrapper
struct ColorGrade {
private var number: Int
init() { self.number = 0 }
var wrappedValue: Int {
get { return number }
set { number = max(0, min(newValue, 255)) }
}
}
上面定义一个限制颜色分量取值范围的 wrapper ,在 set 方法中,保证赋值不超过 255。我们就可以用 @ColorGrade
修饰其他属性定义一个颜色类型了。
struct ColorType {
@ColorGrade var red: Int
@ColorGrade var green: Int
@ColorGrade var blue: Int
public func showColorInformation() {
print("red:\(red) green:\(green) blue:\(blue)")
}
}
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
上面的 ColorGrade
的上限是写死的,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) }
}
}
这样就可以指定上限,在声明属性的时候指定初始值
struct ColorTypeWithMaximum {
@ColorGradeWithMaximum var red: Int // use init()
// @ColorGradeWithMaximum var green: Int = 100 // 和下面的写法一样
@ColorGradeWithMaximum(wrappedValue: 100) var green: Int // (wrappedValue: 2)
@ColorGradeWithMaximum(wrappedValue: 90, maximum: 255) var blue: Int // (wrappedValue: 90, maximum:255)
public func showColorInformation() {
print("red:\(red) green:\(green) blue:\(blue)")
}
}
@ColorGradeWithMaximum var red: Int
是使用 init()
默认初始化方法。
@ColorGradeWithMaximum var green: Int = 100
和@ColorGradeWithMaximum(wrappedValue: 100) var green: Int
上面 2 种写法等价,会调用 (wrappedValue: 2)
的初始化方法。
@ColorGradeWithMaximum(wrappedValue: 90, maximum: 255) var blue: Int
这是调用 (wrappedValue: 90, maximum:255)
的初始化方法。
projectedValue
使用 projected value ,可以让 property wrapper 提供更多的功能,例如可以记录一个数据的变化,projected value 的属性名和 wrapped value 一样,不同之处是需要用 $ 访问。
@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
}
在 Versioned 中我们用 projectedValue 记录下数据的变化过程,可以用 $state 访问到 projectedValue 查看数据修改的历史:
let report = ExpenseReport()
// print: [(2020-11-08 13:36:07 +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)
property wrapper 的局限性
- 不能在协议里的属性使用。
- 不能再 enum 里用。
- wrapper 属性不能定义 getter or setter 方法。
- 不能再 extension 里用,因为 extension 里面不能有存贮属性。
- class 里的 wrapper property 不能覆盖其他的 property。
- wrapper 属性不能是
lazy
、@NSCopying
、@NSManaged
、weak
、或者
、unowned
.
本文 demo
References
- How to use @ObservedObject to manage state from external objects
- Swift UI 编程指南
- SwiftUI 数据流
- 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