前面介绍了
@State
的使用以及它的局限性:如果被观察者是一个对象,那么在数据改变被之后
@State
无法更新 UI。
具体可以看这篇文章
。正是由于这个原因,swiftUI 引入了
@ObservedObject
和
@Published
来解决这个问题。先看下具体用法。
首先,定一个一个类实现
ObservableObject
protocol
class TestViewModel : ObservableObject {
这个类中,数据改变需要更新 UI 的元素使用 @Published
修饰
class TestViewModel : ObservableObject {
@Published var name:String
var age:Int
init(name:String, age:Int) {
self.name = name
self.age = age
最后,在 View 中创建这个类使用 @ObservedObject
修饰
struct ContentView: View {
//酷似 @State var user = User(name: "jaychen")
@ObservedObject var vm = TestViewModel(name: "jaychen", age: 12)
var body: some View {
VStack{
TextField("input your name", text: $vm.name)
.frame(width: 200)
.textFieldStyle(RoundedBorderTextFieldStyle())
Text("your name is \(vm.name)")
}.padding()
完整代码可以在 gist 找到。
由于 TestViewModel
定义为 class,那么 vm
就可以通过传参的形式在不同的 View 中共享数据。
@Published
的使用方式很简单,但是它是如何做到的一直让我很困惑。在翻阅了一些资料之后,终于看到了一段让人豁然开朗的实现。不过,在解释 @Published
的实现之前,我们得先了解下属性包装器。
Property Wrapper
这里是关于属性包装器的详细中文文档:属性包装器。虽然有文档,但是我还是从我的理解上解释下这个东西。
属性包装器是什么
属性包装器也是一个 struct
,只是这个 struct
需要使用 @propertyWrapper
来修饰,引用文档的代码,TwelveOrLess
现在就是一个属性包装器,即使它什么都没做。
@propertyWrapper
struct TwelveOrLess {
属性包装器有什么用
属性包装器,看字面意思,就是用来包装 class/struct
的某一个属性。
struct SmallRectangle{
@TwelveOrLess var height: Int //height这个属性,被TwelveOrLess这个属性包装器包装了!
上面的代码中 height
这个属性就被 @TwelveOrLess
包装了,包装之后,所有对 height
的修改,都需要先经过 @TwelveOrLess
的审查。在文档的例子中,每次对 height
重新赋值之前,@TwelveOrLess
都会检查新的值是否大于 12。
具体怎么用
根据文档的例子,我尝试把这个例子详细解释下。正如文档中说的 TwelveOrLess
用来保证 height 的值用于小于 12。
先定义属性包装器
@propertyWrapper
struct TwelveOrLess {
每一个属性包装器都需要有一个 wrappedValue
的属性,这个属性可以理解为 「被包装属性」的替身。在文档这个例子中,wrappedValue
就是 height
这个属性的替身。
@propertyWrapper
struct TwelveOrLess {
private var number: Int
init() { self.number = 0 }
var wrappedValue: Int {
get { return number }
set { number = min(newValue, 12) }
代码中,需要注意 set
函数,上面说过「wrappedValue
就是 height
这个属性的替身」,所以当修改 height
的值:s.height = 23
,会执行 wrappedValue
的 set
方法。get
同理。
属性包装器除了 wrappedValue
属性之外,还有一个 projectedValue
的属性。这个属性有什么用?
属性包装器本质也是一个 struct
,它可以有很多个属性。但是你会发现,我们不会去写 let t = TwelveOrLess()
的代码,那么如果我们需要获取属性包装器的其他属性,要怎么获取?
这个时候 projectedValue
的作用就体现出来了:projectedValue
可以暴露属性包装器的属性。
假设有代码
@propertyWrapper
struct LogWrapper {
var wrappedValue: Int {
get { 1 }
set {
print("set new :\(newValue)")
// projectedValue 的类型可以是任意的类型
var projectedValue : String {
get {
return "无论什么都行"
class Student {
@LogWrapper var age: Int
init(age: Int) {
self.age = age
var s = Student(age: 234)
print(s.$age) //输出:无论什么都行
如上面代码,只要使用 $
修饰之后,就可以获取到属性包装器的 projectedValue
,并且它可以是任意类型的。
上面就是关于属性包装器的一些内容,总结下:
属性包装器也是一个 struct
,用来包装 class/struct
的某一个属性。
包装某一个属性之后,wrappedValue
变成「被包装属性的替身」,所有对「被包装属性」的 get/set
都会调用 wrappedValue
的 get/set
。
属性包装器有一个 projectedValue
属性,可以暴露属性包装器本身的属性,使用 $
修饰即可获取。
@Published
实现
看到 @
符号大概就能猜到 @Published
本身也是一个属性包装器,它是怎么做到:修饰一个属性之后,当这个属性的值变化就去更新 UI?
前面我们说过 wrappedValue
就是「被包装属性」的替身,那么我们可以在 wappedValue
的 didSet
函数中去通知 UI。听着很可行,那我们尝试下模拟 swiftUI 的 @Published
实现。
我们希望可以像 swiftUI 那样使用它
class Student {
@Published var age: Int
init(age: Int){
self.age = age
var s = Student(age : 12)
s.age = 13 //这个时候通知更新 UI
那就让我们开始飙车吧:
@propertyWrapper
struct Published<Value> {
var wrappedValue: Value {
didSet {
mockUpdateUI()
我们有了上面的几行代码,这里它确确实实实现了 age
变化之后执行某些逻辑。(这里我们使用 mockUpdateUI
模拟 swiftUI 更新)。
更进一步,combine 中能实现下面的功能
class Student {
@Published var age: Int
student.$age.sink{
print("age is changed: \($0)")
那么,我们的 @Published
要怎么实现这个?想一下,其实 sink
执行时机和 wappedValue
中 didSet
执行时机是一样的,我们只要在 didSet
中去执行 sink
传递过来的闭包就行。
于是,我们的代码可以进一步
@propertyWrapper
struct Published<Value> {
var closure: ((Value) -> Void)?
var projectedValue: Published {
get {
return self
var wrappedValue: Value {
didSet {
if closure != nil {
closure!(self.wrappedValue)
init(wrappedValue: Value) {
self.wrappedValue = wrappedValue
func observe(with closure: @escaping (Value) -> Void) {
closure(wrappedValue)
//////测试代码
class Student {
@Published var age: Int
init(age: Int) {
self.age = age
var s = Student(age: 234)
s.age = 44
s.$age.observe { i in
print("age is changed: \(i)")
注意看 projectedValue
,这里返回了 self
整个结构体实例,所以 s.$age
就是结构体本身,可以直接调用 observe
。observe
参数是一个闭包被保存起来,在 didSet
被调用时就会执行闭包的逻辑。
以上就是本次 @Published
的内容了,更多 swiftUI&&Combine 的内容会试着坚持更新✊,如果你愿意给点正向反馈让我更有动力更新那是极好的