美文网首页
SwiftUI2.0 数据绑定@State,@Binding ,

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

作者: 肆点壹陆 | 来源:发表于2020-03-26 19:08 被阅读0次

    开发语言: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和同步修改。

    相关文章

      网友评论

          本文标题:SwiftUI2.0 数据绑定@State,@Binding ,

          本文链接:https://www.haomeiwen.com/subject/ozfhuhtx.html