75142913在线留言
【SwiftUI进阶】理解状态和数据流State、Binding、ObervedObject等_IOS开发_网络人

【SwiftUI进阶】理解状态和数据流State、Binding、ObervedObject等

Kwok 发表于:2021-03-30 10:30:11 点击:222 评论: 0

SwiftUI有严格数据驱动用于数据传递与修改,通过数据流向对视图更新等操作,理解了数据流向就差不多掌握了SwiftUI的终极奥义!

我看在代码里看到的以@开头的属性包装器其内容实现其实是一个struct结构体,之所以@开头这是编译器的语法糖代码。其主要目的是封装了一些应用于vars的模板行为。说人话就是通过@关键字 包装后的属性就获取到了相关的技能,也就能满足我们的开发工作(有点协议的味道,但不是的哦)。

一、数据处理的基本原则

SwiftUI里共用一个数据源,我们需要通过不同的关键字修饰来实现不同等级的数据访问权限。

MVVM:Model - View - ViewModel 即:数据驱动视图,其主要目的是分离视图(View)和模型(Model)而ViewModel就是定义了一个Observer观察者。ViewModel是连接View和Model的中间件(数据与视图之间的胶水)。ViewModel能够观察到监听到数据的变化,并对视图对应的内容更新及通知数据变化

所以我们可以很清晰的梳理出SwiftUI各自的分工。 

  • View层:视图展示、图像、颜色、按钮、文字等。View层是可以持有ViewModel的(虽然不建议但视图里也可以写逻辑运算及方法)。
  • ViewModel层:视图适配器。方法、属性与View元素显示状态对应。一般情况下属性建议是readOnly的(使用专门的方法对数据操作),ViewModel层是可以持有Model的(struct与class写到一起)。
  • Model层:数据模型与持久化抽象模型。一般我们都是从服务器Get数据(JSON)。

SwiftUI进阶理解状态和数据流StateBindingObervedObject等

二、SwiftUI中的数据流向控制

SwiftUI中的界面是严格数据驱动的:运行时界面的修改,只能通过修改数据来间接完成,而不是直接对界面进行修改操作。我们可以通过设置全局变量与数据绑定来解决这一系列的问题。SwiftUI中有多种解决方案。

1、Property 内部可传值属性

在View结构体里定义的var、let属性,主要用于父视图向子视图传递值或者定义私有值。

//父视图
struct ContentView: View {
    var body: some View {
        subView(say: "哈喽~")
    }
}
//子视图
struct subView:View {
    let say:String //Property
    var testSay = ""
    var body: some View{
        VStack{
            /*
             Button("尝试修改say") {
              testSay = "哈个喽~"//不能改变其值:'self'是不可变的
             //Cannot assign to property: 'self' is immutable
             }
             */
            Text(say)//当say改变时,会重新计算body更新视图。
        }
    }
}

就算testSay使用的var变量修饰属性,在body方法里也不能修改,因为修改属性会创建新的结构体。如果让body里可以修改值则需要使用本文下面的修饰方法。

 

2、@State 可变属性(可观察属性)

基础里学到过Struct结构体里的属性不能直接修改(上面的也是),需要加上mutating关键字,当把@State放置到属性前,该属性实际上会被放到Struct的外部存储起来(内存的堆里),这意味着SwiftUI能够随时销毁和重建Struct而不会丢失属性的值。

@State 就是 Source of truth。是一个“Property Wrapper”,即属性包装。当一个属性使用 @State 关键词时,Swift 会为这个属性添加一些额外的操作,比如 Bool 类型可以获得 toggle() 方法,用于在 true/false 之间切换。

