Property Wrappers - tems
- @State属性包装器
- @StateObject属性包装器
- @Published属性包装器
- @ObservedObject属性包装器
- @EnvironmentObject属性包装器
- @Environment属性包装器
- @Binding属性包装器
概述
文章主要分享SwiftUI Modifier的学习过程,将使用案例的方式进行说明。内容浅显易懂,Property Wrappers items介绍具体属性包装器,偏向理论,可以移步Github下载code -> github案例链接
1、@State属性包装器
SwiftUI使用@State
属性包装器可以修改结构体中的值,在结构体中,由于结构体是值类型,默认情况下是不允许的。
使用@State修饰属性时,实际上将其存储在结构体中移除,并放入由SwiftUI管理的共享缓存中
。这代表SwiftUI可以根据需要销毁并重新创建结构体(这在程序运行中可能会发生很多次!),而不会丢失它正在存储的状态。
Swift
struct FFPropertyWrapperState: View {
//由于@State的特性是存储在SwiftUI的共享缓存中,Apple建议使用@State修饰的变量最好标记为private
@State private var prompt = "meta BBLv"
//当然,这不是必须选项,你也可以不使用private定义
@State var hobby = "KEEP LOVING, KEEP LIVING"
//当使用@State修饰引用类型的时候,数据发生更改时,不会收到通知,对于不符合ObservableObject协议的类有用。
var body: some View {
Text("Hello, World!")
}
}
2、@StateObject属性包装器
SwiftUI的@StateObject
属性包装器旨在填补状态管理中的一个非特定的空白:当你需要在视图内创建一个引用类型并确保它在该视图与其他与之共享的视图中保持active状态时。
Swift
//创建一个User类,并遵守obserVableObject
class StateObjectUser: ObservableObject {
var username = "meta BBLv"
}
struct FFPropertyWrapperStateObject: View {
//如果想在视图中使用它,可以在SwiftUI外部创建并注入,或者在SwiftUI中创建并使用@StateObject
@StateObject var user = StateObjectUser()
var body: some View {
Text("Username: \(user.username)")
//这将确保user实例视图更新时不会被销毁
}
}
以前,可以使用@ObservedObject
来获得相同的结果,但这是不安全的操作。在极小的概率下@ObservedObject可能会意外释放它存储的对象,因为它并没有被设计成对象的最终数据源。说人话就是它不持有数据,不对数据负责。而@StateObject
则不会出现此问题,所以引用类型要使用@StateObject修饰。
重要提示:在创建视图时负责对对象的创建,使用@StateObject创建的对象应该被创建一次,然后其他共享的视图使用@ObservedObject来共享对象。
3、@Published属性包装器
@Published
是SwiftUI最重要的属性包装器之一,因为可以创建可观察对象,自动在发生更改时进行通知。SwiftUI会自动监听这些更改,并重新调用依赖于数据的视图的body属性,本质上会刷新视图。
在实际操作中,这意味着每当带有@Published标记属性的对象发生更改时,所有使用该对象的视图都会重新加载来push这些更改。
3.1、默认创建属性
Bag符合ObservableObject
协议,那么SwiftUI的属性可以观察它的更改。但是,因为他的属性items并没有使用@Published标记
,所以永远不会发送更改通知,这个时候,你可以自由的想数组添加数据,但不会自动更新视图。
Swift
//创建一个可观察对象
class Bag: ObservableObject {
var items = [String]()
}
3.2、使用@Published标记属性
Swift
//如果希望它可以接受通知,只需要添加@Published就好了
class BagPublished: ObservableObject {
@Published var items = [String]()
}
这种情况下,不需要执行任何其他操作,@Published
属性包装器实际上会为items添加一个willSet
属性观察器,以便任何更改都会自动发送给观察者。
@Published是一种选择性的方法,需要根据需求列出哪些属性更改应该发送通知,然后使用@Published修饰,默认情况下,未使用@Published创建的变量在变化时是不会引起视图的重新加载的。比如在存储缓存、内部使用属性,这些就值的更改就没必要更新视图。
4、@ObservedObject属性包装器
SwiftUI提供了@ObservedObject
属性包装器,以便视图可以关在外部对象的状态,并在重要的更改发生时得到通知。它在行为上类似@StateObject,但不能用于创建对象,只能使用@ObservedObject修饰已在其他地方创建的对象,以方便共享数据。如果意外使用了@ObservedObject创建了对象,那么SwiftUI可能会意外销毁对象造成crash。
Swift
class Order: ObservableObject {
@Published var items = [String]()
}
struct FFPropertyWrapperObservedObject: View {
@ObservedObject var order: Order
var body: some View {
Text("Hello, World!")
}
}
Order类使用@Published修饰了items,因此当items发生更改时,他将自动发送通知,而FFPropertyWrapperObservedObject使用@ObservedObject来监听这些通知,如果没有@ObservedObject,更改通知依然会发送,但是会被当前视图忽略。
尽管看起来很简单,但值得深入了解一些具体细节:
-
首先,使用
@ObservedObject
标记的任何类型都必须符合ObservableObject协议,这意味着它必须是一个类而不是结构体,这是规定,SwiftUI要起此处必须使用class -
其次,被观察对象专门设计用于在外部观察你的视图的对象,这意味着它可以在多个视图之间共享。@ObservedObject属性包装器将自动确保属性收到密切监视,以便重要的更改出现时重新加载使用它的任何视图。这还代表了数据必须在其他地方被创建,是被传入你的视图的。
-
第三,不是被观察的对象中所有的属性都会导致视图刷新。由你的需求决定了哪些属性更改会发送更改通知,要么使用@Published,要么自定义通知,都可以实现。符合ObservableObject协议的类型会得到一个默认的
objectWillChange
的发布者,可以根据需要进行自定义通知。
5、@EnvironmentObject属性包装器
SwiftUI的@EnvironmentObject
属性包装器可以创建依赖于共享数据的视图,通常跨足整个SwiftUI应用程序。例如,如果创建一个将在应用程序的许多部分之间共享的用户对象,那么使用@EnvironmentObject
Swift
//它符合ObserveableObject协议,这代表了可以使用@Observedobject或@EnvironmentObject来修饰对象。
class EnvironmentOrder: ObservableObject {
@Published var items = [String]()
}
struct FFPropertyWrapperEnvironmentObject: View {
//通过@EnvironmentObject来修饰对象,这是一个共享数据,也就是本身并不持有数据。
@EnvironmentObject var order: Order
var body: some View {
Text("Hello, World!")
}
}
order属性并没有通过@EnvironmentObject初始化,并给出默认值。在当前这个视图中,该值由SwiftUI环境提供,而不是由此视图显示创建。@EnvironmentObject和@ObservedObject有很多相似之处:
- 两者都必须引用遵守OvservableObject协议的class
- 两者可以夸多个视图共享数据
- 在使用@Published属性包装器修饰的属性发生更改时,都会更新任何引用此数据的视图。
- 使用@EnvironmentObject的对象将由外部实体提供,而不是由当前视图创建。
实际应用场景:将视图A的数据传递到视图E,中间间隔了视图(B、C和D)如果使用@ObservedObject需要链式传递,不管中间视图(B、C和D)是否需要数据,都也被动接收, 传递链条:A->B->C->D->E
当使用@EnvironmentObject
时,视图A创建一个对象并将其放入环境中,然后,内部的任何视图都可以在需要时随时访问该环境对象,只需要对环境请求即可,不需要显示传递,传递链条:A(创建并放入Environment)-> E (直接在Environment中读取)
异常点:当显示使用@EnvironmentObject的视图时,SwiftUI将立即在环境中搜索正确类型的对象。如果找不到(比如忘记放入到Environment中),那么应用程序将立即crash
。也就代表着,当你使用@EnvironmentObject时,你实际上是在告诉SwiftUI,我已经将对象存在环境中了,类似使用隐式解包。
6、@Environment属性包装器
SwiftUI提供了@Environment和@EnvironmentObject两个属性包装器,但他们在细节上有一些不同:而@Environmentobject
可以将值注入到环境中,@Environment
特别用于与SwiftUI自己预定义的key一起使用。
Swift
struct FFPropertyWrapperEnvironment: View {
//例如,@Environment很适合读取CoreData托管对象的context、设备是否处于深色模式或浅色模式、渲染视图时使用的大些类别等固定属性,这些属性来自系统
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@Environment(\.managedObjectContext) var managedObjectContext
//@Environment作用是在环境中读取对象
@EnvironmentObject var order: EnvironmentOrder
//这个区别看起来很小,但由于@EnvironmentObject的实现方式,它非常重要。当我们说order的类型是EnvironmentOrder时,SwiftUI将查找其环境以找到该类型的对象并将其附加到order属性上。然而,使用@Environment时,同样的行为不太可能发生,因为许多东西可能共享相同的数据类型。
@Environment(\.accessibilityReduceMotion) var reduceMotion
@Environment(\.accessibilityReduceTransparency) var reduceTransparency
@Environment(\.accessibilityEnabled) var accessibilityEnabled
//这三个环境key都返回一个bool值,所以如果不明确指定是那个Key,就无法正确读取他们。
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
}
}
6.1、@Environment
例如,@Environment很适合读取CoreData托管对象的context、设备是否处于深色模式或浅色模式、渲染视图时使用的大写类别等固定属性,这些属性来自系统。@Environment作用是在环境中读取对象
Swift
struct FFPropertyWrapperEnvironment: View {
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@Environment(\.managedObjectContext) var managedObjectContext
}
然而,使用@Environment时,同引用类型有很大区别,因为许多东西可能共享相同的数据类型。这三个环境key都返回一个bool值,所以如果不明确指定是那个Key,就无法正确读取他们。
Swift
struct FFPropertyWrapperEnvironment: View {
@Environment(\.accessibilityReduceMotion) var reduceMotion
@Environment(\.accessibilityReduceTransparency) var reduceTransparency
@Environment(\.accessibilityEnabled) var accessibilityEnabled
}
6.2、@EnvironmentObject
@EnvironmentObject就不需要制定具体的Key了,关联到应用类型的对象即可。这个区别看起来很小,但由于@EnvironmentObject
的实现方式,它非常重要。当order的类型是EnvironmentOrder时,SwiftUI将查找其环境以找到该类型的对象并将其附加到order属性上。
Swift
struct FFPropertyWrapperEnvironment: View {
@EnvironmentObject var order: EnvironmentOrder
}
7、@Binding属性包装器
@Binding
声明一个值来自其他地方,并且应该在这两个地方共享使用。这与@ObservedObject和@EnvironmentObject不同,后者都是设计用于在多个视图之间共享引用类型。
Swift
//创建AddView视图
struct AddView: View {
@Binding var isPresented: Bool
var body: some View {
Button("关闭") {
isPresented = false
}
}
}
//创建了FFPropertyWrapperBinding视图,其中再创建一个包含@State的属性,用于存储一个子视图是否正在显示。
struct FFPropertyWrapperBinding: View {
@State private var showingAddUser = false
var body: some View {
VStack {
//代码区域
Button("打开用户中心") {
showingAddUser = true
}
}
.sheet(isPresented: $showingAddUser) {
//显示添加用户视图
AddView(isPresented: $showingAddUser)
//整个操作可以理解为FFPropertyWrapperBinding和AddView共享同一个Bool值,
//当他在一个地方发生变化时,也会在另外一个地方发生改变。
}
}
}
使用showingAddUser
作为sheet的isPresented参数,如果bool值更改为true时,将显示present的视图。并且被presnt的视图可以自己关闭,就需要将showingAddUser传递到present的视图。
期待的情况是,present出来的用户视图可以将showingAddUser设置为false,那么FFPropertyWrapperBinding视图就会隐藏它,以达到关闭当前present视图的目的。那么使用@Binding就非常完美,因为它可以在添加用户视图中创建一个属性,表示"这个值来自其他地方,并且在我们之间共享状态。"
调试结果
我并没有为AddView添加dismiss相关函数,完全是靠showingAddUser的bool值来控制的dismiss