·  阅读

前面介绍了 @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,会执行 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,并且它可以是任意类型的。

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

  • 属性包装器也是一个 struct,用来包装 class/struct 的某一个属性。
  • 包装某一个属性之后,wrappedValue 变成「被包装属性的替身」,所有对「被包装属性」的 get/set 都会调用 wrappedValueget/set
  • 属性包装器有一个 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 的内容会试着坚持更新✊,如果你愿意给点正向反馈让我更有动力更新那是极好的