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类型,此时,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()
  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和同步修改。

你可能感兴趣的:(SwiftUI2.0 数据绑定@State,@Binding ,@ObservedObject)