SwiftUI数据流Data Flow之属性包装器propertyWrapper,@State,@Binding,@Published,@ObservedOjbect,@StateObject,@Environment,@EnvironmentObject
所属分类:ios | 发布于 2022-12-04 15:51:45
SwiftUI中的界面是严格数据驱动的:运行时界面的修改,只能通过修改数据来间接完成,而不是直接对界面进行修改操作。
- Data Access as a Dependency:在SwiftUI中数据一旦被使用就会成为视图的依赖,也就是说当数据发生变化了,视图展示也会跟随变化,不会像MVC模式下那样要不停的同步数据和视图之间状态变化。
- A Single Source Of Truth:保持单一来源,在SwiftUI中不同视图之间如果要访问同样的数据,不需要各自持有数据,直接共用一个数据源即可,这样做的好处是无需手动处理视图和数据的同步,当数据源发生变化时会自动更新与该数据有依赖关系的视图。
属性包装器propertyWrapper
@State
@Binding
@Published
@ObservedObject
@StateObject
@Environment
@EnvironmentObject
上面的几个标志符都以@开头,这表示他们均由@propertyWrapper产生。
属性包装器的定义
先看@State属性包装器的定义:
@frozen @propertyWrapper public struct State<Value> : DynamicProperty {
public init(initialValue value: Value)
public var wrappedValue: Value { get nonmutating set }
public var projectedValue: Binding<Value> { get }
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension State where Value : ExpressibleByNilLiteral {
@inlinable public init()
}
创建包装器propertyWrapper时需要实现两个属性:wrappedValue(被包装的值),projectedValue(呈现值)。
可以看到定义属性包装器的语法为:
@propertyWrapper struct 包装器名称 {
@var wrappedValue: 类型 {
get {}
set {}
}
var projectedValue: 类型 {
return
}
初始化方法(可选)
}
其中wrappedValue就是需要包装的那个值,直接访问,projectedValue就是你看你希望通过包装额外呈现出来的某个值,$+projectedValue(属性名称)访问。
@State与@Binding
@State
@State是一个属性包装器,可以用来描述视图的状态。SwiftUI会将其存储在View struct之外的特殊内存中,只有相关视图才能访问它。当@State修饰过的属性发生了变化,SwiftUI会根据新的属性值重新创建视图。
@State修饰的数据作用域为当前view内部,所以@State常与private搭配使用。
而且@State是值引用,常用于修饰简单的数据类型,比如Int,Double,String,Bool,Struct,Enum。
通过上面的State的定义我们可以看到,与一般的存储属性不同,被@State修饰的值,在SwiftUI内部会被自动转换成一对setter和getter,对这个属性的修改操作会触发view的刷新,它的body会被再次强调,底层渲染引擎会找出界面上被修改的部分,根据新的属性值计算出新的view,并进行刷新。
struct StateWrapper: View {
@State private var text = "Hello World!"
var body: some View {
VStack {
Text(text)
Button("ClickMe") {
text = "Hello SwiftUI!"
}
}
}
}
当点击ClickMe时,被@State修饰的text属性的值被设置为"Hello SwiftUI",SwiftUI会自动侦测出text的值发生变化,并刷新view来响应变化。
再看一个带子视图的例子:
struct StateWrapperWithSubView: View {
@State private var text = "Hello World!"
var body: some View {
VStack {
Text(text)
Button("ClickMe") {
text = "Hello SwiftUI!"
}
SubView(subText: text)
}
}
}
struct SubView: View {
@State var subText: String
var body: some View {
VStack {
Text("subText: \(subText)")
Button("ClickMe") {
self.subText = "subText"
}
}
}
}
从父视图传递text="Hello world!"给子视图SubView内部的subText属性,是值的传递,传递的是值的拷贝,修改SubView中的subText时,不会影响到ContentView中的text的值。修改ContentView中text的值,也不会影响到SubView中subText的值。
注意:结构体和枚举是值类型,而Class是引用类型,当用@State修饰引用对象时,对引用对象的修改是无效的。
@Binding
有时候我们会把一个视图的属性传至子view中,但是又不能直接的传递给子view,因为在Swift中值的传递形式是值类型的传递方式,也就是传递给子视图的是一个拷贝过的值。但是通过@Binding修饰器修饰后,属性变成了一个引用类型,传递变成了引用传递,这样父子视图的状态就能管关联起来了。
@Binding和@State类型,也是对属性的修饰,它做的事情是将值语义的属性"转换"为引用语义。对被声明为@Binding的属性进行复制,改变的将不是属性本身,而且它的引用,这个改变可以被向外传递。
@Binding和@State搭配使用,父View用@State,子View用@Binding,以实现父view和子view的数据绑定。
当数据从外部传递的,并且需要和外部保持绑定时,就用@Binding修饰该变量。
上面的父子视图的例子是值传递,假如我们想实现引用传递,就可以使用@Binding解决这个问题。
struct StateWrapperWithSubView: View {
@State private var text = "Hello World!"
var body: some View {
VStack {
Text(text)
Button("ClickMe") {
text = "Hello SwiftUI!"
}
SubView(subText: $text)
}
}
}
struct SubView: View {
@Binding var subText: String
var body: some View {
VStack {
Text("subText: \(subText)")
Button("ClickMe") {
self.subText = "subText"
}
}
}
}
子视图subText的修饰符从@State改为@Binding。
父视图传递text给子视图时,在text前加上了美元符号$。
点击后我们发现,不管是点击父视图的ClickMe还是点击子视图的ClickMe,父视图的text和子视图的subText都发生了改变,这两个值已经被绑定在一起,不管在哪里修改一个的值,另外一个都会跟着发生变化。
@Published、@ObservedObject和@StateObject
ObservableObject与@Published
ObservableObject是Combine响应式框架下的协议类型,它是针对引用类型设计,用于在多个View直接共享数据。满足协议的类(Class)中使用@Published标记可能发生改变的属性。
ObservableObject协议要求实现类型是Class,它只要一个需要实现的属性:objectWillChange。在数据将要发生改变时,这个恶属性用来向外进行"广播",它的订阅者(一般是View)在收到通知后,对View进行刷新。
@ObservedObject@StateObject
用@ObservedObject和@StateObject包装实现了ObservableObject协议的实例,当这个实例的某些被@Published标记的属性发生改变时,(Published发布者)会在改实例被改变之前广播给UI(数据订阅者)。
@Published
@Published是SwiftUI最有用的包装器之一,允许我们创建能够被自动观察的对象属性,SwiftUI会自动监视这个属性,一旦发生了改变,会自动修改与该属性绑定的界面。
@ObservedObject
@ObervedObject的用处和@State非常相似,从名字看来它是修饰一个对象的,这个对象可以给多个独立的View使用。如果你用@ObservedObject来修饰一个对象,那么这个对象必须要实现ObservableObject协议,然后用@Published修饰对象里的属性,表示这个属性是需要被SwiftUI监听的。
初体验:
final class Person:ObservableObject{
@Published var name = "张三"
}
struct ContentView: View {
@ObservedObject var p = Person()
var body: some View {
VStack{
Text(p.name)
.padding()
Button("点我") { //添加一个按钮,指定标题文字为 First button
p.name = "1234567890"
}
}
}
}
带子视图的例子:
struct ObservedObjectWrapper: View {
@State var count = 0
var body: some View {
VStack {
Text("刷新count计数:\(count)")
Button("刷新") {
count += 1
}
SubView()
}
}
}
extension ObservedObjectWrapper {
final class Person: ObservableObject {
@Published var name = 1
deinit {
print("destory")
}
}
struct SubView: View {
@ObservedObject var p = Person()
var body: some View {
VStack {
Text("\(p.name)")
Button("+1") {
p.name += 1
}
}
}
}
}
多次点击+1,让p.name=5,再点击刷新时,发生p.name被设置为了1,也就是父视图的刷新过程中,子视图里的p对象被销毁了。
@StateObject
和@ObservedObject类似,区别是StateObject由SwiftUI负责针对一个指定的View,创建和管理一个实例对象,不管多少次刷新,都能够使用本地对象数据而不丢失。
初次体验:
import SwiftUI
final class Person:ObservableObject{
@Published var name = "测试"
deinit{
print("销毁")
}
}
struct ContentView: View {
@StateObject var p = Person()
var body: some View {
VStack{
Text(p.name)
Button("点击"){
p.name = "哈哈哈"
}
}
}
}
进阶版,把上面带子视图的例子中的@ObservedObject改成@StateObject实施
struct StateObjectWrapper: View {
@State var count = 0
var body: some View {
VStack {
Text("刷新count计数:\(count)")
Button("刷新") {
count += 1
}
SubView()
}
}
}
extension StateObjectWrapper {
final class Person: ObservableObject {
@Published var name = 1
deinit {
print("destory")
}
}
struct SubView: View {
@StateObject var p = Person()
var body: some View {
VStack {
Text("\(p.name)")
Button("+1") {
p.name += 1
}
}
}
}
}
发现不管怎点击,p对象一直都不会被销毁。
区别:
@StateObject的生命周期和当前所在View生命周期保持一致,即当View被销毁后,StateObject的数据被销毁,当View被刷新时,StateObject的数据会保持。
@ObservedObject只作为View的数据依赖,不被View持有,View更新时ObservedObject对象可能被销毁,也可能被保持,适合数据在SwiftUI外部存储,把@ObservedObject包裹的数据作为视图的依赖,比如数据库中存储的数据。
@Environment和@EnvironmentObject
从名字上可以看出,这两个修饰器是针对全局环境的。通过它们,我们可以在初始化View时,直接从环境中获取变量。
@Environment
SwiftUI本身就有很多系统级别的环境变量,我们可以通过@Environment来获取他们。
struct EnvironmentWrapper: View {
@Environment(\.calendar) var calendar: Calendar
@Environment(\.locale) var locale: Locale
@Environment(\.colorScheme) var colorScheme: ColorScheme
var body: some View {
Text(locale.identifier) // en_US
}
}
@EnvironmentObject
在SwiftUI中,View提供了environmentObject(_:)方法,来把某个某个ObservableObject的值注入到当前View层级及其子View层级中,在这个View的子层级中,就可以使用@EnvironmentObject来直接获取这个绑定的环境值。
final class Person: ObservableObject {
@Published var name = "888"
}
struct EnvironmentObjectWrapper: View {
var body: some View {
VStack {
let p = Person()
SubView().environmentObject(p)
}
}
}
struct SubView: View {
@EnvironmentObject var p: Person
var body: some View {
VStack {
Text("p.name: \(p.name)")
Button("ClickMe") {
p.name = "999"
}
}
}
}
如果对象存活于整个生命周期,并且大量view需要共享此对象,那么用@EnvironmentObject修饰是最好的。
当然,你也可以用@ObservedObject来达到同样的目的,但你需要通过view1传递给view2,传递给view3,传递链条太长了。此时,用@EnvironmentObject就显得方便很多。
在父View中被观察对象(被ObseredObject保证的对象)需要在子view中使用时,值需要在子View用@EnvironmentObject包装同类型的变量,并在调用的时候使用.environmentObject()来显示的说名。