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 帮你写样板代码!

相关推荐
大龄秃头程序员2 小时前
Swift 并发实战:从 Task 到 Actor 重入性,我掉进了“逻辑不一致”的坑
swift
LinXunFeng3 小时前
给 Flutter 插件加上 Swift Package Manager 支持
flutter·swift
大龄秃头程序员8 小时前
Swift 面试反直觉:Property Wrapper 真的拿不到宿主 self 吗?
swift
FeliksLv9 小时前
从 +load 到 Swift Macros:自动注册的演进与实践
ios·objective-c·swift
语歌1 天前
AI 语言学习系统的工程边界:为什么 LLM 不该负责复习排期
人工智能·swift
2501_916008892 天前
iOS应用开发工具全面解析:如何选择与优化开发效率
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
大龄秃头程序员2 天前
Swift 属性包装器进阶:从“语法糖”到“并发安全”的 5 个深坑
swift
Lvan的前端笔记3 天前
SwiftUI:iOS 常用视图组件速查
ios·swiftui·swift
智购科技无人售货机工厂3 天前
自动售货机硬件选型避坑:单片机 / 树莓派 / ESP32 怎么选?~YH
人工智能·单片机·嵌入式硬件·r语言·swift·perl·symfony