SwiftUI2.0 数据绑定@State,@Binding ,@ObservedObject

开发语言:SwiftUI 2.0
开发环境:Xcode 12.0.1
发布平台:IOS 14

在SwiftUI中,有自己独特的一套数据绑定机制,利用此机制构建数据结构后,一旦数据源发生更新,SwiftUI内部会自动触发画面刷新,保持数据和界面的同步。数据绑定使用以下关键字:

  • @State和@Binding
  • ObservableObject协议,@ObservedObject和@Published,@StateObject(2.0新增)
  • @EnvironmentObject

这些关键字分别有着自己的适用场景,下面分别进行介绍

1、 @State和@Binding

1.1、 @State

假设有以下场景,View中存在一个Button,点击Button会修改Button的文字显示,使用SwiftUI实现此View。

struct SubView:View {
    var content:String

    var body: some View {
        VStack{
            VStack {
                Button(action: {
                    self.content = "changed"
                }) {
                    Text(content)
                }
            }
        }
    }
}

我们期待点击Button时修改content的值,但这样使用编译时会报错,原因是SubView是struct,我们无法在此结构体内修改变量的值。SwiftUI使用@State标记解决此问题,修改后的代码如下:

struct SubView:View {
    @State var content:String

    var body: some View {
        VStack{
            VStack {
                Button(action: {
                    self.content = "changed"
                }) {
                    Text(content)
                }
            }
        }
    }
}

由于使用了@State标记,SwiftUI会自动管理被标记的属性,在属性值修改后,会触发使用此属性的界面更新。

1.2、@Binding

延续以上例子,在新增一个ContentView。

struct ContentView: View {
    var content = "init"
    var body: some View {
        VStack{
            Text(content)
            SubView(content: content)
        }
    }
}
struct SubView:View {
    @State var content:String

    var body: some View {
        VStack{
            VStack {
                Button(action: {
                    self.content = "SubViewTap"
                }) {
                    Text(content)
                }
            }
        }
    }
}

主View中包含一个Text和子View,Text显示的内容由content变量维护,并且传递content至子View,我们期待点击子View中的Button时,主View中Text显示的文字也会改变。
但是运行程序后,发现点击Button后,只有Subview中的文本改变了,原因是因为ContentView和SubView中的content对象不是同一个对象,在点击Button后,只有Subview中的对象的值被修改了,SwiftUI使用@Binding标记解决此问题,修改后的代码如下:

struct ContentView: View {
    @State var content = "init"
    var body: some View {
        VStack{
            Text(content).onTapGesture {
                self.content = "ContentViewTap"
            }
            SubView(content: $content)
        }
    }
}

struct SubView:View {
    @Binding var content:String

    var body: some View {
        VStack{
            VStack {
                Button(action: {
                    self.content = "SubViewTap"
                }) {
                    Text(content)
                }
            }
        }
    }
}


使用@Binding标记子画面中的content属性,并且在构造SubView时,使用$符号将String类型转换为Binding<String>类型,此时,SubView持有的是主View的content的投影属性,无论我们通过点击ContentView还是通过点击SubView来修改content的值,两个View均会同步更新。

2 ObservableObject协议,@ObservedObject和@Published

现在将界面相关的数据封装到Model中,我们期望在点击ContentView或SubView时,记录下当前点击的次数,同时修改文本的显示/隐藏状态,并且在ContentView和SubView中,同步显示这些值。

class Model {
    var clickTimes = 0
    var show = true
}

struct ContentView: View {
    @State var model = Model()
    var body: some View {
        VStack{
            Text(String(self.model.clickTimes)).onTapGesture {
                self.model.clickTimes += 1
                self.model.show.toggle()
            }
            if model.show {
                Text("ContentViewShow")
            }
            SubView(model: $model)
        }
    }
}

struct SubView:View {
    @Binding var model:Model

    var body: some View {
        VStack{
            VStack {
                Button(action: {
                    self.model.clickTimes += 1
                    self.model.show.toggle()
                }) {
                    Text(String(self.model.clickTimes))
                }
                if model.show {
                    Text("SubViewShow")
                }
            }
        }
    }
}

我们使用第一节中的@State和@Binding标记,来同步model,但是实际使用时,不管点击ContentView还是SubView,界面都没有发生改变,原因是因为点击事件里:

self.model.clickTimes += 1
self.model.show.toggle()

我们直接修改了model中的值,但model本身没有发生改变,@State和@Binding只有在其关联的变量本身发生改变后,才会触发相应的刷新功能,所以点击事件修改如下:

