@Environment 背景

@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 属性包装器,需要对以上代码进行两个小更改:

1. 把对象放入环境修饰符

首先不再需要直接将值传递到子视图,而是使用 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>

2. 读取环境属性

一旦将对象放入环境中,任何子视图都可以将其读回。我们需要在子视图中将其 player 属性修改为:

// 这里要小心:如果您声明了环境对象,但实际上环境中没有该对象,您的应用程序就会崩溃
@Environment(Player.self) var player

读取时,似乎我们没有给它一个默认值,所以你可能会认为这里有问题。然而  @Environment 属性包装器后面的变量,它已经存在于环境中了。显示此视图时,SwiftUI 将自动在【环境对象列表】中查找 Player 类型的内容,并将其附加到该属性。如果找不到 Order 对象,那么就会遇到问题:我们所说的东西不存在,代码就会崩溃。这就像一个隐式展开的可选选项,所以要小心。在预览的时候就会因为这个崩溃,所以要修复…

3. 修复预览

#Preview {
    //加上这个可以预览 ItemDetailView 在导航堆栈中的情况
    NavigationStack {
        ItemDetailView(item: MenuItem.example)
            //当视图使用了环境对象时,预览代码中必须加上这一行,否则会崩溃
            .environment(Order())
    }
}

4. 环境属性用作绑定

虽然这在大多数情况下工作得很好,但有一个地方存在问题,您几乎肯定会遇到它:当尝试使用 @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 属性的副本,然后将其包装在我可以使用的一些绑定中。” 老实说它有点难看,希望以后它不再需要了!

5. 注入某个环境键

通过 @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

这样一来,您可以读取整个对象或仅观察其中的一部分。如果需要,您可以获得充分的灵活性,或者将视图重新加载限制为仅部分数据。


environment & environmentObject

在 SwiftUI 中,.environment.environmentObject 都是用来将数据或依赖注入到视图层次结构中的,但它们的作用和用法有所不同。

使用 .environment

例如 .environment(\\.managedObjectContext, dataController.container.viewContext)

使用 .environmentObject

例如 .environmentObject(dataController)

两者对比结论

  1. 数据类型
  2. 数据注入方式
  3. 使用场景

如果你有一个 DataController 管理 Core Data 的上下文,你可能会同时使用这两种方式。这表明你可以同时使用 @Environment@EnvironmentObject,它们用于解决不同类型的数据传递问题。

struct ContentView: View {
    @Environment(\\.managedObjectContext) var managedObjectContext
    @EnvironmentObject var dataController: DataController

    var body: some View {
        // 既可以使用 Core Data 的上下文
        // 也可以访问 dataController 的属性
    }

}

@Environment 环境值大全

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 }
    }
}