@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
用于将键路径指定的环境值传递给视图层次结构。它依赖于 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
用于将 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
的上下文,你可能会同时使用这两种方式。这表明你可以同时使用 @Environment
和 @EnvironmentObject
,它们用于解决不同类型的数据传递问题。
struct ContentView: View {
@Environment(\\.managedObjectContext) var managedObjectContext
@EnvironmentObject var dataController: DataController
var body: some View {
// 既可以使用 Core Data 的上下文
// 也可以访问 dataController 的属性
}
}
SwiftUI 中常用的环境值有以下这些,我们可以从环境中获取它们的值,赋予变量:@Environment(\\.colorScheme) var colorScheme
名称 | 用途说明 | 枚举值或示范用例 |
---|---|---|
colorScheme | 当前系统的颜色模式(浅色或深色) | .dark , .light |
colorSchemeContrast | 当前颜色模式的对比度设置 | .standard , .increased |
accessibilityEnabled | 当前是否启用了辅助功能 | true , false |
locale | 当前应用的语言环境设置 | Locale.current |
calendar | 当前系统使用的日历 | Calendar.current |
timeZone | 当前系统使用的时区 | TimeZone.current |
displayScale | 获取当前显示屏的缩放比例 | 2.0, 3.0 |
sizeCategory | 当前的内容大小类别(字体大小) | .extraSmall , .small , .medium , .large , .extraLarge , .extraExtraLarge , .extraExtraExtraLarge , .accessibilityMedium , .accessibilityLarge , .accessibilityExtraLarge , .accessibilityExtraExtraLarge , .accessibilityExtraExtraExtraLarge |
horizontalSizeClass | 当前界面的水平尺寸类别(紧凑或常规) | .compact , .regular |
verticalSizeClass | 当前界面的垂直尺寸类别(紧凑或常规) | .compact , .regular |
layoutDirection | 当前界面的布局方向(从左到右或从右到左) | .leftToRight , .rightToLeft |
deviceOrientation | 当前设备的方向 | .unknown , .portrait , .portraitUpsideDown , .landscapeLeft , .landscapeRight , .faceUp , .faceDown |
defaultMinListRowHeight | List 列表的每行最小高度 | @Environment(\\.defaultMinListRowHeight) var defaultMinRowHeight |
scenePhase | 当前应用场景的生命阶段(活动、非活动或后台) | .active , .inactive , .background |
presentationMode | 当前视图的呈现模式 | presentationMode.wrappedValue.dismiss() |
dismiss | 获取“关闭当前视图”的方法 | dismiss() |
undoManager | 当前视图的撤销管理器 | UndoManager() |
redactionReasons | 当前视图的隐蔽理由 | .placeholder , .privacy |
editMode | 当前编辑模式的状态 | .inactive , .active , .transient |
编辑模式可以代表一个页面是否处于编辑状态;您可以通过绑定设置编辑模式,也可以配合 EditButton 按钮来自动完成此操作。即点击 EditButton 的时候,就会将 editMode 的状态改变。 | ||
openURL | 获取“在Safari应用中打开 URL” 的方法 | openURL(URL(string: "<https://www.apple.com>")!) |
SwiftUI 的 @Entry
宏使为环境创建自定义值变得简单,具体做法如下
https://www.hackingwithswift.com/quick-start/swiftui/how-to-create-and-use-custom-environment-values
// 创建自定义的 EnvironmentKey 并加入到 EnvironmentValues 扩展中方便使用
// 控制 Tabbar 展示
struct ShowCustomTabbarKey: EnvironmentKey {
static let defaultValue: Binding<Bool> = .constant(true)
}
extension EnvironmentValues {
var showTabBar: Binding<Bool> {
get { self[ShowCustomTabbarKey.self] }
set { self[ShowCustomTabbarKey.self] = newValue }
}
}