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_9159184114 小时前
有没有Xcode 替代方案?在快蝎 IDE 中完成 iOS 开发的过程
ide·vscode·ios·个人开发·xcode·swift·敏捷流程
songgeb1 天前
Compositional layout in iOS
ios·swift·设计
1024小神1 天前
记录xcode项目swiftui配置APP加载启动图
前端·ios·swiftui·swift
wjm0410063 天前
ios学习路线-- swift基础2
学习·ios·swift
游戏开发爱好者83 天前
如何使用Instruments和Keymob进行Swift应用性能优化分析
开发语言·ios·性能优化·小程序·uni-app·iphone·swift
游戏开发爱好者85 天前
新的 iOS 开发工具体验,在快蝎 IDE 里完成应用开发与真机调试
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
东坡肘子5 天前
50 岁的苹果和 51 岁的我 -- 肘子的 Swift 周报 #127
人工智能·swiftui·swift
denggun123455 天前
Sendable 协议-Swift 结构化并发的核心安全保障
ios·swift
denggun123456 天前
结构化并发(Structured Concurrency)
开发语言·ios·swift
denggun123456 天前
Swift6.0为主版本小版本迭代
ios·swift