SwiftUI 中的 @Environment —— 用于从系统环境中读取值的属性包装器,这些值在 View 层次结构中自动传播。该组件提供对颜色方案、区域设置、字体大小、managedObjectContext 以及数十个其他系统参数的访问。根据 Apple Developer Documentation (2025),@Environment 确保环境值的任何更改都会导致所有订阅的 View 重新绘制,无需手动调用即可实现界面的响应式更新。
要点
@Environment —— 是一个 SwiftUI 属性包装器,用于从系统环境中读取值。环境是一个层次化的值容器,SwiftUI 自动从父 View 向子 View 传播。每个环境值由一个键标识 —— 一个符合 EnvironmentKey 协议的类型。
环境机制让人联想到框架级别的依赖注入:系统提供了一组预定义值 —— 颜色方案(浅色/深色)、区域设置、字体大小、用于 Core Data 的 managedObjectContext、用于关闭屏幕的 dismiss 以及许多其他值。声明了带有特定键的 @Environment 的 View 会自动接收当前值,并在其更改时重新绘制。
SwiftUI 环境的架构基于 EnvironmentValues 协议 —— 一个包含所有系统值的结构。每个值作为该结构的属性存储,带有 getter 和 setter。@Environment 使用 key path 访问特定属性:@Environment(\.colorScheme) —— 访问颜色方案,@Environment(\.locale) —— 访问区域设置。
@Environment 属性包装器实现了两个关键机制:从环境读取值以及订阅其更改。在创建 View 时,SwiftUI 遍历所有 @Environment 属性并将它们与当前上下文中对应的值关联。如果父 View 通过 .environment() 修改器更改了值,所有读取该值的子 View 将自动重新绘制。
重要特性:@Environment 支持可选值。如果值未在层次结构中设置,则返回 EnvironmentKey 中定义的默认值。对于系统键,默认值总是合理的 —— 例如,默认颜色方案是 .light。对于自定义键,开发者在 EnvironmentKey 协议的 defaultValue 方法中自行定义默认值。
struct EnvironmentReaderView: View {
@Environment(\.colorScheme) var colorScheme
@Environment(\.locale) var locale
@Environment(\.sizeCategory) var sizeCategory
var body: some View {
VStack {
Text("当前方案: \(colorScheme == .dark ? "Dark" : "Light")")
Text("Locale: \(locale.identifier)")
Text("字体大小: \(sizeCategory)")
}
}
}
在示例中,View 读取三个系统环境值。当 colorScheme 更改时 —— 例如,用户在设置中启用了深色模式 —— View 会自动使用新值重新绘制。当区域或字体大小(Dynamic Type)更改时也是如此。View 无需订阅通知或调用刷新 —— SwiftUI 自动管理这一切。
SwiftUI 提供了数十个系统环境值,涵盖界面和行为的各个方面。颜色方案(\.colorScheme)—— 最受欢迎的值之一,允许将界面调整为浅色和深色主题。区域设置(\.locale)包含用于格式化日期、数字和货币的用户区域设置。
对于 Core Data,使用 managedObjectContext(\.managedObjectContext)—— 通过环境从持久化容器传递的上下文。对于导航,dismiss(\.dismiss)用于关闭当前屏幕,isPresented(\.isPresented)用于模态视图。对于日历和时区 —— calendar 和 timeZone。
| Key Path | 类型 | 用途 |
|---|---|---|
| \.colorScheme | ColorScheme | 浅色或深色主题 |
| \.locale | Locale | 区域设置 |
| \.sizeCategory | ContentSizeCategory | Dynamic Type 字体大小 |
| \.managedObjectContext | NSManagedObjectContext | Core Data 上下文 |
| \.dismiss | DismissAction | 关闭屏幕 |
| \.calendar | Calendar | 当前日历 |
| \.timeZone | TimeZone | 时区 |
| \.horizontalSizeClass | UserInterfaceSizeClass | 屏幕水平尺寸 |
要访问系统值,请使用带点的 key path:@Environment(\.dismiss) var dismiss。编译器会检查 EnvironmentValues 中 key path 的存在性,因此错误的键将在编译阶段导致错误。Apple 在每个 iOS 版本中都会添加新的系统值 —— 当前列表可在 EnvironmentValues 文档中找到。
尽管名称相似,@Environment 和 @EnvironmentObject 解决不同的任务。@Environment 读取通过 EnvironmentKey 注册的系统或自定义值。@EnvironmentObject —— 是一个 ObservableObject 的属性包装器,通过环境按类型传递,无需显式键。
@EnvironmentObject 用于依赖注入:父 View 创建一个对象(例如 ViewModel)并通过 .environmentObject() 修改器将其传递给子 View。子 View 通过 @EnvironmentObject 接收它,并可以读取和修改其属性。而 @Environment —— 只读系统值,不支持反馈。
| 参数 | @Environment | @EnvironmentObject |
|---|---|---|
| 用途 | 系统和自定义值 | ObservableObject 注入 |
| 键 | EnvironmentValues key path | 按对象类型 |
| 写入 | 只读 | 读写 |
| 自定义值 | 通过 EnvironmentKey | 通过 ObservableObject 类 |
| 默认值 | 有(defaultValue) | 无(必须传递) |
在实践中:使用 @Environment 访问系统参数(主题、区域设置、字体大小)和运行时不变的自定义配置。使用 @EnvironmentObject 在 View 层次结构中传递 ViewModel 或服务,当需要从子组件更改状态时。
让我们来看一下创建自定义环境值的过程。为此,需要定义一个符合 EnvironmentKey 协议的结构,并用新属性扩展 EnvironmentValues。这允许在没有 props 的情况下通过整个 View 树传递主题配置或应用程序设置。
struct AppThemeKey: EnvironmentKey {
static let defaultValue: AppTheme = .system
}
extension EnvironmentValues {
var appTheme: AppTheme {
get { self[AppThemeKey.self] }
set { self[AppThemeKey.self] = newValue }
}
}
enum AppTheme { case system, light, dark }
EnvironmentKey 协议要求实现静态属性 defaultValue —— 如果父 View 没有设置自定义环境时将使用的值。EnvironmentValues 扩展使用带键的 subscript 添加了计算属性 appTheme。之后,任何 View 都可以通过 @Environment(\.appTheme) 读取该值。
struct ThemedView: View {
@Environment(\.appTheme) var appTheme
@Environment(\.colorScheme) var colorScheme
var body: some View {
VStack {
if appTheme == .dark || (appTheme == .system && colorScheme == .dark) {
Text("深色模式已激活")
.foregroundStyle(.white)
.background(Color.black)
} else {
Text("浅色模式已激活")
.foregroundStyle(.black)
.background(Color.white)
}
}
}
}
struct ContentView: View {
@State private var selectedTheme = AppTheme.system
var body: some View {
ThemedView()
.environment(\.appTheme, selectedTheme)
}
}
ThemedView 读取两个环境:自定义的 appTheme 和系统的 colorScheme。这种组合允许实现灵活的主题设置:用户可以选择“浅色”、“深色”或“系统”主题。如果选择了系统主题 —— 值从 colorScheme 获取,该值在 iOS 设置中切换主题时自动更改。父 View(ContentView)通过 .environment() 修改器设置 appTheme 值。
struct ModalView: View {
@Environment(\.dismiss) var dismiss
@State private var name = ""
var body: some View {
NavigationStack {
Form {
TextField("Your name", text: $name)
Button("保存") { dismiss() }
}
.navigationTitle("Edit Profile")
}
}
}
该示例演示了 dismiss —— 来自环境的 DismissAction 实例 —— 的实际使用。调用 dismiss() 作为函数会关闭模态屏幕或返回 NavigationLink。唯一的要求是 View 必须以模态方式呈现或位于 NavigationStack 内。dismiss 根据上下文自动确定:如果 View 作为 sheet 打开 —— 关闭 sheet,如果作为 popover —— 关闭 popover。
常见问题
不可以,@Environment 仅用于读取。要更改值,请使用带有 ObservableObject 的 @EnvironmentObject 或 @Binding。自定义 EnvironmentKey 可以在扩展中具有 setter,但通过它进行的更改不会触发 UI 更新 —— 这在技术上是可行的,但不建议这样做。
@Binding 与真相源(State、StateObject、ObservableObject)建立双向连接。@Environment —— 从层次化上下文中单向读取。@Binding 适合向子 View 传递数据,@Environment —— 适合访问系统或全局设置。
定义一个实现了带有 static defaultValue 的 EnvironmentKey 协议的结构。然后通过 subscript[key] 为 EnvironmentValues 添加带有 getter/setter 的属性扩展。注册后,使用 @Environment(\.yourKey) 进行读取,使用 .environment(\.yourKey, value) 进行设置。
SwiftUI 提供了超过 50 个系统值:colorScheme、locale、sizeCategory、managedObjectContext、dismiss、calendar、timeZone、horizontalSizeClass、verticalSizeClass、accessibilityEnabled、layoutDirection、legibilityWeight 等。完整列表在 EnvironmentValues 文档中。
是的,@Environment 在 Preview 中有效,但默认值可能与模拟器不同。要在 Preview 中测试,请直接在 Preview 代码中使用 .environment() 修改器:ThemedView().environment(\.colorScheme, .dark)。这允许直观地检查不同的环境状态。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。