@Observable 遇上属性包装器:一键绕过‘计算属性’禁令的 Swift 5.9 实战技巧

在 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 忽略观察,再挂包装器" ------ 问题秒解。

相关推荐
如此风景17 小时前
Swift异步详解
swift
HarderCoder18 小时前
强制 SwiftUI 重新渲染:`.id()` 这把“重启键”你用对了吗?
swift
HarderCoder19 小时前
Swift 6.2 新语法糖:在字符串插值里直接给 Optional 写默认值
swift
HarderCoder19 小时前
窥探 `@Observable` 的“小黑盒”:private 属性到底会不会被观察?
swift
zzywxc78719 小时前
AI 在金融、医疗、教育、制造业等领域有着广泛的应用,以下是这些领域的一些落地案例
人工智能·python·spring cloud·金融·swift·空间计算
HarderCoder20 小时前
Swift 并发避坑指南:自己动手实现“原子”属性与集合
swift
HarderCoder1 天前
惊!只是 `import Foundation`,`String.contains("")` 的返回值居然变了?
swift
HarderCoder1 天前
Swift 6.2 新武器:`weak let` —— 既弱引用又不可变的安全魔法
swift
HarderCoder1 天前
吃透 Swift 的 `@autoclosure`:把“表达式”变“闭包”的延迟利器
swift