本文缘起于一个晚上,看着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》

  • 我正在参与技能社区创作者签约方案招募活动,点击链接报名投稿。