@Observable 不能直接用于 Actor 类型

@Observable 可以作用于 class 和 struct,不能作用于 Actor。Actor 类型的底层实现与 @Observable 的状态观察逻辑存在冲突,比如 Actor 的状态访问需要异步传递,而 @Observable 依赖同步的属性访问来触发观察。Swift 编译器明确禁止将 @Observable 应用在 actor 声明的类型上。比如下方代码:

@Observable
actor ChuanActor {
}

编译期间就会报错:'@Observable' cannot be applied to actor type 'ChuanActor'。

解决办法一般是将 ChuanActor 中需要被观察的数据抽离到使用 @Observable 标记的结构体中。

@Observable + @MainActor

在实际开发中,在需要处理 UI 更新等场景时,@MainActor + @Observable 是非常常见的组合。@MainActor 属性包装器确保了被修饰的类型的属性访问、方法执行必须在主线程,而 @Observable 的状态观察本身也需要在主线程执行。两者的结合强化了线程安全:@Observable 负责状态观察,@MainActor 保证状态的读写和观察通知都在主线程执行,避免了多线程下的 UI 刷新问题。

对 @Observable 属性的 Binding

struct UserForm: View {
    @Environment(UserProfile.self) private var profile
    
    var body: some View {
        Form {
            TextField("Name", text: $profile.name)
            TextField("Age", text: $profile.age)
        }
    }
}

@Environment 获取的对象默认不是可绑定的,上述代码 $profile.name 这里就会报错。除了可以在子视图中将 @Observable 属性声明为 @Bindable:

struct UserForm: View {
    @Environment(UserProfile.self) private var profile
    
    var body: some View {
        UserFormSub(profile: $profile)
    }
}

struct UserFormSub: View {
    @Bindable var profile: UserProfile
    
    var body: some View {
        Form {
            TextField("Name", text: $user.name)
            TextField("Age", text: $user.eag)
        }
    }
}

还可以直接在当前视图内定义 @Bindable 变量:

struct UserForm: View {
    @Environment(UserProfile.self) private var profile
    
    var body: some View {
		// 使用 @Bindable 修饰
		@Bindable var bindableprofile = profile

        Form {
            TextField("Name", text: $bindableprofile.name)
            TextField("Age", text: $bindableprofile.age)
        }
    }
}

@AppStorage 和 @Observable 同时使用的问题

@Observable
final class Preference {
	@AppStorage("chuan_sort_by")  var sort_by: SortBy = .date_created
	@AppStorage("chuan_sort_order") var sort_order: SortOrder = .reverse
}

上面这段代码会报错:Invalid redeclaration of synthesized property。

原因:@Observable 宏会自动为所有存储属性生成观察功能,与 @AppStorage 的属性包装器机制存在冲突。

注意:此时不能使用 @ObservationIgnored 修饰 @AppStorage 属性来规避这个问题,会消除报错但会导致 @AppStorage 属性失去可观察性。

SwiftUI 中的 UserDefaults 与 Observation:如何实现精准响应 一文中,作者给出了两个解决方案:

1、不使用 @AppStorage,而是手动实现 get 和 set 方法以操作 UserDefaults

@Observable
class Settings {
    @ObservationIgnored
    var name: String {
        get {
            access(keyPath: \.name)
            return UserDefaults.standard.string(forKey: "name") ?? _nameDefault
        }
        set {
            withMutation(keyPath: \.name) {
                UserDefaults.standard.set(newValue, forKey: "name")
            }
        }
    }
}

2、使用作者开源的 ObservableDefaults

withObservationTracking

Swift Observation 框架同时还提供了一个 withObservationTracking 函数,开发者可以用它来跟踪 Observable 对象的属性是否发生变化。

public func withObservationTracking<T>(_ apply: () -> T, onChange: @autoclosure () -> @Sendable () -> Void) -> T

withObservationTracking 接受两个闭包参数:apply 和 onChange。使用时将需要监听的属性放在 apply 闭包中,如果这些属性发生改变其 onChange 闭包将会被调用。

import Observation

@Observable final class Counter {
    var count = 0
}

let counter = Counter()
func startObservation() {
    withObservationTracking {
        // 在 apply 闭包中访问需要观察的属性
        print("当前计数: \(counter.count)")
    } onChange: {
        // 属性变化时,onChange 会被调用
        print("检测到计数变化!")
        
        // 由于 onChange 默认只触发一次,我们递归调用以继续观察
        // 有时为了避免 onChange 在实际变化应用前立即运行,可以将其包装在 Task 中
        Task {
            startObservation()
        }
    }
}