SwiftUI有严格数据驱动用于数据传递与修改,通过数据流向对视图更新等操作,理解了数据流向就差不多掌握了SwiftUI的终极奥义!
我看在代码里看到的以@开头的属性包装器其内容实现其实是一个struct结构体,之所以@开头这是编译器的语法糖代码。其主要目的是封装了一些应用于vars的模板行为。说人话就是通过@关键字 包装后的属性就获取到了相关的技能,也就能满足我们的开发工作(有点协议的味道,但不是的哦)。
SwiftUI里共用一个数据源,我们需要通过不同的关键字修饰来实现不同等级的数据访问权限。
MVVM:Model - View - ViewModel 即:数据驱动视图,其主要目的是分离视图(View)和模型(Model)而ViewModel就是定义了一个Observer观察者。ViewModel是连接View和Model的中间件(数据与视图之间的胶水)。ViewModel能够观察到、监听到数据的变化,并对视图对应的内容更新及通知数据变化。
所以我们可以很清晰的梳理出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
《【SwiftUI进阶】理解状态和数据流State、Binding、ObervedObject等》的网友评论(0)