@Environment
仅适用于类,不适用于结构体。
假设应用程序中有多个视图,所有视图都排列在一个链中:视图 A 显示视图 B,视图 B 显示视图 C,C 显示 D,D 显示 E。视图 A 和 E 都想要访问同一个对象,但是要从 A 到 E,您需要经过 B、C 和 D,而他们并不关心该对象。这时如果我们使用 @ObservedObject
,我们需要将对象从每个视图传递到下一个视图,直到它最终到达可以使用它的视图 E,这很烦人,因为 B、C 和 D 不关心它。
// 例如存在以下代码:
// 创建一个可观察的 Player 类
@Observable
class Player {
var name = "Anonymous"
var highScore = 0
}
// 然后在一个小视图中显示它的属性
struct HighScoreView: View {
var player: Player
var body: some View {
Text("Your high score: \\(player.highScore)")
}
}
// 在 ContentView 中展示小视图,需要传递类的实例进去
struct ContentView: View {
@State private var player = Player()
var body: some View {
VStack {
Text("Welcome!")
HighScoreView(player: player)
}
}
}
该问题有更好的解决方案:我们可以在视图 A 将对象放入环境中,然后在视图 E 使用 @Environment
属性包装器将其读回。在这个过程中,视图 B、C 和 D 不必知道发生了什么,这就要好得多。
使用 @Environment
属性包装器,需要对以上代码进行两个小更改:
首先不再直接将值传递到子视图,而是使用 environment()
修饰符将对象放入环境中:
struct ContentView: View {
//1. 用 @State 新建该对象的【实例】,注意是要实作它
@State private var player = Player()
var body: some View {
VStack {
Text("Welcome!")
HighScoreView()
}
//2. 把该类的实例放进环境修饰符中
.environment(player)
}
}
<aside>
💡 environment
修饰符(没有 @
)是为使用 @Observable
宏的类设计的。宏所做的事情之一是:遵循名为 Observable
的协议
</aside>
一旦将对象放入环境中,任何子视图都可以将其读回。我们需要在子视图中将其 player
属性修改为:
@Environment(Player.self) var player
//这里要小心:如果您声明了环境对象,但实际上环境中没有该对象,您的应用程序就会崩溃
读取时,似乎我们没有给它一个默认值,所以你可能会认为这里有问题。然而 @Environment
属性包装器后面的变量,它已经存在于环境中了。显示此视图时,SwiftUI 将自动在【环境对象列表】中查找 Player
类型的内容,并将其附加到该属性。如果找不到 Order
对象,那么就会遇到问题:我们所说的东西不存在,代码就会崩溃。这就像一个隐式展开的可选选项,所以要小心。在预览的时候就会因为这个崩溃,所以要修复…
#Preview {
//加上这个可以预览 ItemDetailView 在导航堆栈中的情况
NavigationStack {
ItemDetailView(item: MenuItem.example)
//当视图使用了环境对象时,预览代码中必须加上这一行,否则会崩溃
.environment(Order())
}
}
虽然这在大多数情况下工作得很好,但有一个地方存在问题,您几乎肯定会遇到它:当尝试使用 @Environment
值作为绑定时。
注意:如果您在 iOS 18 发布后阅读本文,希望 Apple 已经解决了这个问题,但现在使用的是 iOS 17,这是一个问题。
struct HighScoreView: View {
@Environment(Player.self) var player
var body: some View {
Stepper("High score: \\(player.highScore)", value: $player.highScore)
}
}
// 这里尝试将 highScore 属性绑定到步进器。如果这里是使用 @State 创建 player 实例,则可以正常使用;
// 但不适用于 @Environment。苹果对此的解决方案(至少现在是这样)是直接在 body 属性中使用 @Bindable ,如下所示:
@Bindable var player = player
//这实际上意味着“在本地创建 player 属性的副本,然后将其包装在我可以使用的一些绑定中。” 老实说它有点难看,希望以后它不再需要了!
通过 @Environment
属性包装器,我们还可以将数据分解为离散的块并只观察其中的一部分,而不是将已发布属性的整个对象注入环境中。
// 例如,如果一个视图只关心玩家的高分,我们会将其创建为环境键:
struct HighScoreKey: EnvironmentKey {
static var defaultValue = 0
}
extension EnvironmentValues {
var highScore: Font {
get { self[HighScoreKey.self] }
set { self[HighScoreKey.self] = newValue }
}
}
// 然后创建一个小 View 扩展,以便于设置此环境键
extension View {
func highScore(_ score: Int) -> some View {
environment(\\.highScore, score)
}
}
// 现在,可以在任何需要它的视图中读取它,而不是在一个对象中读取所有用户数据:
@Environment(\\.highScore) var highScore
在实践中,这可能会令人沮丧,因为您需要一个接一个地创建各种环境键,因此您可能会发现将整个对象注入环境,但只观察它的特定部分,会更容易,如下所示:
@Environment(\\.user.highScore) var highScore
这样一来,您可以读取整个对象或仅观察其中的一部分。如果需要,您可以获得充分的灵活性,或者将视图重新加载限制为仅部分数据。
在 SwiftUI 中,.environment
和 .environmentObject
都是用来将数据或依赖注入到视图层次结构中的,但它们的作用和用法有所不同。
.environment(\\.managedObjectContext, dataController.container.viewContext)
.environment
:.environment
用于将键路径指定的环境值传递给视图层次结构。它依赖于 SwiftUI 内置的环境键(EnvironmentKey
),通过键路径将数据注入到视图树中。\\\\.managedObjectContext
是一个 SwiftUI 内置的 EnvironmentKey
,它表示 Core Data 的 NSManagedObjectContext
。通过使用 .environment
,你将 managedObjectContext
(由 dataController.container.viewContext
提供)注入到视图层次结构中,使得子视图可以通过 @Environment(\\\\.managedObjectContext)
获取并使用 Core Data 上下文。struct ContentView: View {
@Environment(\\\\.managedObjectContext) var managedObjectContext
var body: some View {
// 使用 managedObjectContext 进行 Core Data 操作
}
}
.environmentObject(dataController)
.environmentObject
:.environmentObject
用于将 ObservableObject
的实例注入到视图层次结构中。这个对象可以是任何符合 ObservableObject
协议的类,它允许视图通过 @EnvironmentObject
属性包装器在视图层次结构的任何地方访问该对象,并响应其属性的变化。dataController
是一个符合 ObservableObject
协议的对象(假设它实现了 ObservableObject
),并通过 .environmentObject
将它注入到视图树中,使得子视图可以通过 @EnvironmentObject
获取并使用它。class DataController: ObservableObject {
@Published var someData: String = "Data"
}
struct ContentView: View {
@EnvironmentObject var dataController: DataController
var body: some View {
Text(dataController.someData)
}
}
.environment
适用于特定的环境键值对(如 managedObjectContext
),通常用于注入一些简单的值(如布尔值、颜色、字体等),或者像 NSManagedObjectContext
这样的标准类型。.environmentObject
适用于自定义的 ObservableObject
,这种对象可以在多个视图之间共享,并且当对象中的数据发生变化时,视图会自动更新。.environment
是通过环境键路径将数据传递给视图树,而 @Environment
允许子视图访问这些值。.environmentObject
是将 ObservableObject
的实例注入到视图树,子视图通过 @EnvironmentObject
访问对象并绑定数据变化。.environment
更适合用于注入一些上下文相关的值,如 managedObjectContext
,colorScheme
,locale
等。.environmentObject
更适合用于共享一个复杂的对象(如 ViewModel
或 DataController
),并响应对象中数据的动态变化。如果你有一个 DataController
管理 Core Data
的上下文,你可能会同时使用这两种方式:
struct ContentView: View {
@Environment(\\\\.managedObjectContext) var managedObjectContext
@EnvironmentObject var dataController: DataController
var body: some View {
// 既可以使用 Core Data 的上下文
// 也可以访问 dataController 的属性
}
}
这表明你可以同时使用 @Environment
和 @EnvironmentObject
,它们用于解决不同类型的数据传递问题。