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

相关推荐
songgeb2 天前
YYImage Bug:一行之差,正常 JPEG 如何在 iOS 被读成条纹
ios·swift
fthux2 天前
Tick Rest,一个休息提醒工具的自我修养
macos·开源·swift
Fatbobman(东坡肘子)3 天前
快速消失的硬盘容量 -- 肘子的 Swift 周报 #160
开发语言·人工智能·redis·ios·swiftui·iphone·swift
chaoahyl3 天前
WaterDiary 喝水记录 App 实战(2)· 实现喝水首页与本地保存
swift
茶底世界之下4 天前
iPhone Duo 适配实战,Awaking 踩过的 8 个坑
ios·swiftui·swift
xbzb4 天前
Swift 对象存储建环实验 + Ceilometer 计量:两个收尾服务
swift·ring·实验·ceilometer·象存储·建环
2501_916008894 天前
iOS App 图标、启动屏和多语言搭配 方法,上架前使用KXBuild一次做好 App 图标
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
2501_915918414 天前
iOS 推送通知怎么实现,从 APNs p8 密钥到 iOS 真机收到第一条推送
vscode·ios·objective-c·个人开发·swift·敏捷流程
东坡肘子4 天前
快速消失的硬盘容量 -- 肘子的 Swift 周报 #160
人工智能·swiftui·swift
2501_916007476 天前
Windows OpenSSL 生成 iOS 证书,制作 p12 与描述文件,KXBuild 签名实操
windows·vscode·ios·objective-c·个人开发·swift·敏捷流程