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()来显示的说名。

 

文哥博客(https://wenge365.com)属于文野个人博客,欢迎浏览使用

联系方式:qq:52292959 邮箱:52292959@qq.com

备案号:粤ICP备18108585号 友情链接