@State 包装的属性通常是设置成私有的(private,不让外部使用。如果想让外部使用,则应该使用@ObservedObject和@EnvironmentObject,他们能够使外部修改属性后,状态能够得到改变。 

@State 修饰的属性,只要属性改变,SwiftUI 内部会自动的重新计算 View的body部分,构建出View Tree,由于 View 都是结构体,SwiftUI 每次构建这个 View Tree 都极快,这使得性能有很强的保障。

@State 修饰的属性只能在当前 View 的 body 内修改,所以它的使用场景是只影响当前 View 内部的变化的操作(界面更新)。

struct ContentView: View {
    @State private var changed = false //状态监听属性
    var body: some View {
        VStack{
            Button("我要成长") {
                changed.toggle()//修改状态
            }
            .padding()
            if changed{
                Text("我变成了一个油腻大叔")
            }else{
                Text("我还是曾经的那个少年")
            }
        }
    }
}

@State只适合于struct,当然这个struct可以是常用的Double, Int, String等类型,也可以是自己定义的类型。但@State,不适合于class。struct是实例类型,class是引用类型,导致了他们俩在处理时有所不同。

当修改的State属性值没有在body中(上面代码里如果不判断changed则视图不会更新)使用或者修改后的State属性值和上一次相同,并不会触发视图的更新(会检测State属性被使用和检测值变更来决定要不要更新视图)。

视图经常会被系统重建,所以需要给属性赋一个默认的值。若属性被标记为 @State,系统会使用储存的变量的值,而不是每次都使用初始化的值。

同时,为了保证运行效率,系统会比较值改变导致哪些视图需要被重新渲染,最后只重新渲染那些需要更新的视图。 

只要视图还在屏幕上值就不会被销毁,所以可以把 @State包装后的值理解为与视图共生死。

@State包装后的值也可以使用初始化,虽然用得特别少,但是有些情况也些会用到。下面是初始代码:

@State private var myVar:Int//这里没有赋值
//下面开始初始化操作
int(){
   _myVar = .init(initialValue:5) //变量以下划线开始且使用.init方法并指定值类型
}

3、@StateObject 可观察对象节能版

上面的@State是用于修饰struce属性的,而这里的@StateObject与下文将要介绍的@ObservedObject功能类似,通过名字理解为带有Object功能的State属性包装器,上面有说到@State会随着视图的消失而销毁掉,@StateObject也是同样的,所以它们的区别是:@StateObject会随着 View 的创建后保持数据直到视图被销毁(复用)而@ObservedObject将随着视图的调用会多次创建(浪费资源)。而 @StateObject 保证对象只会被创建一次(节约资源)。因此,如果是在 View 里自行创建的 ObservableObject model 对象,大概率来说使用 @StateObject 会是更正确的选择。@StateObject 基本上来说就是一个针对 class 的 @State 升级版。关于用法参考下面的@ObservedObject。

所以我们在使用的时候需要理解它们的使用区别为:

a.在开发中你永远不要使用@ObservedObject var myObserved = ObservedObject() 因为这是不是真实的数据源,而在真实数据源的引用。正确的使用方法为:@ObservedObject var myObserved:ObservedObject 这和@StateObject有明显的区别。

b.如果想临时使用数据源的时候我们就需要@StateObject var myObserved = ObservedObject() 直接创建的真实数据源,所以这2者的区别就是等号后面的事情。

c.

 

4、@ObservedObject 可观察对象

一般情况下数据来源本地或者远程API,这些数据默认是与 SwiftUI 没有依赖关系的,我们需要建立依赖关系就要使用到@ObservedObject和@Published。@ObservedObject允许外部进行访问和修改。

@ObservedObject告诉SwiftUI,这个对象是可以被观察的,里面含有被@Published包装了的属性。

@ObservedObject包装的对象,必须遵循ObservableObject协议。也就是说必须是class对象,不能是struct(结构体类型请使用@State)。

@Published与@ObservedObject配套使用的,允许我们创建出能够被自动观察的对象属性,SwiftUI会自动监视这个属性,一旦发生了改变,会自动修改与该属性绑定的界面。

a.使用前首先需要遵循ObservableObject属性

class WebSite {var myWeb = [String]()}//未遵守ObservableObject不能使用@Published

b.包装属性发布@Published

class WebSite: ObservableObject { @Published var myWeb = [String]() }//符合协议,可发布

c、视图使用@ObservedObject

struct ContentView: View {
//@ObservedObject修饰ObservableObject(单词不一样啊啊吼吼)
  @ObservedObject var myWeb = WebSite() 
  var body: some View {
      //视图内容
  }
}

这样就完成了。@Published包装会自动添加willSet方法监视属性的改变。一旦修饰的属性发送了变化,会自动触发 ObservableObject 的objectWillChange 的 send方法刷新页面,SwiftUI 已经默认帮我实现好了,但也可以自己手动触发这个行为。

 ObservableObject 是一个协议,必须要类去实现该协议。ObservableObject 适用于多个 UI 之间的同步数据。

@StateObject VS @ObservedObject 

对于 View 自己创建的 ObservableObject 状态对象来说,极大概率你可能需要使用新的 @StateObject 来让它的存储和生命周期更合理。

而对于那些从外界接受 ObservableObject 的 View,究竟是使用 @ObservedObject 还是 @StateObject,则需要根据情况和需要确定。像是那些存在于 NavigationLink 的 destination 中的 View,由于 SwiftUI 对它们的构建时机并没有做 lazy 处理,在处理它们时,需要格外小心。

 

5、@Binding 属性绑定

@Binding也是非常重要的一个包装,声明一个属性是从外部获取的,并且与外部是共享的。与Property功能类似,用于父视图向子视图传递值。但功能更强大,@Binding 主要有两个作用:

a.在不持有数据源的情况下,可以让子视图任意读取数据(传值)。

b.在实际开发中我们常配合@State使用,可以子视图里修改上层数据,相当于外部传过来的引用类型(指针传递)。并且内外部修改Binding属性会触发父视图@State改变重新计算body让SwiftUI的监视生效并更新视图。可以实现反向数据流的功能(双向绑定)。 

//父视图
struct ContentView: View {
    @State var testSay:String? //定义可选值
    var body: some View {
        subView(say: "哈喽~", testSay: $testSay) //binding参数向下传指针时前面加个$
    }
}
//子视图
struct subView:View {
    let say:String //Property
    @Binding var testSay:String? //与外部绑定
    var body: some View{
        VStack{
             Button("修改testSay") {
              testSay = "哈个喽~"//在内部修改外部的值
             }
            if let wantSay = testSay{
                Text(wantSay) //修改成功后显示这个
            }else{
                Text(say) //当say改变时,会重新计算body更新视图。
            }
        }
    }
}

@Binding 除了@State(可观察属性)还支持 @ObservedObject(可观察对象), @EnvironmentObject(环境对象)和@StateObject(可观察对象节能版)的引用 

@Binding 为子视图提供可读/写 Source of truth 的方法

如果要将子视图打包成一个新的自定义视图,同时又依赖了父视图的 @State 的属性,那么这种情况不应该在子视图里使用 @State 来标记属性。因为使用 @State 会导致子视图中的属性成为新的 Source of truth。子视图的渲染依赖父视图的属性,所以它不应该有自己的 Source of truth。它只需要获取父视图的这个属性,然后修改它。这种情况,应该要在子视图中使用 @Binding。

 

6、@EnvironmentObject 环境对象

 @EnvironmentObject 主要是为了解决在整个应用程序中共享对象,是一种注入式的数据传递模式。如果您有一个(只能传递1个数据)要在整个应用程序中使用的数据模型对象,但又不想将其传递给层次结构的多个层次,则可以使用view修饰符将该对象放入环境中:environmentObject(_:)。如果要传递一些设置、配置信息等。类似于PHP里的超级全局变量。

import SwiftUI
//只能使用class因为是从ObservableObject类继承而来
class Student: ObservableObject {
    @Published var score = 95 //发布数据:成绩99
}

struct ContentView: View {
    @EnvironmentObject var student: Student//声明全局的数据,不需要初始化
      var body: some View {
        Text("成绩:" + String(student.score))//使用环境对象里的值
    }
    
}
//预览界面
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().environmentObject(Student())//加入环境对象数据
    }
}

 通过.environmentObject()方法在View对象里向下传导全局数据,所有可访问的视图共享同一个环境对象并且可以监听变更等。

