开发语言: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
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的响应式编程方式。具体的工作流程如下:
- 继承了ObservableObject协议的类,会自动创建以下变量:
let objectWillChange = PassthroughSubject()
使用@Published标记的变量发生改变后,会使用objectWillChange发出一个事件。
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和同步修改。