为什么会出现 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 帮你写样板代码!