SwiftUI深入理解@Environment原理
SwiftUI深入理解@Environment原理

SwiftUI深入理解@Environment原理

在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<Bool>类型。

可以通过手动创建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:https://fangjunyu.com/2024/12/23/swiftui%e7%8a%b6%e6%80%81%e7%ae%a1%e7%90%86%e6%9c%ba%e5%88%b6observable%e5%92%8cenvironment/

2、Swift environmentObject预览报错:https://fangjunyu.com/2024/10/24/swift-environmentobject%e9%a2%84%e8%a7%88%e6%8a%a5%e9%94%99/

3、Swift通过 @EnvironmentObjec共享和传递数据:https://fangjunyu.com/2024/10/23/swift%e9%80%9a%e8%bf%87-environmentobjec%e5%85%b1%e4%ba%ab%e5%92%8c%e4%bc%a0%e9%80%92%e6%95%b0%e6%8d%ae/

   

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开发者。

欢迎加入我们的 微信交流群QQ交流群,交流更多精彩内容!
微信交流群二维码 QQ交流群二维码

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注