SwiftUI 带来的 Swift 5.1 的新特性比框架本身更重要。我们可以预见到,这些新的语言特性很快会被各个库作者所使用。在上一篇中,我们解释了 SwiftUI 代码中 some View
的 some
是什么以及它为何很重要 SwiftUI 和 Swift 5.1 新特性(1) 不透明返回类型 Opaque Result Type。在这篇中,我们需要一起学习下 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
}
}
}
然而如果有很多属性都是这样的逻辑,这样的写法是很冗余的。所以属性代理就是解决这个问题的:
@propertyDelegate
public 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
}
}
}
// 应用属性代理 LateInitialized
public struct MyType {
@LateInitialized public var text: String?
}
属性代理 LateInitialized
是一个泛型类型,它本身用 @propertyDelegate
修饰,它必须有一个叫 value
的属性类型为 Value
,有了这些约定后,编译器可以为 MyType
的 text
生成以下代码:
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
@propertyDelegate
public 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
}
}
}
// 应用属性代理 DefensiveCopying
public 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
,这也印证了我们之前说的$path
和path
的本质。但是这样的语法毕竟有点难看,在不需要$path
出现的时候应该尽可能隐藏它:
public struct MyType {
@DefensiveCopying(withoutCopying: UIBezierPath())
public var path: UIBezierPath
}
3. 包装 UserDefaults 的存取
我们经常需要将属性写成针对UserDefaults存取的计算属性,而这个通用访问策略也能用属性代理实现:
@propertyDelegate
struct 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)
}
}
}
// 应用属性代理 UserDefault
enum 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 的知识,我们在下一篇中,给大家详细说一说。
相关文章:
SwiftUI 和 Swift 5.1 新特性(1) 不透明返回类型 Opaque Result Type
扫描下方二维码,关注“面试官小健”
image
网友评论