cover_image

SwiftUI 和 Swift 5.1 新特性(2) 属性代理Property Delegates

小健 该账号已冻结
2019年06月09日 13:46

SwiftUI 带来的 Swift 5.1 的新特性或许比框架本身更重要。我们可以预见到,这些新的语言特性很快会被各个库作者所使用。在上一篇中,我们解释了 SwiftUI 代码中 some Viewsome 是什么以及它为何很重要 Swift 5.1 协议新特性:不透明结果类型。在这篇中,我们需要一起学习下 Swift UI 中 @State@Binding 的准备知识, 这种标记的本质是属性代理(Property Delegates),也叫属性包装器(Property Wrappers)。代码如下:

struct OrderForm : View {  @State private var order: Order    var body: some View {    Stepper(value: $order.quantity, in: 1...10) {      Text("Quantity: \(order.quantity)")    }  }}

这个语言特性非常通用,任何对于属性的存取有“套路”的访问,都可以用它来包装这种“套路”。我们先来学习一下几个套路。


1.包装懒初始化逻辑

为了实现属性 text 为懒初始化的属性,我们可以写成如下代码:

public struct MyType {  var textStorage: String? = nil    public var text: String {    get {      guard let value = textStorage else {        fatalError("text has not yet been set!")      }      return value    }        set {      textStorage = newValue    }  }}

然而如果有很多属性都是这样的逻辑,这样的写法是很冗余的。所以属性代理就是解决这个问题的:

@propertyDelegatepublic struct LateInitialized<Value> {  private var storage: Value?    public init() {    storage = nil  }    public var value: Value {    get{      guard let value = storage else {        fatalError("value has not yet been set!")      }      return value    }    set {      storage = newValue    }  }}
// 应用属性代理 LateInitializedpublic struct MyType { @LateInitialized public var text: String?}

属性代理 LateInitialized 是一个泛型类型,它本身用 @propertyDelegate 修饰,它必须有一个叫 value 的属性类型为 Value,有了这些约定后,编译器可以为 MyTypetext 生成以下代码:


public struct MyType {  var $text: LateInitialized<String> = LateInitialized<String>()
public var text: String { get { $text.value } set { $text.value = newValue} }}

可以看到,经过属性代理包装过后的 text,编译器帮助生成了一个存储属性为 $text,类型就是这个属性代理,而 text本身变成了一个计算属性。大家可能觉得 $text属性是编译器生成的,所以不可以访问,事实恰恰相反,text$text 都可以用。


2.包装防御性拷贝

我们再来看一下一个防御性拷贝的例子,它基于 NSCopying

@propertyDelegatepublic struct DefensiveCopying<Value: NSCopying> {  private var storage: Value    public init(initialValue value: Value) {    storage = value.copy() as! Value  }    public var value: Value {    get { storage }    set {      storage = newValue.copy() as! Value    }  }}
// 应用属性代理 DefensiveCopyingpublic struct MyType { @DefensiveCopying public var path: UIBezierPath = UIBezierPath()}

属性代理 DefensiveCopying 的不同点在于它的初始化函数 init(initialValue:),这个函数由于编译器的约定,所以一定得叫这个名字。与上个例子一样,编译器会生成存储属性 $path,并用初始值初始化。

这里我们吹毛求疵一下,UIBezierPath 被强制拷贝了一次,所以我们再提供一个属性代理的初始化函数,并应用它:

// DefensiveCopying 中增加  public init(withoutCopying value: Value) {    storage = value  }  // 应用不拷贝的初始化函数public struct MyType {  @DefensiveCopying public var path: UIBezierPath    init() {    $path = DefensiveCopying(withoutCopying: UIBezierPath())  }}

在应用的部分我们看到可以像初始化一个一般变量一样初始化$path,这也印证了我们之前说的$pathpath的本质。但是这样的语法毕竟有点难看,在不需要$path 出现的时候应该尽可能隐藏它:

public struct MyType {  @DefensiveCopying(withoutCopying: UIBezierPath())  public var path: UIBezierPath}

3. 包装 UserDefaults 的存取

我们经常需要将属性写成针对UserDefaults存取的计算属性,而这个通用访问策略也能用属性代理实现:

@propertyDelegatestruct UserDefault<T> {  let key: String  let defaultValue: T    var value: T {    get {      return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue    }    set {      UserDefaults.standard.set(newValue, forKey: key)    }  }}
// 应用属性代理 UserDefaultenum GlobalSettings { @UserDefault(key: "FOO_FEATURE_ENABLED", defaultValue: false) static var isFooFeatureEnabled: Bool @UserDefault(key: "BAR_FEATURE_ENABLED", defaultValue: false) static var isBarFeatureEnabled: Bool}

结语

所有对于属性访问策略的抽象,都可以使用属性代理来实现,我们还可以想到 Thread-local storage(线程本地存储)属性存取、原子属性存取、Copy-on-write 属性存取、引用包装类型属性的存取都可以使用属性代理来实现。当然 SwiftUI 的 @State@Binding 也是属性代理,要详细解释它们,还需要一些 Swift 的知识,我们在下一篇中,给大家详细说一说。


相关文章:

Swift 5.1 协议新特性:不透明结果类型

图片

扫描上方二维码,关注“面试官小健”


继续滑动看下一个
该账号已冻结
向上滑动看下一个