简介
WWDC2019 发布了 SwiftUI 和 Swift5.1,我们看到很多全新带 @ 的属性例如 @State、@Binding、@EnvironmentObject,这些其实都依赖于 Property Wrappers。
动机
Swift 语言为属性提供了一些关键字,比如 layz 它推迟初始化其默认值,直到第一次访问才进行初始化,我们可以自己尝试实现。
struct Too {
// lazy var too = 1
private var _too: Int?
var too: Int {
mutating get {
if let value = _too { return value }
let initialValue = 1
_too = initialValue
return initialValue
}
set {
_too = newValue
}
}
}
Swift 语言让我们能够使用 lazy 很方便的让属性获得推迟初始化的功能,但是把 lazy 构建到语言层面也有一些缺点?;崾褂镅院捅嘁肫鞅涞母丛?,也不灵活。惰性初始化还有很多有意义的变体,但是语言层面不会全部支持。
而且我除了懒加载以外我们可能还需要很多不同属性模式,语言层面也不会全部支持,我自己在每个属性的 set, get 方法里面去实现又太麻烦,所以我们需要一个模板,能让我们快速的实现我们自己的属性关键字,然后放到属性前面。所以 Property Wrappers 就诞生了。
我们现在可以通过 Property Wrappers 来自己实现一个 lazy 关键字。
@propertyWrapper
enum Lazy<Value> {
case uninitialized(() -> Value)
case initialized(Value)
init(wrappedValue: @autoclosure @escaping () -> Value) {
self = .uninitialized(wrappedValue)
}
var wrappedValue: Value {
mutating get {
switch self {
case .uninitialized(let initializer):
let value = initializer()
self = .initialized(value)
return value
case .initialized(let value):
return value
}
}
set {
self = .initialized(newValue)
}
}
}
struct Too {
@Lazy var too = 1
}
属性包装器
属性包装器就是在管理属性如何存储和定义属性的代码之间添加了一个分隔层。一次性定义好属性的管理代码,我们就可以在多个属性上进行复用。
定义一个属性包装器,你需要创建一个定义 wrappedValue 属性的结构体、枚举或者类。
@propertyWrapper
struct Limit {
private var value: Double
private var range: ClosedRange<Double>
init() {
value = 0
self.range = 0...1
}
var wrappedValue: Double {
get { value }
set { value = min(max(range.lowerBound, newValue), range.upperBound) }
}
}
struct Movie {
@Limit var progress: Double
}
var movie = Movie()
movie.progress = 2
print(movie.progress)
// 打印 1.0
movie.progress = -1
print(movie.progress)
// 打印 0.0
movie.progress = 0.5
print(movie.progress)
// 打印 0.5
当你把一个包装器应用到一个属性上时,编译器将合成提供包装器存储空间和通过包装器访问属性的代码。等同于下面的代码。
struct Movie {
private var _progress = Limit()
var progress: Double {
get { return _progress.wrappedValue }
set { _progress.wrappedValue = newValue }
}
}
设置被包装属性的初始值
我们给上面的 Limit 属性包装器再添加两个构造器.
@propertyWrapper
struct Limit {
private var value: Double
private var range: ClosedRange<Double>
var wrappedValue: Double {
get { value }
set { value = min(max(range.lowerBound, newValue), range.upperBound) }
}
init() {
value = 0
range = 0...1
}
init(wrappedValue: Double) {
range = 0...1
value = min(max(range.lowerBound, wrappedValue), range.upperBound)
}
init(wrappedValue: Double, range: ClosedRange<Double>) {
self.range = range
value = min(max(range.lowerBound, wrappedValue), range.upperBound)
}
}
当你把包装器应用于属性且没有设定初始值时,Swift 使用 init() 构造器来设置包装器。举个例子:
struct Movie {
@Limit var progress: Double
}
var movie = Movie()
print(movie.progress)
// 打印 0.0
当你为属性指定初始值时,Swift 使用 init(wrappedValue:) 构造器来设置包装器。举个例子:
struct Movie {
@Limit var progress = 0.6
}
var movie = Movie()
print(movie.progress)
// 打印 0.6
这里当你设置 = 0.6 时, 调用 Limit(wrappedValue: 0.6) 来创建包装 progress 的实例。
当你在自定义特性后面把实参写在括号里时,Swift 使用接受这些实参的构造器来设置包装器。
struct Movie {
@Limit(wrappedValue: 0.6, range: 0...1) var progress
}
var movie = Movie()
print(movie.progress)
// 打印 0.6
这里等于调用了 init(wrappedValue: 0.6, range: 0...1) 来创建实例。
当包含属性包装器实参时,你也可以使用赋值来指定初始值。Swift 将赋值视为 wrappedValue 参数,且使用接受被包含的实参的构造器。举个例子:
struct Movie {
@Limit(range: 0...1) var progress = 0.6
}
var movie = Movie()
print(movie.progress)
// 打印 0.6
这里等于调用了 init(wrappedValue: 0.6, range: 0...1) 来创建实例。
ProjectedValue
除了被包装值,属性包装器可以通过定义被呈现值暴露出其他功能。属性包装器可以返回任何类型的值作为它的被呈现值。除了以货币符号($)开头,被呈现值的名称和被包装值是一样的。
@propertyWrapper
struct SmallNumber {
private var number: Int
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"
写下 someStructure.someNumber 的值为 false。但是,在尝试存储一个较大的数值时,如 55 ,被呈现值变为 true。
@State
来看一看 SwiftUI 中 @State 中的实现 public 的部分只有几个初始化方法和 property wrapper 的标准的 value:
struct State<Value> : DynamicProperty {
init(wrappedValue value: Value)
init(initialValue value: Value)
var wrappedValue: Value { get nonmutating set }
var projectedValue: Binding<Value> { get }
}
可以看到有两个初始化方法, 从文档上面没有看出两者有什么区别, 使用 init(wrappedValue value: Value) 时候, 苹果建议我门不要直接调用初始化方法,而是使用声明属性赋值的方式,例如:
@State private var isPlaying: Bool = false
我们可以通过 dump State 的值,很容易知道它的几个私有变量,继而猜测其内部结构.
struct ContentView: View {
@State var flag = false
init() {
dump(self._flag)
}
var body: some View {
get {
Button("flag = \(flag)" as String, action: { self.flag = true; dump(self._flag) })
}
}
}
我们可以大致猜测一下 @State 结构:
@propertyWrapper
struct MyState<Value>:DynamicProperty{
private var _value:Value
private var _location:StoredLocation<Value>?
init(wrappedValue:Value){
self._value = wrappedValue
self._location = StoredLocation(value: wrappedValue)
}
var wrappedValue:Value{
get{ _location?._value.pointee ?? _value}
nonmutating set {
_location?._value.pointee = newValue
update()
}
}
var projectedValue:Binding<Value>{
Binding<Value>(
get:{self.wrappedValue},
set:{self._location?._value.pointee = $0}
)
}
func update() {
print("重绘视图")
}
}
class StoredLocation<Value>{
let _value = UnsafeMutablePointer<Value>.allocate(capacity: 1)
init(value:Value){
self._value.pointee = value
}
}
这段代码并不能和视图建立依赖。但是我们可以和使用 @State 一样来使用 @MyState,同样支持绑定、修改,除了视图不会自动刷新。
这里你可能会有个疑问为啥单独用了一个 class 来存储 value,不直接就用 var _value 属性.
SwiftUI 里面, 我们会经常在 body 里对 @State 修饰属性的值进行更改, 从而触发界面 View 的刷新.然而在 Struct 中本身是个常量要更改属性属性需要 mutating 关键字修饰,例如:
struct ContentView: View {
var number: Int = 1
var body: some View {
mutating get {
Button {
number += 1
} label: {
Text("Text")
}
}
}
}
但是在 SwiftUI body 使用 mutating 编译报错 Type 'ContentView' does not conform to protocol 'View'
struct ContentView: View {
var number: Int = 1
var body: some View {
mutating get {
Text("ContentView")
}
}
}
View body 没法修改那么只能去修改属性 set 方法使用 nonmutating 告诉编译器不会修改内部的值.例如:
struct ContentView: View {
private var _number: Int = 0
var number: Int {
get {
return _number
}
nonmutating set {
print("存储新值")
}
}
var body: some View {
get {
Button {
number += 1
} label: {
Text("Text")
}
}
}
}
然后再把 _number 换成我们 StoredLocation 里面又一个存储 value 的指针.
以上就是 @State 关于 propertyWrapper 使用.
struct Demo {
@State var number: Int
init(number: Int) {
self.number = number + 1
}
}
上述代码会有编译错误 Variable 'self.number' used before being initialized, 开始你会感觉很奇怪. 但仔细一想便知道了, @State 的声明,会在当前 View 中带来一个自动生成的私有存储属性.
truct Demo {
private var _number: State<Int>
var number : Int {
get {
_number.wrappedValue
}
nonmutating set {
_number.wrappedValue = newValue
}
}
init(number: Int) {
self.number = number
}
}
init 里面调用 self.number 的时候,底层 _number 是没有完成初始化的.
Examples
属性需要用 UserDefaults 进行存储的.
@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
enum GlobalSettings {
@UserDefault(key: "FOO_FEATURE_ENABLED", defaultValue: false)
static var isFooFeatureEnabled: Bool
@UserDefault(key: "BAR_FEATURE_ENABLED", defaultValue: false)
static var isBarFeatureEnabled: Bool
}
属性的存储值限定到某个特定范围内.
@propertyWrapper
struct Clamping<V: Comparable> {
var value: V
let min: V
let max: V
init(wrappedValue: V, min: V, max: V) {
value = wrappedValue
self.min = min
self.max = max
assert(value >= min && value <= max)
}
var wrappedValue: V {
get { return value }
set {
if newValue < min {
value = min
} else if newValue > max {
value = max
} else {
value = newValue
}
}
}
}
struct Color {
@Clamping(min: 0, max: 255) var red: Int = 127
@Clamping(min: 0, max: 255) var green: Int = 127
@Clamping(min: 0, max: 255) var blue: Int = 127
}
组合
属性包装器还可以组合使用,比如把之前 @Lazy 和 @Clamping 给一个属性那这个属性就能获得两种效果.
struct Demo {
@Lazy @Clamping(min: 0, max: 255) var red: Int = 127
// 相当于下面代码
// private var _red: Lazy<Clamping<Int>> = Lazy(wrappedValue: Clamping<Int>(wrappedValue: 127, min: 0, max: 255))
// var red: Int {
// mutating get {
// return _red.wrappedValue.wrappedValue
// }
// set {
// _red.wrappedValue.wrappedValue = newValue
// }
// }
}