開(kāi)發(fā)語(yǔ)言:SwiftUI 2.0
開(kāi)發(fā)環(huán)境:Xcode 12.0.1
發(fā)布平臺(tái):IOS 14
在SwiftUI中替蛉,有自己獨(dú)特的一套數(shù)據(jù)綁定機(jī)制,利用此機(jī)制構(gòu)建數(shù)據(jù)結(jié)構(gòu)后,一旦數(shù)據(jù)源發(fā)生更新躲查,SwiftUI內(nèi)部會(huì)自動(dòng)觸發(fā)畫(huà)面刷新它浅,保持?jǐn)?shù)據(jù)和界面的同步。數(shù)據(jù)綁定使用以下關(guān)鍵字:
- @State和@Binding
- ObservableObject協(xié)議镣煮,
@ObservedObject和@Published罚缕,@StateObject(2.0新增) - @EnvironmentObject
這些關(guān)鍵字分別有著自己的適用場(chǎng)景,下面分別進(jìn)行介紹
1怎静、 @State和@Binding
1.1邮弹、 @State
假設(shè)有以下場(chǎng)景,View中存在一個(gè)Button蚓聘,點(diǎn)擊Button會(huì)修改Button的文字顯示腌乡,使用SwiftUI實(shí)現(xiàn)此View。
struct SubView:View {
var content:String
var body: some View {
VStack{
VStack {
Button(action: {
self.content = "changed"
}) {
Text(content)
}
}
}
}
}
我們期待點(diǎn)擊Button時(shí)修改content的值夜牡,但這樣使用編譯時(shí)會(huì)報(bào)錯(cuò)与纽,原因是SubView是struct,我們無(wú)法在此結(jié)構(gòu)體內(nèi)修改變量的值塘装。SwiftUI使用@State標(biāo)記解決此問(wèn)題急迂,修改后的代碼如下:
struct SubView:View {
@State var content:String
var body: some View {
VStack{
VStack {
Button(action: {
self.content = "changed"
}) {
Text(content)
}
}
}
}
}
由于使用了@State標(biāo)記,SwiftUI會(huì)自動(dòng)管理被標(biāo)記的屬性蹦肴,在屬性值修改后僚碎,會(huì)觸發(fā)使用此屬性的界面更新。
1.2阴幌、@Binding
延續(xù)以上例子勺阐,在新增一個(gè)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中包含一個(gè)Text和子View矛双,Text顯示的內(nèi)容由content變量維護(hù)渊抽,并且傳遞content至子View,我們期待點(diǎn)擊子View中的Button時(shí)议忽,主View中Text顯示的文字也會(huì)改變懒闷。
但是運(yùn)行程序后,發(fā)現(xiàn)點(diǎn)擊Button后栈幸,只有Subview中的文本改變了愤估,原因是因?yàn)镃ontentView和SubView中的content對(duì)象不是同一個(gè)對(duì)象,在點(diǎn)擊Button后侦镇,只有Subview中的對(duì)象的值被修改了灵疮,SwiftUI使用@Binding標(biāo)記解決此問(wèn)題,修改后的代碼如下:
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標(biāo)記子畫(huà)面中的content屬性壳繁,并且在構(gòu)造SubView時(shí)震捣,使用$符號(hào)將String類(lèi)型轉(zhuǎn)換為Binding<String>類(lèi)型荔棉,此時(shí),SubView持有的是主View的content的投影屬性蒿赢,無(wú)論我們通過(guò)點(diǎn)擊ContentView還是通過(guò)點(diǎn)擊SubView來(lái)修改content的值润樱,兩個(gè)View均會(huì)同步更新。
2 ObservableObject協(xié)議羡棵,@ObservedObject和@Published
現(xiàn)在將界面相關(guān)的數(shù)據(jù)封裝到Model中壹若,我們期望在點(diǎn)擊ContentView或SubView時(shí),記錄下當(dāng)前點(diǎn)擊的次數(shù)皂冰,同時(shí)修改文本的顯示/隱藏狀態(tài)店展,并且在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")
}
}
}
}
}
我們使用第一節(jié)中的@State和@Binding標(biāo)記赂蕴,來(lái)同步model,但是實(shí)際使用時(shí)舶胀,不管點(diǎn)擊ContentView還是SubView概说,界面都沒(méi)有發(fā)生改變,原因是因?yàn)辄c(diǎn)擊事件里:
self.model.clickTimes += 1
self.model.show.toggle()
我們直接修改了model中的值嚣伐,但model本身沒(méi)有發(fā)生改變糖赔,@State和@Binding只有在其關(guān)聯(lián)的變量本身發(fā)生改變后,才會(huì)觸發(fā)相應(yīng)的刷新功能轩端,所以點(diǎn)擊事件修改如下:
//構(gòu)建一個(gè)新的model并賦值給self.model
let newModel = Model()
newModel.clickTimes = self.model.clickTimes + 1
newModel.show = !self.model.show
self.model = newModel
重新編譯程序后放典,界面可以按照我們的要求顯示。
但是在真實(shí)的開(kāi)發(fā)中船万,這樣寫(xiě)代碼實(shí)在太反人類(lèi)了刻撒,SwiftUI使用ObservableObject解決此問(wèn)題骨田,修改代碼如下:
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類(lèi)繼承了ObservableObject協(xié)議,同時(shí)SubView和ContentView使用@ObservedObject標(biāo)記了model變量态贤,并且使用@Published標(biāo)記了model的變量舱呻。這些標(biāo)記和協(xié)議底層的實(shí)現(xiàn)方式是Combine,一種類(lèi)似Rx的響應(yīng)式編程方式悠汽。具體的工作流程如下:
- 繼承了ObservableObject協(xié)議的類(lèi)箱吕,會(huì)自動(dòng)創(chuàng)建以下變量:
let objectWillChange = PassthroughSubject<Void, Never>()
使用@Published標(biāo)記的變量發(fā)生改變后,會(huì)使用objectWillChange發(fā)出一個(gè)事件柿冲。
objectWillChange發(fā)出事件后茬高,會(huì)通知使用@ObservedObject的標(biāo)記的畫(huà)面刷新界面。
注意<俪怎栽!@ObservedObject在某些情況下丽猬,會(huì)產(chǎn)生與我們預(yù)料的結(jié)果不一樣的情況!
在如下代碼中熏瞄,ContentView包含一個(gè)Text和一個(gè)SubView脚祟,單擊Text時(shí),會(huì)修改Text的文字强饮,而單擊SubView由桌,通過(guò)model記錄了當(dāng)前點(diǎn)擊Button的次數(shù)。
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))
}
}
}
}
}
我們?cè)趩螕鬝ubView中的Button時(shí)邮丰,程序似乎按照我們預(yù)想的情況運(yùn)行:
此時(shí)我們點(diǎn)擊了多次Button行您,程序也成功的記錄了次數(shù),然而在我們點(diǎn)擊ContentView中的Text時(shí)候剪廉,出現(xiàn)問(wèn)題了邑雅。
我們的點(diǎn)擊計(jì)數(shù)被清空了!
這是由于我們?cè)邳c(diǎn)擊Text時(shí)候妈经,觸發(fā)了ContentView內(nèi)部的重繪淮野,而且這個(gè)重繪過(guò)程,會(huì)重新生成一個(gè)SubView吹泡,當(dāng)然也會(huì)重新生成SubView中的model骤星,看似合情合理,但是與需求不符合爆哑,為了解決這個(gè)問(wèn)題洞难,在SwiftUI2.0的版本中,推出了@StateObject揭朝,使用此關(guān)鍵字標(biāo)記的model队贱,不會(huì)隨著畫(huà)面重構(gòu)和重新生成,它只會(huì)被創(chuàng)建一次潭袱。這樣就解決了以上的問(wèn)題柱嫌。
3 @EnvironmentObject
@EnvironmentObject和@ObservedObject類(lèi)似,@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")
}
}
}
}
}
注意,現(xiàn)在創(chuàng)建 SubView()時(shí)彤悔,不需要傳遞model了嘉抓,因?yàn)锧EnvironmentObject為全局屬性,而使用EnvironmentObject時(shí)如下:
ContentView().environmentObject(Model())
此時(shí)晕窑,ContentView中的自建View抑片,都可以通過(guò)@EnvironmentObject標(biāo)記來(lái)獲取model和同步修改。