在 Swift 5.9 引入的 @Observable
宏(Observable framework)让"全部属性默认被观察"成为可能,但也带来了一个副作用:
被宏展开后,所有存储属性都变成了计算属性,于是再给它们加自定义 Property Wrapper(如
@Injected
、@UserDefault
等)就会直接报错:
vbnet
Property wrapper cannot be applied to a computed property
下面给出原因剖析 + 最小修复示例 + 两种长期策略,让你既能享受 @Observable
的简洁,又能继续用属性包装器做 DI、缓存、格式化等横切逻辑。
先复现问题
swift
@Observable
final class ViewModel {
var data: [String] = []
@Injected // ❌ 编译失败
private var dataProvider: ProviderProtocol
}
展开后的大致伪代码:
swift
final class ViewModel: Observable {
var data: [String] {
get { _storage.data } // 计算属性
set { _storage.data = newValue }
}
// 同样,dataProvider 也被转成计算属性 → 无法附加 @Injected
}
最小修复:@ObservationIgnored
苹果提供了忽略观察的宏:@ObservationIgnored
作用:让 @Observable
不要把该属性转成计算属性,保持原样(存储属性),于是就能继续挂 Property Wrapper。
swift
@Observable
final class ViewModel {
var data: [String] = []
@ObservationIgnored // ✅ 1. 先忽略观察
@Injected // ✅ 2. 再挂自定义包装器
private var dataProvider: ProviderProtocol
func loadData() {
data = dataProvider.getData()
}
}
- 观察链:
data
变化仍能触发 SwiftUI 刷新; - 注入链:
dataProvider
仍是@Injected
托管的存储属性; - 零成本:
@ObservationIgnored
编译期生效,运行期无额外开销。
原理速览:宏 vs 属性包装器
维度 | Property Wrapper | Macro(@Observable、@ObservationIgnored) |
---|---|---|
执行时机 | 运行期 | 编译期 |
产物 | 生成存储 + 计算属性 | 生成计算属性 + 观测逻辑 |
能否叠加 | ❌ 计算属性上不能再挂包装器 | ✅ 宏可组合 |
典型用途 | 注入、缓存、格式化 | 观察、代码生成 |
结论:属性包装器与宏不是替代关系,而是互补工具;冲突时先用宏"放行",再用包装器"加功能"。
长期策略
策略 A:包装器内移------把 @Injected
放到下层类型
swift
@Observable
final class ViewModel {
var data: [String] = []
// 不再直接注入,而是持有一个"已注入"的对象
private let repo = DataRepository() // 内部已用 @Injected
}
- 优点:ViewModel 代码干净,100 % 被观察
- 缺点:需要多一层类型
策略 B:自定义宏------用 SwiftSyntax 写 @InjectedMacro
swift
@Observable
final class ViewModel {
var data: [String] = []
// 未来可能出现官方或第三方注入宏
#Injected(.singleton)
private var dataProvider: ProviderProtocol
}
- 优点:编译期展开,无运行时反射,性能更好
- 缺点:目前需自己实现
实战小结(Copy-Paste 模板)
swift
import Observation
import Foundation
// 1. 定义注入协议(示例)
protocol ProviderProtocol {
func getData() -> [String]
}
// 2. 自定义 Property Wrapper
@propertyWrapper
struct Injected<T> {
var wrappedValue: T
init() {
// 简单演示:从全局容器解析
self.wrappedValue = DIContainer.shared.resolve()
}
}
// 3. 可观察模型
@Observable
final class ViewModel {
var data: [String] = []
@ObservationIgnored // ← 关键:忽略观察
@Injected // ← 继续用包装器
private var dataProvider: ProviderProtocol
func loadData() {
data = dataProvider.getData()
}
}
一句话记住
只要看到 "Property wrapper cannot be applied to a computed property"
立刻想:"先 @ObservationIgnored
忽略观察,再挂包装器" ------ 问题秒解。