Swift Property Wrapper:优雅地消除样板代码

原文链接:www.avanderlee.com/swift/prope...

为什么会出现 Property Wrapper?

在业务代码里,我们经常写出大量 重复的模式:

swift 复制代码
var username: String {
    get { UserDefaults.standard.string(forKey: "username") ?? "guest" }
    set { UserDefaults.standard.set(newValue, forKey: "username") }
}

当属性越来越多时,样板代码 呈指数级增长。

Apple 在 WWDC 2019 引入 Property Wrapper(SE-0258),把"如何存取值"这一横切关注点抽象出来,封装成可复用的 包装类型。

什么是 Property Wrapper?

Property Wrapper 是一个带 @propertyWrapper 标注的结构体/类,它决定了被包装属性的存储与读取方式。

核心必须实现:

swift 复制代码
@propertyWrapper
struct Wrapper<T> {
    var wrappedValue: T   // 真正读写的值
    // 可选:var projectedValue: SomeType  // 投影值,用于暴露更多能力
}

实战:UserDefaults 的 Property Wrapper

传统写法(痛点)

swift 复制代码
extension UserDefaults {
    enum Keys {
        static let hasSeenAppIntroduction = "has_seen_app_introduction"
    }

    var hasSeenAppIntroduction: Bool {
        get { bool(forKey: Keys.hasSeenAppIntroduction) }
        set { set(newValue, forKey: Keys.hasSeenAppIntroduction) }
    }
}

缺点

  • 每个属性都要写一遍 get / set
  • Key 字符串散落各处,易错

封装成 @UserDefault

swift 复制代码
@propertyWrapper
struct UserDefault<Value> {
    let key: String
    let defaultValue: Value
    var container: UserDefaults = .standard

    var wrappedValue: Value {
        get {
            container.object(forKey: key) as? Value ?? defaultValue
        }
        set {
            container.set(newValue, forKey: key)
        }
    }
}

使用:

swift 复制代码
extension UserDefaults {
    @UserDefault(key: "has_seen_app_introduction", defaultValue: false)
    static var hasSeenAppIntroduction: Bool

    @UserDefault(key: "username", defaultValue: "Antoine")
    static var username: String
}

一行即可声明,零样板!

进阶:支持可选值 & 移除 Key

Swift 的泛型不支持"可选与非可选"同时满足,需要一点技巧:

swift 复制代码
public protocol AnyOptional {
    var isNil: Bool { get }
}
extension Optional: AnyOptional {
    public var isNil: Bool { self == nil }
}

改造 setter:

swift 复制代码
var wrappedValue: Value {
    get { ... }
    set {
        if let optional = newValue as? AnyOptional, optional.isNil {
            container.removeObject(forKey: key)   // 置 nil 时删除
        } else {
            container.set(newValue, forKey: key)
        }
    }
}

于是支持:

swift 复制代码
@UserDefault(key: "year_of_birth")
static var yearOfBirth: Int?

UserDefaults.yearOfBirth = nil   // 自动删除 key

Projected Value:把属性变成 Publisher

有时我们想 监听 变化,Combine 友好:

swift 复制代码
import Combine

@propertyWrapper
struct UserDefault<Value> {
    ...
    private let publisher = PassthroughSubject<Value, Never>()

    var wrappedValue: Value { ... publisher.send(newValue) }

    var projectedValue: AnyPublisher<Value, Never> {
        publisher.eraseToAnyPublisher()
    }
}

订阅:

swift 复制代码
let cancellable = UserDefaults.$username.sink {
    print("用户名变为:\($0)")
}
UserDefaults.username = "新名字"
// 控制台:用户名变为:新名字

访问包装器本体

Swift 预留了两个魔法前缀:

前缀 说明 示例
_ 直接访问包装器实例 _username.key
$ 访问 projectedValue $username(即 AnyPublisher)
swift 复制代码
extension UserDefaults {
    static func debugKeys() {
        print(_username.key)   // "username"
        print($username)       // AnyPublisher
    }
}

在函数参数里用 Property Wrapper

swift 复制代码
@propertyWrapper
struct Debuggable<Value> {
    init(wrappedValue: Value, description: String = "") { ... }
    var wrappedValue: Value { ... }
}

func animate(@Debuggable(description: "动画时长") duration: Double) {
    UIView.animate(withDuration: duration) { ... }
}

animate(withDuration: 2.0)
// 控制台:
// Initialized '动画时长' with value 2.0
// Accessing '动画时长', returning: 2.0

调试神器!

更多灵感

  • @UsesAutoLayout var label = UILabel()

    自动把 translatesAutoresizingMaskIntoConstraints = false

  • @SampleFile(fileName: "avatar.jpg") var avatarURL: URL

    一键获取 Bundle 内资源 URL

小结

能力 传统写法 Property Wrapper
去除重复 ❌ ✅
类型安全 ❌ ✅
可测试性 ❌ ✅
组合能力(Combine、Debug...) ❌ ✅

一句话总结:

只要发现属性读写有重复模式,就考虑封装成 Property Wrapper,让 Swift 帮你写样板代码!

相关推荐
茶底世界之下7 小时前
Metal 冷启动并发下,Pipeline Cache 为什么必须合并在途编译
ios·swift
黑科技iOS上架1 天前
双引擎解决oc/swift开发引用加固难题
开发语言·ios·swift·审核·ios混淆·执行程序差异化
传奇开心果编程3 天前
【SwiftUI提高练中学】第13课 实时活动与灵动岛:ActivityKit 实战
学习·ui·ios·swiftui·swift
AC赳赳老秦3 天前
数据采集全链路审计留痕:用 OpenClaw 实现合规审计与追溯
开发语言·汇编·python·php·swift·deepseek·openclaw
传奇开心果编程3 天前
【SwiftUI入门练中学】第17课 通知与后台任务
学习·ui·ios·swiftui·swift
逐流人4 天前
Ceph对象存储与文件系统存储管理:从RADOS网关、S3与Swift到CephFS快照同步
linux·运维·ceph·云原生·云计算·swift·rados
传奇开心果编程5 天前
【SwiftUI入门练中学】第7课 动画与手势:让界面更灵动
学习·macos·ui·ios·swiftui·swift
传奇开心果编程6 天前
【SwiftUI娓娓道来】第3课:让界面活起来——交互与动画指南
学习·macos·ui·ios·swiftui·swift
用户38034165882976 天前
控制中心一键后台录音:我被自己的 Spike 骗了两次
ios·swift