@EnvironmentObject 那些事

10 阅读2分钟

先理解 @EnvironmentObject 解决什么问题

假设你的 App 里有一个登录用户的信息,很多层级的 View 都需要用到它:

ContentView
 └── HomeView
       └── FeedView
             └── PostView
                   └── AuthorLabel  ← 这里需要用户信息

@ObservedObject 的话,你需要把 user 一层一层手动往下传:

ContentView(user: user)
  HomeView(user: user)
    FeedView(user: user)
      PostView(user: user)
        AuthorLabel(user: user)  // 终于到了

中间那三层根本用不到 user,却被迫声明和传递它。 这叫 prop drilling(属性钻透),非常痛苦。


@EnvironmentObject 就是用来解决这个问题的

把数据对象注入到环境里,任何子孙 View 都可以直接从环境里取,不需要逐层传递。

// 数据模型(和 @ObservedObject 一样需要 ObservableObject)
class UserSession: ObservableObject {
   @Published var userName: String = "Tom"
   @Published var isLoggedIn: Bool = true
}

// 在顶层注入环境
@main
struct MyApp: App {
   @StateObject var session = UserSession()

   var body: some Scene {
       WindowGroup {
           ContentView()
               .environmentObject(session)  // 注入!所有子孙 View 都能取到
       }
   }
}

// 任意层级的子 View 直接取用,不需要父 View 传递
struct AuthorLabel: View {
   @EnvironmentObject var session: UserSession  // 直接从环境里拿

   var body: some View {
       Text("作者:\(session.userName)")
   }
}

中间的 HomeView、FeedView、PostView 完全不需要知道 UserSession 的存在。


@EnvironmentObject 的本质

SwiftUI 的 View 树本质上维护了一个按类型索引的环境字典

环境字典:{
   UserSession.self → <UserSession 实例>,
   ThemeSettings.self → <ThemeSettings 实例>,
   ...
}

.environmentObject(session) 做的事情是:把 sessionUserSession.self 为 key,写入当前节点及所有子孙节点的环境字典。

@EnvironmentObject var session: UserSession 做的事情是:在 View 初始化时,从环境字典里用 UserSession.self 查找,找到了就绑定,找不到就运行时崩溃


使用 @EnvironmentObject 时需要关心的问题

  1. 忘记注入会直接崩溃@EnvironmentObject 是运行时查找,不是编译期保证。如果你在 View 里声明了 @EnvironmentObject,但没有在祖先 View 里调用 .environmentObject(),App 会直接崩溃,且错误信息不太友好,调试时要特别注意。

  2. 按类型区分,每种类型只能注入一个:环境字典的 key 是类型本身(UserSession.self),所以同一个类型注入两次,下层的会覆盖上层的。如果你有两个相同类型但不同用途的对象,需要把它们包成不同的类型。

  3. 不要滥用@EnvironmentObject 适合真正的全局共享状态,比如登录信息、主题、语言设置。对于只在局部几个 View 间共享的数据,还是老实用 @ObservedObject 传参,环境是全局的,滥用会让数据流向变得不透明,难以维护。

  4. Preview 里需要手动注入:Xcode Preview 不走 App 的初始化流程,所以凡是用了 @EnvironmentObject 的 View,Preview 里需要手动加 .environmentObject(),否则 Preview 也会崩溃。

#Preview {
   AuthorLabel()
       .environmentObject(UserSession())  // Preview 里必须手动注入
}