@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(\\.managedObjectContext, dataController.container.viewContext)

使用 .environment

例子:

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

    var body: some View {
        // 使用 managedObjectContext 进行 Core Data 操作
    }
}

2. .environmentObject(dataController)

使用 .environmentObject

例子:

class DataController: ObservableObject {
    @Published var someData: String = "Data"
}

struct ContentView: View {
    @EnvironmentObject var dataController: DataController

    var body: some View {
        Text(dataController.someData)
    }
}

区别总结:

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

示例结合

如果你有一个 DataController 管理 Core Data 的上下文,你可能会同时使用这两种方式:

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

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

这表明你可以同时使用 @Environment@EnvironmentObject,它们用于解决不同类型的数据传递问题。