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

相关推荐
2501_915106322 天前
Windows 用 KXBuild 开发 iOS 项目,CocoaPods、SPM 怎么处理?
windows·vscode·ios·个人开发·swift·敏捷流程·cocoapods
2501_916008893 天前
Windows 上写第一个 SwiftUI App,KXBuild 空工程起步,跑到 iPhone 真机
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
songgeb4 天前
iOS App支持设置RTL语言的实践
ios·swift·ui kit
2501_915106325 天前
KXApp 的 VS Code 真机调试:按下 Cmd+R 之后发生了什么
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
2601_955518185 天前
36. 跨语言:C/C++与Swift&Objective-C混合编程(一)
objective-c·swift·c/c++·混合编程·跨语言
东坡肘子6 天前
谁是 AI-relay App 的真正威胁 -- 肘子的 Swift 周报 #158
人工智能·swiftui·swift
叫Guava不是瓜娃7 天前
我用 Swift 写了个本地语音输入:按住说话,松开就是整理好的文字(开源)
swift
Yannik8 天前
iOS Swift Package Manager(SPM)私有组件完整实操
ios·swift
我就是不信8 天前
Swift 方法派发机制详解
开发语言·ios·swift
用户380341658829710 天前
八个私有 SPM 包撑起六款 App:分支当版本、钻石依赖,和一次谁都没报错的失效
ios·swift