从SwiftUI的@State来看看Property Wrapper

2,562 阅读6分钟

本文缘起于一个晚上,看着SwiftUI突然想象,@State怎么这么能干,我就想看看它的羁绊,顺便向大家呼唤一个点赞~

@State是什么?

我想每一个学习SwiftUI的人,肯定会看到这样的代码:

struct PlayButton: View {
    @State private var isPlaying: Bool = false

    var body: some View {
        Button(isPlaying ? "Pause" : "Play") {
            isPlaying.toggle()
        }
    }
}

这其中有一个和以往不太一样的东西:@State,在最开始的时候,我以为它是类似于@objc 之类的这种关键字,结果发现不然,因为我可以直接从Xcode中跳转查看@State对外暴露的API:

@frozon @propertyWrapper public struct State<Value>: DynamicProperty {
			public init(wrapperdValue value: Value){...}

			public init(initialValue value: Value){...}
			
			public var wrappedValue: Value { get nonmutating set }
			
			public var projectedValue: Binding<Value> { get }
}

@PropertyWrapper是什么?

看到这里,我想大家都意识到了@State 只是表现形式,真正关键的是@propertyWrapper 也就是属性包装器,在第一次遇到这种概念的时候,难免有些不知所措,但是这个特性早已经在Swift的官方文档中有过介绍了:

A property wrapper adds a layer of separation between code that manages how a property is stored and the code that defines a property.(属性包装器在管理属性如何存储和定义属性的代码之间添加了一个隔离层)

**也就是说这个隔离层做了什么就是属性包装器的真正作用!**可是它具体做了什么呢?我想通过一个案例来聊一聊,我们都使用过UserDefaults 来存储过数据,通常我会这样使用:

extension UserDefaults {
    enum Keys {
       static let didLogIn = "didLogIn"
   }
    
    static var didLogIn: Bool {
        get {
            return UserDefaults.standard.object(forKey: UserDefaults.Keys.didLogIn) as? Bool ?? false
        }
        set {
            UserDefaults.standard.set(newValue, forKey: UserDefaults.Keys.didLogIn)
        }
    }
}

值得庆幸的是这里只有一个Key: didLogIn 但是往往在项目中,我们可能会用到多个Key来保存不同的数据,不可否认,重复的代码会越来越多,这和Swift简洁优雅的语言特性是不符合的,那么有没有一个更好的方式呢?有,那就是**@PropertyWrapper ,它可以封装属性内部的行为,再加上范型的使用,消除重复的逻辑代码,提高代码的可读性,降低代码量。**具体来说如下:

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

extension UserDefaults {
    @UserDefault(key: UserDefaults.Keys.didLogIn, defaultValue: false)
    static var didLogIn: Bool
}

通过上述代码,我们可以定义一个UserDefault ,用它来封装对于属性的管理,使用的时候直接在属性前添加@UserDefault 即可,非常的具备可读性。那我对**@PropertyWrapper** 做的定义如下:属性包装器将对属性的管理行为做了封装,具体来说get/set/wiiSet/didSet等行为,并提供了复用的机制。

如何使用@PropertyWrapper?

那么接下来我们将聊一聊@PropertyWrapper是如何使用的,如果是已经有基础的朋友,看到这里就可以点个赞👍 然后去看我其他的文章了,如果没有相关基础,那我们接着往下走。我将从Apple属性包装器的文档中的案例来展开:

如何定义?

  • 需要在类型前加上@propertyWrapper,类型可以是结构体、枚举或者类
  • 类型一定要定义一个wrapperValue属性

就只有这两点规则,比如我们实现一个:

**@propertyWrapper
struct EmptyProperty<Value> {
    var wrappedValue: Value
}**

如何初始化?

定义是非常简单的,初始化也很简单,上述例子中**@propertyWrapper**修饰的本身就是一个类型,我们之所以不添加初始化方法,是因为struct帮我们自动生成来初始化方法,实际上我们最好是自己实现初始化方法。

@propertyWrapper
struct SmallNumber {
    private var maximum: Int
    private var number: Int

    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, maximum) }
    }

    init() {
        maximum = 12
        number = 0
    }
    init(wrappedValue: Int) {
        maximum = 12
        number = min(wrappedValue, maximum)
    }
    init(wrappedValue: Int, maximum: Int) {
        self.maximum = maximum
        number = min(wrappedValue, maximum)
    }
}

