SwiftUI深入理解@Environment原理

49 阅读4分钟

在SwiftUI中,视图是一个“状态的函数”:

View = f(State)

只要数据变化,SwiftUI会自动重新计算依赖该数据的视图。

如果某者状态不是局部的(比如用户偏好、主题、App配置),也不想一层层的参数传递,如果想要子视图访问这个状态,就可以使用 Environment。

什么是Environment(环境)?

Environment是SwiftUI提供的一种依赖注入机制(Dependency Injection),允许上层视图将某个值放到一个全局的字典中。

所有子视图都可以读取这个值,而不用显式地传递参数。

这个字典在内部叫做 EnvironmentValues。

可以把它理解为:

struct EnvironmentValues {
    var colorScheme: ColorScheme
    var locale: Locale
    var openURL: OpenURLAction
    // ...
    // 还有单独注册的类型
}

@Environment工作方式

在SwiftUI中,当调用系统的环境变量时:

@Environment(.colorScheme) var colorScheme

编译器会自动生成访问代码:

1、视图被创建时,SwiftUI会从当前环境树中找到对应 key 的值;

2、将该值注入到这个属性;

3、如果上层环境发生改变(如用户切换浅色/深色模式),SwiftUI会重新生成视图;

这是最常见的KeyPath环境值方式,适用于内置值或EnvironmentValues扩展。

从 Swift 5.9 / iOS 17开始,Apple引入了新的API:

.environment(MyManager.self, myManager)
@Environment(MyManager.self) var manager

这是“类型化环境注入”,不同于早期的keyPath模式。

SwiftUI内部会构建一个类似的泛型容器:

Dictionary<ObjectIdentifier, Any>

当视图调用 @Environment(MyManager.self)时,SwiftUI就在环境树中查找ObjectIdentifier(MyManager.self)对应的值。

这个机制与旧式的 @Environment(.keyPath)并存。

Environment的生命周期与继承链

实例通常是由 @State 或 @StateObject 创建或持有的对象,SwiftUI 会管理它的生命周期。

@State private var sound = SoundManager.shared

在顶层视图中,通过 environment 或者 environmentObject 将实例注入到子视图的环境变量中。

@main
struct pigletApp: App {
    @State private var sound = SoundManager.shared
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .environment(sound)
    }
}

这样,子视图就可以通过 @Environment 读取注入的实例。

environment和environmentobject的使用区别在于:

在iOS 13+ 中,通过 @StateObject 创建的实例对象,只能使用 environmentobject 注入。

// 旧版本
@StateObject private var sound = SoundManager.shared
.environmentobject(sound)

在 iOS 17+中,通过 @State创建的实例对象,可以使用environment注入。

// 新版本
@State private var sound = SoundManager.shared
.environment(sound)

Environment注入到子视图后,子视图中的所有视图,都可以通过 @EnvironmentObject 获取该实例。

在子视图中,通过 @EnvironmentObject 获取该实例。

@EnvironmentObject var sound: SoundManager   // iOS 13+ 需要 ObservableObject

或者使用

@Environment(SoundManager.self) var sound   // iOS 17+

当实例注入到子视图中,所有的子视图都可以读取,即使是多层嵌套的子视图: ContentView > View1 > View2,View2仍然可以读取注入Content的环境变量。

总结

在子视图中,一共有三种读取环境变量的方法:

@Environment(.colorScheme) var colorScheme // iOS 13+ 获取系统环境变量
@EnvironmentObject var sound: SoundManager   // iOS 13+ 需要 ObservableObject
@Environment(SoundManager.self) var sound // iOS 17+

1、Environment()获取系统环境变量,可以访问SwiftUI内置的EnvironmentValues容器,并通过 KeyPath 获取值。适用于系统提供的环境变量(如colorScheme、locale、openURL)或自定义EnvironmentValues扩展。

2 、EnvironmentObject 适用于 iOS 13+,旧的Combine系统,基于Combine + ObservableObject的环境注入机制:

目标类型必须配合ObservableObject使用;

内部依赖Combine的objectWillChange通知;

使用时必须由父视图的 .environmentObject() 注入。

class AppStorageManager: ObservableObject {
    @Published var name = "Swift"
}

struct RootView: View {
    @StateObject private var manager = AppStorageManager()
    var body: some View {
        ChildView()
            .environmentObject(manager)
    }
}

struct ChildView: View {
    @EnvironmentObject var manager: AppStorageManager
    var body: some View {
        Text(manager.name)
    }
}

如果目标类型不使用ObservableObject,@EnvironmentObject获取环境变量时报错:

Generic struct 'EnvironmentObject' requires that 'AppStorageManager' conform to 'ObservableObject'

3 、Environmen(Type.self) 适用于 iOS 17+,这是Swift Observation框架(Swift 5.9)新加入的机制,完全不依赖Combine,也不需要 .environmentObject()。

目标类型必须标记 @Observable;

SwiftUI 自动跟踪属性访问,环境注入通过 .environment(::)传递。

@Observable
class AppStorageManager {
    var name = "Swift"
}

struct RootView: View {
    var body: some View {
        ChildView()
            .environment(AppStorageManager(), for: AppStorageManager.self)
    }
}

struct ChildView: View {
    @Environment(AppStorageManager.self) var manager
    var body: some View {
        Text(manager.name)
    }
}

EnvironmentObject和Environmen(Type.self) 在绑定对象方面存在一些区别。

EnvironmentObject可以直接绑定 Toggle 控件:

EnvironmentObject var appStorage: AppStorageManager
Toggle("",isOn: $appStorage.isModelConfigManager)

Environment(Type.self) 不支持直接绑定:

@Environment(AppStorageManager.self) var appStorage
Toggle("",isOn: $appStorage.isModelConfigManager) // 报错,Cannot find '$appStorage' in scope

这是因为Environment(Type.self)提供的是对象引用,而Toggle需要一个Binding类型。

可以通过手动创建Binding:

@Environment(AppStorageManager.self) var appStorage

Toggle("", isOn: Binding(
    get: { appStorage.isModelConfigManager },
    set: { appStorage.isModelConfigManager = $0 }
))

因为SwiftUI 17向下兼容 Combine模型,所以如果类是 @Observable,那么既可以使用@Environment(Type.self),也可以使用 EnvironmentObject,这样就可以实现属性绑定。此外,在一个项目中,也可以混用这两种体系。

相关文章

1、SwiftUI状态管理机制@Observable和@Environment:fangjunyu.com/2024/12/23/…

2、Swift environmentObject预览报错:fangjunyu.com/2024/10/24/…

3、Swift通过 @EnvironmentObjec共享和传递数据:fangjunyu.com/2024/10/23/…