SwiftUI 探索 - 状态和数据流

SwiftUIiOS13新出的声明式UI框架,将会完全改变以前命令式操作UI的开发方式。此文章主要介绍SwiftUI中状态管理的方式。

可变状态

@State

ReactFlutter中的State类似,只不过ReactFlutter中需要显式调用setState方法。在SwiftUI 中直接修改State属性值,就触发视图更新。

因为State是使用了@propertyDelegate修饰的属性值,其内部实现应该是在状态值set方法中进行变更视图的操作。

class Model: BindableObject {
    var didChange = PassthroughSubject<Model, Never>()
    var count: Int = 0 {
        didSet {
            didChange.send(self)// 调用didChange触发变更操作
        }
    }
}
struct ContentView: View {
   @State private var text: String = "a"// 使用@State修饰
   @State private var model = Model()// 使用@State修饰
    var body: some View {
        VStack {
          Text(text)
          Text(model.count)
          Button(action: {
              self.text = "b"// 修改text会更新视图
              self.count += 1
            }) {
              Text("update-text")
          }
        }
    }
}
  • 如果想使用class类型属性作为State属性,类对象需要实现BindableObject协议。当调用didChangesend方法时,会通知关联的View更新视图。didChangePublisher(新出的Combine异步事件处理框架,类似RxSwift)类型,调用send时会发送一个新的值给订阅者。
  • 当修改的State属性值没有在body中使用或者修改后的State属性值和上一次相同,并不会触发重新计算body

State属性修改时,会检测State属性被使用和检测值变更来决定要不要更新视图和触发body方法。

  • State属性用class类型。在触发body重新计算前会检查State值有没有改变,当修改类对象属性时,因为类对象指针并没有改变,所以并不会触发视图更新。如果想触发视图变更,可以在修改State时生成新的对象(这种方式不太好)或者使用BindableObject

属性

Property

React中的Props类似,用于父视图向子视图传递值。

struct PropertyView: View {
    let text: String// 当text改变时,会重新计算`body`。
    var body: some View {
        Text(text)
    }
}
struct ContentView: View {
    var body: some View {
        PropertyView(text: "a")
    }
}
  • 使用let变量。使用var变量修饰属性,在body方法里也不能修改,因为修改属性会创建新的结构体。

@Binding

Property功能类似,用于父视图向子视图传递值。只不过Binding属性可以修改,修改Binding属性会触发父视图State改变重新计算body。可以实现反向数据流的功能,有点类似MVVM的双向绑定。

struct BindingView : View {
    @Binding var text: String // 使用@Binding修饰
    var body: some View {
        VStack {
            Button(action: {
                self.text = "b"
            }) {
                Text("update-text")
            }
        }
    }
}
struct ContentView : View {
    @State private var text: String = "a" // State
    var body: some View {
        VStack {
            BindingView(text: $text)// State变量使用$获取Binding
            Text(text)
        }
    }
}

@ObjectBinding

@ObjectBinding似乎和State相似,暂时不太清楚使用上有什么区别。@State替换@ObjectBinding使用没有问题,@Binding替换@ObjectBinding使用也没有问题。

class Model: BindableObject {
    var didChange = PassthroughSubject<Model, Never>()
    var count: Int = 0 {
        didSet {
            didChange.send(self)
        }
    }
}
struct ChildView: View {
//    @Binding var model: Model
//    @ObjectBinding var model: Model
    var body: some View {
        VStack {
            Text("count2-\(model.count)")
            Button(action: {
                self.model.count += 1
            }) {
                Text("update")
            }
        }
    }
}

struct ContentView : View {
//    @State private var model = Model()
//    @ObjectBinding private var model = Model()
    var body: some View {
        VStack {
            ChildView(model: model)
            Text("count1-\(model.count)")
        }
    }
}

上面StateObjectBindingBinding注释的地方任意使用结果都一样,视图能正确更新。

@EnvironmentObject

通过Property或者Binding的方式,我们只能显式的通过组件树逐层传递。

显式逐层传递的缺点

  • 当组件树复杂的时候特别繁琐,修改起来也很麻烦。
  • 有些属性在视图树中间的层级不会使用到,只有底层会使用。会增加中间层级视图的复杂度。也可以避免中间的层级重复计算body触发视图更新。

