|
|
|
|
公众号矩阵

详解SwiftUI数据流是怎么在View间传递的

作为一个声明式的UI框架,SwiftUI帮我们处理了几乎所有关于UI和数据之间的交互,这使我们不再需要关注数据变化时刷新UI和用户交互以后更新数据的逻辑。

作者:iOS开发栈来源:掘金|2021-02-06 11:57

作为一个声明式的UI框架,SwiftUI帮我们处理了几乎所有关于UI和数据之间的交互,这使我们不再需要关注数据变化时刷新UI和用户交互以后更新数据的逻辑。

为了实现数据和UI的绑定,我们需要利用Swift的一些关属性包装器来向SwiftUI描述它们之间的关系,那么让我们开始吧。

State Properties @State

在前面的一篇文章中当我们给数组添加或者删除元素时,列表会自动响应变化,正是因为使用了@State来标记View中的model。

  1. struct ContentView: View { 
  2.     @State private var title: String = "" 
  3.     var body: some View { 
  4.         VStack { 
  5.             Text("\(title)"
  6.             TextField("Please Enter Title", text: $title) 
  7.         } 
  8.     } 

使用@State包装的变量是可以被SwiftUI读取的值,这些值通常是一些字符串或数字等常量值。

当被State包装的属性改变时,SwiftUI会重新计算和绘制使用到该属性的视图所在的整个视图层级,通常是说变量所在View的Body会被重新绘制,在本例子中就是指ContentView的body。

被@State包装的变量一定要用private修饰,并且这个变量只能在当前view以及其子View的body中使用。

正如TextField中的$title一样,我们可以通过*$*前缀把变量和另一个view绑定起来,这样就可以在另一个view中对这个变量进行修改了。下面的代码使用一个Toggle来控制Wi-Fi的开关:

  1. struct ContentView: View { 
  2.     @State private var isOn = false 
  3.     var body: some View { 
  4.         VStack { 
  5.             Text("Wi-Fi State: \(isOn ? "On" : "Off")"
  6.             Image(systemName: "\(isOn ? "wifi" : "wifi.slash")"
  7.             Toggle("Wi-Fi State", isOn: $isOn) 
  8.         } 
  9.     } 

正如上面的代码所示,我们在isOn属性和Toggle控件之间建立了一个绑定。Toggle可以修改isOn的值,另外当isOn改变的时候,Text和Image的内容都会发生变化。

State Binding 状态绑定

使用@state包装的属性只在它所属view的内部使用,那么当它的子视图要访问这个属性的时候就要用到@binding了。就像上面例子用到的Toggle,我们把Text和Image放到一个自定义的View中。

  1. struct WiFiView: View { 
  2.     @Binding var isOn: Bool 
  3.      
  4.     var body: some View { 
  5.         Text("Wi-Fi State: \(isOn ? "On" : "Off")"
  6.         Image(systemName: "\(isOn ? "wifi" : "wifi.slash")"
  7.     } 

在这里我们使用了@Binding来创建数据和界面之间的依赖,它和@State不同的地方在于,binding的属性并不被当前的视图所持有,并且binding的值是可以被state的属性值导出的。

在这里如果把@Binding换成@State就会使WiFiView和它的父视图分别拥有各自的isOn属性,其中一个修改不会影响到另一个,而这显然不是我们想要的结果。

使用Combine框架的Publisher

使用@State包装的属性只能在当前View内部或者它的子视图中使用,并且state属性时临时的——由于state包装的属性是隶属于它所在view的,当view被销毁时对应的state属性也会消失,这明显是不够的,另外我们在开发过程中还要处理一些非界面的信息,比如说Timer、Notification等,它们携带的信息通常也会有更新界面的需求。这种情况就要用到Combine中的Publisher了。

Combine是在iOS13中被引入的,主要目的是为了处理App中的各种事件消息。如果你之前接触过RxSwift或者ReactiveCocoa那你对这个概念应该很容易理解,它的原理就是发布者和订阅者的模式。

  1. class Contact: ObservableObject { 
  2.     @Published var name: String 
  3.     @Published var age: Int 
  4.      
  5.     init(name: String, age: Int) { 
  6.         self.name = name 
  7.         self.age = age 
  8.     } 
  9.  
  10. struct ContentView: View { 
  11.     @ObservedObject var xiaowang = Contact(name"xiaowang", age: 21) 
  12.     var body: some View { 
  13.         VStack { 
  14.             Text("小王:\(xiaowang.name)"
  15.              
  16.             // 这里只是一个例子,通常不会在这里对Publisher进行修改 
  17.             Button("修改联系人") { 
  18.                 xiaowang.name = "小王" 
  19.             } 
  20.         } 
  21.     } 

我们先创建一个遵守ObservableObject协议的联系人类,然后在SwiftUI视图中添加一个被ObservedObject包装的变量,在body中使用这个变量,当被@Published包装的变量改变时,body会使用新值重新加载。

如果你在看WWDC2019的Introducing Combile视频的时候发现BindableObject/didChange.send()/onReceive,这些内容现在已经被移除了。

只有class可以遵守ObservableObject协议,否则会报错 Non-class type 'Contact' cannot conform to class protocol 'ObservableObject'

在iOS14中,引入了一个新的@StateObject来丰富这种使用场景。它和ObservedObject的区别在于当view刷新时被ObservedObject包装的属性会重置到初始值,而被StateObject使用的不会。

除非在某些必要的情况下需要使用ObservedObject之外,大多数情况都适用于StateObject。

环境变量 Evironment Objects

除了以上列出的几个场景之外,假设我们的app需要从一个页面跳转到另一个页面,这是一个很常见的场景,并且在后一个页面要用到前面页面的一些属性。通常可以这样做:

  1. NavigationLink(destination: nextView(aModel: aModel)) { 
  2.     Text("Detail"

上面用到了NavigationLink来做导航,destination是要弹出的页面,初始化时带着当前页面的一个属性。

这样处理没有什么大的问题,不过如果层级变多,后面层级又出现很多新层级,再有反向传值的话就会很复杂容易出错——就像使用UIKit的时候。而为了解决这个问题,SwiftUI引入了Evironment Objects。

  1. // DataSource.swift 
  2.  
  3. class DataSource: ObservableObject { 
  4.     @Published var counter = 0 
  5.  
  6. // ContentView.swift 
  7.  
  8. struct ContentView: View { 
  9.     let dataSource = DataSource() 
  10.     var body: some View { 
  11.         NavigationView { 
  12.             VStack { 
  13.                 Button("Click") { 
  14.                     dataSource.counter += 1 
  15.                 } 
  16.                 NavigationLink( 
  17.                     destination: ContactView()) { 
  18.                     Text("Enter Next Page"
  19.                 } 
  20.             } 
  21.         } 
  22.         .environmentObject(dataSource) 
  23.     } 
  24.  
  25. // ContactView.swift 
  26.  
  27. struct ContactView: View { 
  28.     @EnvironmentObject var dataSource: DataSource 
  29.     var body: some View { 
  30.         Text("\(dataSource.counter)"
  31.     } 

Environment Object和ObservedObject/StateObject用法非常相似,首先DataSource遵守ObservableObject协议,要观察的属性counter使用Publisher包装。

被@EnvironmentObject包装的属性会随着Publised属性的改变而变化,所在的view也会重新加载。

.environmentObject是一个Modifier,它向环境变量中注入一个属性,如果不在使用@EnvironmentObject之前使用把属性注入到环境变量中,就会包错 {% label danger@MissingEnvironmentObjectError: Missing EnvironmentObject %}

总结

在这篇文章中我们学习了SwiftUI框架中数据的流动相关知识,其中主要涉及了几个常用的属性包装器(property wrapper):@State @Binding @ObservedObject @StateObject @EnvironmentObject用法和适用场景,希望对你所有帮助。

【责任编辑:未丽燕 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

订阅专栏+更多

数据湖与数据仓库的分析实践攻略

数据湖与数据仓库的分析实践攻略

助力现代化数据管理:数据湖与数据仓库的分析实践攻略
共3章 | 创世达人

5人订阅学习

云原生架构实践

云原生架构实践

新技术引领移动互联网进入急速赛道
共3章 | KaliArch

31人订阅学习

数据中心和VPDN网络建设案例

数据中心和VPDN网络建设案例

漫画+案例
共20章 | 捷哥CCIE

217人订阅学习

视频课程+更多

kubernetes/k8s全栈技术讲解+企业级实战项目课程【新版】

kubernetes/k8s全栈技术讲解+企业级实战项目

讲师:先超27648人学习过

Spring 5.x框架

Spring 5.x框架

讲师:张晨光883人学习过

清华编程高手尹成带你实战python

清华编程高手尹成带你实战python

讲师:尹成108342人学习过

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊

51CTO服务号

51CTO官微