@EnvironmentObject查找获取到对应的对象。当组件树有多个组件使用environmentObject方法注入同类型的对象时,获取时会查找最近的父组件的对象。

7、@Environment 系统环境数据

@Environment与@EnvironmentObject作用是不同的,@Environment是从不同硬件设置及软件环境中取出预定义的值(系统内置属性),比如获得当前是暗黑模式还是正常模式,屏幕的大小等等。

@Environment(.horizontalSizeClass) var horizontalSizeClass
@Environment(.managedObjectContext) var managedObjectContext

环境变量有很多可用值,我们可以通过官方文档查看到所有可用的EnvironmentValue

8、@GestureState

@GestureState能够让我们传递手势的状态,虽然使用@State也能实现,但@GestureState能让手势结束后我们回到最初的状态(设置属性为zero意味着手势结束后,会回到最初的值)。

三、数据流怎么用?

1、当前视图

a.只读取使用:Property

b.读、写、监听、更新视图使用:@State

2、父视图 -> 子视图向下传递

a.只读使用:Property

b.读、写使用:@Binding(可传递多种数据,看上面)

3、父视图 -> 子视图跨层级(多视图)向下传递或者整个APP

使用@EnvironmentObject 配合.environmentObject()方法向下传递全局数据

本文将会根据日后开发经验及新版本SwiftUI更新而更新~ 

