@Published in SwiftUI

3,674 阅读2分钟

前面介绍了 @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。

  1. 先定义属性包装器
@propertyWrapper
struct TwelveOrLess {

}
  1. 每一个属性包装器都需要有一个 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,会执行 wrappedValueset 方法。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,并且它可以是任意类型的。

上面就是关于属性包装器的一些内容,总结下:

  1. 属性包装器也是一个 struct,用来包装 class/struct 的某一个属性。
  2. 包装某一个属性之后,wrappedValue 变成「被包装属性的替身」,所有对「被包装属性」的 get/set 都会调用 wrappedValueget/set
  3. 属性包装器有一个 projectedValue 属性,可以暴露属性包装器本身的属性,使用 $ 修饰即可获取。

@Published 实现

看到 @ 符号大概就能猜到 @Published 本身也是一个属性包装器,它是怎么做到:修饰一个属性之后,当这个属性的值变化就去更新 UI?

前面我们说过 wrappedValue 就是「被包装属性」的替身,那么我们可以在 wappedValuedidSet 函数中去通知 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执行时机和 wappedValuedidSet 执行时机是一样的,我们只要在 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 就是结构体本身,可以直接调用 observeobserve 参数是一个闭包被保存起来,在 didSet 被调用时就会执行闭包的逻辑。

以上就是本次 @Published 的内容了,更多 swiftUI&&Combine 的内容会试着坚持更新✊,如果你愿意给点正向反馈让我更有动力更新那是极好的