ObservableObject 协议是 SwiftUI 早期的观察机制,Swift 5.9 引入 @Observable 宏作为 Swift 观察的"现代"解决方案。本文讲解从 ObservableObject 迁移到 @Observable。

@Observable 宏方案适配 iOS 17、iPadOS 17、macOS 14、tvOS 17和watchOS 10 及以上版本,在 App 支持 OS 版本允许的的情况下,应该优先使用 @Observable。

传统方案:ObservableObject

定义:

import Combine

class UserProfile: ObservableObject {
    @Published var name: String = "Chuan"
    @Published var age: Int = 25
    
    var displayName: String {
        "\(name) (\(age))"
    }
}
  1. 导入 Combine 框架;
  2. 必须遵循 ObservableObject 协议;
  3. 使用 @Published 标记可观察的属性;

使用:

struct ProfileView: View {
    @StateObject private var profile = UserProfile()
    // 或 @ObservedObject var profile: UserProfile
    
    var body: some View {
        VStack {
            Text(profile.name)
            Text("\(profile.age)")
        }
    }
}

使用时需要 @StateObject 或 @ObservedObject 修饰 ObservableObject 对象。

@Observable 方案

定义:

import Observation

@Observable 
class UserProfile {
    var name: String = "Chuan"
    var age: Int = 25
    
    var displayName: String {
        "\(name) (\(age))"
    }
}
  1. 导入 Observation 框架;
  2. 使用 @Observable 宏;
  3. 可观察属性不需要使用 @Published 标记,Swift 会自动追踪 body 视图中读取到的所有可观察属性。

使用:

struct ProfileView: View {
    let profile: UserProfile // 不再需要 @ObservedObject 或 @StateObject 包装器
    
    var body: some View {
        VStack {
            Text(profile.name) // 自动观察 name
            Text("\(profile.age)") // 自动观察 age
        }
    }
}

很明显可以看到:@Observable 方案大大简化了设计过程,不再需要 @Published、ObservableObject、@ObservedObject 了。

另外从性能上看,@Observable 也更具优势。@Observable 能精确至对象属性的变化来更新 view,而 ObservableObject 则是整个对象变化都会通知。

追踪机制对比:

class Counter: ObservableObject {
    @Published var count = 0
    @Published var name = "Counter"
}

let counter = Counter()

// 修改任何一个 @Published 属性都会触发整个对象的更新
counter.count += 1 // 触发更新
counter.name = "New Name" // 也触发更新,即使视图只使用 count

而 @Observable 方案:

@Observable class Counter {
    var count = 0
    var name = "Counter"
}

let counter = Counter()

// SwiftUI 只会更新实际使用特定属性的视图
// 如果视图只使用 count,修改 name 不会触发视图更新

environment 的变动

将 ObservableObject 对象添加到 environment 中必须使用 environmentObject:

@main
struct BookApp: App {
    @StateObject private var profile = UserProfile()

    var body: some Scene {
        WindowGroup {
            LibraryView()
                .environmentObject(profile)
        }
    }
}

@Observable 方案中不再需要 environmentObject:

@main
struct BookApp: App {
    @State private var profile = UserProfile()

    var body: some Scene {
        WindowGroup {
            LibraryView()
                .environment(profile)
        }
    }
}

对应的读取环境变量也从原来的:

struct LibraryView: View {
    @EnvironmentObject var profile: UserProfile

    var body: some View {
        UserView(profile: profile)
    }
}

变为:

struct LibraryView: View {
    @Environment(UserProfile.self) private var profile

    var body: some View {
        UserView(profile: profile)
    }
}

@Bindable

@Bindable 是与 @Observable 宏配合使用的属性包装器,它允许将 @Observable 类型的属性转换为可绑定引用,以便在绑定的视图中可以更改可观察属性的值。

// 父视图
struct ParentView: View {
    @State private profile = UserProfile()
    
    var body: some View {
        UserForm(profile: profile) // 这里 profile 自动变为 @Bindable
    }
}

// 子视图在参数位置接收 @Bindable
struct UserForm: View {
    @Bindable var profile: UserProfile
    
    var body: some View {
        Form {
            TextField("Name", text: $profile.name) // $profile.name 将同步到 ParentView
            TextField("Age", text: $profile.age)
        }
    }
}

参考:

Migrating from the Observable Object protocol to the Observable macro