为了避免层层传递属性,可以使用Environment变量。Environment属性可以在任意子视图获取并使用。和React中的Context很相似。

struct EnvironmentView1: View {
    var body: some View {
        return VStack {
            EnvironmentView2()
            EnvironmentView3()
        }
    }
}
struct EnvironmentView2: View {
    @EnvironmentObject var model: Model// 使用@EnvironmentObject修饰属性
    var body: some View {
       Button(action: {
                self.model.change()
            }) {
                Text("update-Environment")
            }
    }
}
struct EnvironmentView3: View {
    @EnvironmentObject var model: Model// EnvironmentObject
    var body: some View {
        Text(model.text)
    }
}
struct ContentView: View {
    var body: some View {
        //EnvironmentObject需要使用environmentObject方法注入到组件树中
        EnvironmentView1().environmentObject(Model())
    }
}
  • 通过environmentObject方法注入对象到组件树中,子组件树中共享同一个对象并且可以监听变更。
  • @EnvironmentObject查找如何能获取到对应的对象,大概是根据属性的类型进行查找,所以多个属性只要类型相同,就能取到同样的对象。当组件树有多个组件使用environmentObject方法注入同类型的对象时,获取时会查找最近的父组件的对象。

目前好像没有方式实现根据不同的key来注入多个对象并获取。

数据流

父视图 -> 子视图向下传递

  • 不需要修改使用Property
  • 需要修改使用@Binding

父视图 -> 子视图跨层级向下传递

  • Environment

全局状态层管理

  • 应该是结合Combine框架根据模块功能领域分层进行管理。

视图更新流程

  • 修改State触发视图更新,检测State是否被使用以及值是否被改变。
  • 重新计算body生成新的视图树,会重新创建所有子视图的View结构体。
  • 遍历所有子视图,判断View结构体与更新前是否一致。当不一致时,触发子视图更新,调用子视图body

Tips

关于 State

class Model: BindableObject {
    var didChange = PassthroughSubject<Model, Never>()
    var count: Int = 0 {
        didSet {
            didChange.send(self)
        }
    }
    init() {
        print("Model-init-\(count)")// 这里count始终为0
    }
}
struct Struct {
    private(set) var count = 0
    init() {
        print("Struct-\(count)")// 这里count始终为0
    }
    mutating func update() {
        print("update-\(count)")
        count += 1
    }
}
struct ChildView: View {
    @State private var model2 = Struct()
    @State private var model = Model2()
    @State private var count = 0
    var body: some View {
        return VStack {
            Text("\(model.count)")
            Text("\(model2.count)")
            Text("\(count)")
            Button(action: {// 修改 State
                self.model.count += 1
                self.count += 1
                self.model2.update()
            }) {
                Text("update")
            }
            }
        }
}
struct ContentView: View {
    @State private var count = 0
    var body: some View {
        return VStack {
            ChildView()
            Button(action: {
                self.count += 1
            }) {
                Text("update")
            }
            Text("\(count)")
        }
    }
}
  • ContentView更新时,会重新创建ChildView结构体。
  • ChildView中的State都会重新创建,StructModel初始化方法中,count一直为0,即使ContentViewState曾经修改过。但是下一次修改State值时,State会使用之前的值做运算。

不太清楚这里是如何处理的,State虽然重新初始化了一次,似乎还是使用的之前的State

  1. 例如当点击Button时,会修改ChildViewmodel, model2count+=1,当前count=1。
  2. ChildView重新创建时,modelmodel2初始化方法中,count=0。
  3. 当下一次点击Button修改count值时,count会在1的基础上+1,之后count=2。

性能

  • 当视图发生变更时,由于body会经常重新计算,所以应该尽量避免在body中进行重复和耗时计算。
  • 视图变更时,视图组件View结构体会重新创建,所以应该避免在init方法中进行重复和耗时计算。(包括属性的重新生成)
  • 根据上面 State的特性,当State属性为结构体或类时,应避免在init方法中访问或修改属性。因为当State修改过后,在init方法中获取到的值不是正确的,修改值也会生效。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350