//构建一个新的model并赋值给self.model
let newModel = Model()
newModel.clickTimes = self.model.clickTimes + 1
newModel.show = !self.model.show

self.model = newModel

重新编译程序后,界面可以按照我们的要求显示。

但是在真实的开发中,这样写代码实在太反人类了,SwiftUI使用ObservableObject解决此问题,修改代码如下:

class Model:ObservableObject {
    @Published var clickTimes = 0
    @Published var show = true
}

struct ContentView: View {
    @ObservedObject var model = Model()
    var body: some View {
        VStack{
            Text(String(self.model.clickTimes)).onTapGesture {
                self.model.clickTimes += 1
                self.model.show.toggle()
            }
            if model.show {
                Text("ContentViewShow")
            }
            SubView(model: model)
        }
    }
}

struct SubView:View {
    @ObservedObject var model:Model

    var body: some View {
        VStack{
            VStack {
                Button(action: {
                    self.model.clickTimes += 1
                    self.model.show.toggle()
                }) {
                    Text(String(self.model.clickTimes))
                }
                if model.show {
                    Text("SubViewShow")
                }
            }
        }
    }
}

首先,model类继承了ObservableObject协议,同时SubView和ContentView使用@ObservedObject标记了model变量,并且使用@Published标记了model的变量。这些标记和协议底层的实现方式是Combine,一种类似Rx的响应式编程方式。具体的工作流程如下:

  1. 继承了ObservableObject协议的类,会自动创建以下变量:
let objectWillChange = PassthroughSubject<Void, Never>()
  1. 使用@Published标记的变量发生改变后,会使用objectWillChange发出一个事件。

  2. objectWillChange发出事件后,会通知使用@ObservedObject的标记的画面刷新界面。

注意!!@ObservedObject在某些情况下,会产生与我们预料的结果不一样的情况!

在如下代码中,ContentView包含一个Text和一个SubView,单击Text时,会修改Text的文字,而单击SubView,通过model记录了当前点击Button的次数。

class Model:ObservableObject {
    @Published var clickTimes = 0
}

struct ContentView: View {
    @State var show:Bool = false
    var body: some View {
        VStack{
            Text(self.show ? "Show" : "hide").onTapGesture {
                self.show.toggle()
            }
            SubView()
        }
    }
}

struct SubView:View {
    @ObservedObject var model = Model()

    var body: some View {
        VStack{
            VStack {
                Button(action: {
                    self.model.clickTimes += 1
                }) {
                    Text(String(self.model.clickTimes))
                }
            }
        }
    }
}

我们在单击SubView中的Button时,程序似乎按照我们预想的情况运行:

此时我们点击了多次Button,程序也成功的记录了次数,然而在我们点击ContentView中的Text时候,出现问题了。

我们的点击计数被清空了!

这是由于我们在点击Text时候,触发了ContentView内部的重绘,而且这个重绘过程,会重新生成一个SubView,当然也会重新生成SubView中的model,看似合情合理,但是与需求不符合,为了解决这个问题,在SwiftUI2.0的版本中,推出了@StateObject,使用此关键字标记的model,不会随着画面重构和重新生成,它只会被创建一次。这样就解决了以上的问题。

3 @EnvironmentObject

@EnvironmentObject和@ObservedObject类似,@EnvironmentObject为View的全局属性,修改上诉例子中所有的@ObservedObject为@EnvironmentObject。

class Model:ObservableObject {
    @Published var clickTimes = 0
    @Published var show = true
}

struct ContentView: View {
    @EnvironmentObject var model:Model
    var body: some View {
        VStack{
            Text(String(self.model.clickTimes)).onTapGesture {
                self.model.clickTimes += 1
                self.model.show.toggle()
            }
            if model.show {
                Text("ContentViewShow")
            }
            SubView()
        }
    }
}

struct SubView:View {
    @EnvironmentObject var model:Model

    var body: some View {
        VStack{
            VStack {
                Button(action: {
                    self.model.clickTimes += 1
                    self.model.show.toggle()
                }) {
                    Text(String(self.model.clickTimes))
                }
                if model.show {
                    Text("SubViewShow")
                }
            }
        }
    }
}

注意,现在创建 SubView()时,不需要传递model了,因为@EnvironmentObject为全局属性,而使用EnvironmentObject时如下:

ContentView().environmentObject(Model())

此时,ContentView中的自建View,都可以通过@EnvironmentObject标记来获取model和同步修改。

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

推荐阅读更多精彩内容