官方文档:https://developer.apple.com/documentation/swiftui/state-and-data-flow

最后附上WWDC2019官方视频解释什么是数据流:https://developer.apple.com/videos/play/wwdc2019/226/

四、更多好用的属性包装器

虽然本文是讲数据流的,但大多都是以@符合开始的属性包装器,所以做为本文的扩展知识点,这里再啰嗦一些超好用的新增属性包装器。

1、@ScaledMetric var fontSize:CGFloat = 28 通过这个ScaledMetric包装后的字体可以像.LargeTitle一样随着用户在系统设置里调整了字体而放大。

struct ContentView: View {
    @ScaledMetric var mySize:CGFloat = 28 //随用户设置联动缩放
    var body: some View {
        VStack{
            Image(systemName: "message")
                .resizable()
                .frame(width: mySize, height: mySize)//通过框架让图标也可以联动缩放
            Text("55mx.com")
                .font(.system(size: mySize))//字体联动缩放
        }
    }
}

2、@SceneStorage("识别用的key") var myValue = 1 关闭app后再启动会记住myValue被修改后的值,按场景保持值。

需要自动恢复值的状态时,可以使用SceneStorage。 SceneStorage的工作方式与State非常相似,不同之处在于,如果先前保存了初始值,则系统会恢复其初始值,并且该值会与同一场景中的其他SceneStorage变量共享。

系统代表您管理SceneStorage的保存和还原。支持SceneStorage的基础数据不可用,因此您必须通过SceneStorage属性包装器对其进行访问。该系统无法保证何时以及多长时间保存一次数据。 每个场景都有自己的SceneStorage概念,因此不会在场景之间共享数据。

enum Tab:String {
    case first
    case second
}
//通过使用SceneStorage 记住用户退出时打开的是哪个Table
struct ContentView: View {
    @SceneStorage("selectedTab") var selectedIndex:Tab = Tab.first;
    var body: some View{
        TabView(selection:$selectedIndex){
                Text("第一个页面").navigationTitle("首页").tabItem {
                Image(systemName: "message")
            }.tag(Tab.first)
                 Text("第二个页面").navigationTitle("子页").tabItem {
                Image(systemName: "person.2")
            }.tag(Tab.second)
        }
    }
}

确保与SceneStorage一起使用的数据是轻量级的。不应将大型数据(例如模型数据)存储在SceneStorage中,因为这可能会导致性能下降。 如果场景被明确销毁(例如,切换快照在iPadOS上被销毁或在macOS上窗口被关闭),则数据也将被销毁。不要将SceneStorage与敏感数据一起使用。

新的SceneStorage属性在具有多窗口支持的应用程序(通常基于iPadOS和macOS构建)的状态恢复中非常方便。 

3、@AppStorage 属性包装器用于读取和写入值到UserDefaults 。 每次AppStorage属性包装器的值更改时,SwiftUI视图都会无效并重新绘制。

它的行为与@State属性包装器相同,不同之处在于,它用于以方便的方式在UserDefaults键和SwiftUI视图之间进行通信。

@AppStorage("name") var myValue: String = "网络人 55mx.com"

上面的代码如果使用SwiftUI 1.0来写如下:

@State var myValue: String = "网络人 55mx.com"{
    get {
        UserDefaults.standard.string(forKey: "name")
    }
    set {
        UserDefaults.standard.set(newValue, forKey: "name")
    }
}

可以把appStorage理解为是一个快速使用的UserDefaults。当然也可以直接使用不同的init来直接读取使用UserDefaults里的值:

@AppStorage("name", store: UserDefaults(...))
除非注明,网络人的文章均为原创,转载请以链接形式标明本文地址:https://www.55mx.com/post/114
标签:数据流MVVMKwok最后编辑于:2021-06-25 11:25:22
0
感谢打赏!

《【SwiftUI进阶】理解状态和数据流State、Binding、ObervedObject等》的网友评论(0)

本站推荐阅读

热门点击文章