在上述定义中实现了三种初始化方法,所以我们使用的时候也可以使用三种初始化方法:

struct A {
    @SmallNumber var num1: Int
    
    @SmallNumber var num2 = 13
    @SmallNumber(wrappedValue: 13) var num3
    
    @SmallNumber(wrappedValue: 13, maximum: 15) var num4
    @SmallNumber(maximum: 15) var num5 = 13
}
  • struct A中第一种方式对应定义中的第一种初始化方法:init()
  • struct A中第二种方式和第三种方式等价,都对应定义中的第二种初始化方法:init(wrappedValue: Int)
  • struct A中第四种方式和第五种方式等价,都对于定义中的第三种初始化方法:init(wrappedValue: Int, maximum: Int)

我们能看出,直接对该属性赋值就等于是对属性包装器中的wrappedValue 进行了赋值。

换言之,我们定义的num1属性就是属性包装器中的wrappedValue属性,那么如何获得属性包装器本身呢?很简单使用_num1 前面加上下划线即可:

_num1.wrappedValue 等价于 num1

_num1 就是属性包装器类型 (即为SmallNumber类型)

如何使用投射属性?

除了wrappedValue ,属性包装器还可以通过定义projectedValue暴露出其他功能,即投射任何属性(包括自身),而这个被呈现值需要以 $ 符号来开头,除此之外被呈现值的名称和被包装值是一样的。比如说在上面SmallNumber的例子中投射一个需求:是否在存储之前调整了新值。

@propertyWrapper
struct SmallNumber {
    private var number: Int
		// 被投射值
    private(set) var projectedValue: Bool

    var wrappedValue: Int {
        get { return number }
        set {
            if newValue > 12 {
                number = 12
                projectedValue = true
            } else {
                number = newValue
                projectedValue = false
            }
        }
    }

    init() {
        self.number = 0
        self.projectedValue = false
    }
}
struct SomeStructure {
    @SmallNumber var someNumber: Int
}
var someStructure = SomeStructure()

someStructure.someNumber = 4
print(someStructure.$someNumber)
// 打印 "false"

someStructure.someNumber = 55
print(someStructure.$someNumber)
// 打印 "true"

也就是说我们可以得到如下的对应关系:

_属性名称 = PropertyWrapper类型
(如:_num1 的类型是SmallNumber类型)

$属性名称 = _属性名称.projectedValue
(如:$someNumerber是projectedValue的值)

属性名称 = _属性名称.wrapperedValue
(如:_num1 的类型是SmallNumber类型中的wrapperedValue)

声明时的限制

  • 被修饰的属性不能是lazy、weak、或者unowned的
  • PropertyWrapper 类型本身必须和wrappedValue、projectedValue必须有同样的access control level

@PropertyWrapper的更多实例

虽然我们已经知道了该特性的用法,但是千万不能从字面上理解,比如每一次使用didSet时都采用PropertyWrapper,而是要从具体的使用场景出发,发现更通用的需求,最好可以结合范型,以应对更多的类型场景。以下我拿两个实际场景来举例:

场景1: 懒加载

// 懒加载
@propertyWrapper
public struct LateInitialized<Value> {
    public var wrappedValue: Value {
        get {
            guard let value = storage else {
                fatalError("value is wrong")
            }
            return value
        }
        
        set {
            storage = newValue
        }
    }
    
    public init(_ value: Value) {
        storage = value
    }
    
    private(set) var storage: Value?
    
    public var projectedValue: Self { self }
}

struct MyTypeOne {
    @LateInitialized var text: String
}

场景2: 防御性拷贝

@propertyWrapper
public struct DefensiveCopying<Value: NSCopying> {
    private var storage: Value
    
    public var wrappedValue: Value {
        get { return storage }
        set {
            storage = newValue.copy() as! Value
        }
    }
    
    public init(wrappedValue: Value) {
        storage = wrappedValue.copy() as! Value
    }
}

总结

为什么我会聊这个呢?因为最近在看Swift自定义DSL,而弄懂这个,需要一些前置条件,所以本文就聊了聊关于PropertyWrapper的内容,后续会聊到Result Builder以及Key Path Look Up,以及最后的DSL。

参考

1、Swift官方文档

2、SwiftGG文档

3、WWDC19 Session415 《Modern Swift API Design》