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

相关推荐
东坡肘子2 天前
毕业 30 年同学群:一场 AI 引发的“真假难辨”危机 -- 肘子的 Swift 周报 #112
人工智能·swiftui·swift
Antonio9152 天前
【Swift】UIKit:UIAlertController、UIImageView、UIDatePicker、UIPickerView和UISwitch
ios·cocoa·swift
Antonio9153 天前
【Swift】UIKit:UISegmentedControl、UISlider、UIStepper、UITableView和UICollectionView
开发语言·ios·swift
1***81533 天前
Swift在服务端开发的可能性探索
开发语言·ios·swift
S***H2833 天前
Swift在系统级应用中的开发
开发语言·ios·swift
HarderCoder3 天前
SwiftUI 状态管理极简之道:从“最小状态”到“状态树”
swift
Antonio9153 天前
【Swift】 UIKit:UIGestureRecognizer和UIView Animation
开发语言·ios·swift
蒙小萌19933 天前
Swift UIKit MVVM + RxSwift Development Rules
开发语言·prompt·swift·rxswift
Antonio9154 天前
【Swift】Swift基础语法:函数、闭包、枚举、结构体、类与属性
开发语言·swift
Antonio9154 天前
【Swift】 Swift 基础语法:变量、类型、分支与循环
开发语言·swift