Swift 作为现代、高效、安全的编程言语,其背面有很多高级特性为之支撑。
『 Swift 最佳实践 』系列对常用的言语特性逐个进行介绍,助力写出更简洁、更优雅的 Swift 代码,快速完成从 OC 到 Swift 的改变。
该系列内容主要包括:
- Optional
- Enum
- Closure
- Protocol
- Generics
- Property Wrapper
- Structured Concurrent
- Result builder
- Error Handle
- Advanced Collections (Asyncsequeue/OptionSet/Lazy)
- Expressible by Literal
- Pattern Matching
- Metatypes(.self/.Type/.Protocol)
ps. 本系列不是入门级语法教程,需求有必定的 Swift 基础
本文是系列文章的第六篇,介绍在 Swift 5.1 引进的 Property Wrapper (Swift-Evolution 0258 Property Wrappers)。
初识 Property Wrapper
A property wrapper adds a layer of separation between code that manages how a property is stored and the code that defines a property.
— Swift Dosc Property-Wrappers
简单讲,Property Wrapper 是对特点的一层封装,躲藏与特点相关的逻辑细节,提高代码的复用性。
// 界说 Property Wrapper
// SomePropertyWrapper 能够是 struct、enum 或 class
//
@propertyWrapper
struct SomePropertyWrapper {
var wrappedValue: Int
}
class SomeClass {
// 运用 Property Wrapper
//
@SomePropertyWrapper var a: Int = 1
}
如上,界说了一个最简单的 Property Wrapper SomePropertyWrapper
,几个要害点:
-
@propertyWrapper
,用于界说 Property Wrapper - Property Wrapper 的详细类型能够是 class、struct 或 enum (很少用)
- Property Wrapper 有必要有一个名为
wrappedValue
的特点 - 运用时,经过
@PropertyWrapperName
符号相关特点
有何用❓
假设,我们要界说一个 struct,用于存储 RBG 色值:
struct RGB {
let r: Int
let g: Int
let b: Int
init(r: Int, g: Int, b: Int) {
self.r = max(0, min(255, r))
self.g = max(0, min(255, g))
self.b = max(0, min(255, b))
}
}
因为 r
、g
、b
的有用取值规模为:[0, 255]
故,显式完成了 init
办法,并对每个值做了维护
如果用 PropertyWrapper 完成:
@propertyWrapper
struct RGBValue {
var value: Int = 0
var wrappedValue: Int {
get {
value
}
set {
value = max(0, min(255, newValue))
}
}
}
struct RGB {
@RGBValue var r: Int
@RGBValue var g: Int
@RGBValue var b: Int
}
如上,对 r
、g
、b
取值维护的逻辑封装到了 Property Wrapper 中
有利于代码复用、逻辑封装
内情
用 Property Wrapper 符号的特点 (如上述 a
) 编译器会自动合成相关代码:
class SomeClass {
@SomePropertyWrapper var a: Int = 1
}
// compiler synthesizes pseudo code ==>
class SomeClass {
//
private var _a = SomePropertyWrapper(wrappedValue: 1)
//
var a: Int {
get {
_a.wrappedValue
}
set {
_a.wrappedValue = newValue
}
}
}
- 编译器会生成一个 PropertyWrapper 类型的「存储特点」(如上
_a
) - 用 PropertyWrapper 符号的特点,实际上是个「核算特点」,是对 PropertyWrapper
wrappedValue
特点的代理
Initial
如上节所述,PropertyWrapper 必定是在界说时完成初始化的,如:
@SomePropertyWrapper var a: Int // no initial value
// ---->
private var _a = SomePropertyWrapper()
@SomePropertyWrapper var a: Int = 1 // has initial value
// ---->
private var _a = SomePropertyWrapper(wrappedValue: 1)
如上,依据有没有供给初始值分别调用不同的 init
办法:
- 没供给初始值时,调用 PropertyWrapper 的
init()
办法 - 供给了初始值,则调用
init(wrappedValue:)
办法
故,需求确保对应的 init
办法存在,否则编不过
除了,init()
、init(wrappedValue:)
,还能够供给更多自界说 init 办法,如:
@propertyWrapper
struct RGBValue {
var value: Int = 0
var minValue: Int
var maxValue: Int
init(minValue: Int, maxValue: Int) {
self.minValue = minValue
self.maxValue = maxValue
}
init(wrappedValue: Int, minValue: Int, maxValue: Int) {
self.minValue = minValue
self.maxValue = maxValue
value = max(minValue, min(wrappedValue, maxValue))
}
var wrappedValue: Int {
// ...
}
}
struct RGB {
// 调用 init(minValue: Int, maxValue: Int)
//
@RGBValue(minValue: 100, maxValue: 200) var r: Int
// 调用 init(wrappedValue: Int, minValue: Int, maxValue: Int)
//
@RGBValue(wrappedValue: 10, minValue: 0, maxValue: 100) var g: Int
// 调用 init(wrappedValue: Int, minValue: Int, maxValue: Int)
// 留意 ⚡️:关于这种写法,wrappedValue: 有必要是 init 的第一个参数
//
@RGBValue(minValue: 200, maxValue: 255) var b: Int = 2
}
Projected Value
Property Wrapper 除了对外曝露 wrappedValue
,还能够曝露一个值,称之为 Projected Value,如:
@propertyWrapper
struct RGBValue {
var value: Int = 0
//
private(set) var projectedValue: Bool = false
var wrappedValue: Int {
get {
value
}
set {
projectedValue = newValue <= 255 && newValue >= 0
value = max(0, min(255, newValue))
}
}
}
struct RGB {
@RGBValue var r: Int
@RGBValue var g: Int
@RGBValue var b: Int
func someFunc() {
//
let x = $r
}
}
-
projectedValue
能够是恣意类型 -
projectedValue
能够是存储特点,也能够是核算特点 - 运用时,只需在原始特点名前加上
$
(如$r
)
关于 Projected Value 更常见的用法是,直接返回 self
,并借此调用 Property Wrapper 供给的辅佐能力,如:
@propertyWrapper
struct RGBValue {
var value: Int = 0
//
var projectedValue: RGBValue { self }
var wrappedValue: Int {
get {
value
}
set {
value = max(0, min(255, newValue))
}
}
//
var hex: String {
String(format:"%02X", value)
}
}
struct RGB {
@RGBValue var r: Int
@RGBValue var g: Int
@RGBValue var b: Int
func hexRGB() -> String {
//
"#($r.hex)($g.hex)($b.hex)"
}
}
如上,RGBValue
将 projectedValue
界说为同类型的特点,并返回 self
一起,RGBValue
供给了转十六进制的辅佐核算特点:hex
运用时,经过 projectedValue
就能够访问到 hex
($r.hex
)
运用
关于 Property Wrapper 的运用,「只有想不到,没有做不到」,是一个充满想象力和创造力的地方!
SwiftUI
SwiftUI 供给了大量的 Property Wrapper,能够说 Property Wrapper 是为 SwiftUI 而生,离开它们步履维艰,如:
- @State
- @Binding
- @StateObject
- @ObservedObject
- @EnvironmentObject
- …
Protected
线程安全是一个常见且处理繁碎、容易出错的问题
经过 Property Wrapper 能够很好地将这些逻辑封装起来,极大简化了事务上的处理
GitHub – Alamofire 中供给了相关的 Property Wrapper (代码略有删简):
/// A thread-safe wrapper around a value.
@propertyWrapper
final class Protected<T> {
private let lock = UnfairLock()
private var value: T
init(_ value: T) {
self.value = value
}
var wrappedValue: T {
get { lock.around { value } }
set { lock.around { value = newValue } }
}
//
var projectedValue: Protected<T> { self }
init(wrappedValue: T) {
value = wrappedValue
}
func read<U>(_ closure: (T) throws -> U) rethrows -> U {
try lock.around { try closure(self.value) }
}
@discardableResult
func write<U>(_ closure: (inout T) throws -> U) rethrows -> U {
try lock.around { try closure(&self.value) }
}
}
关于需求线程安全维护的特点,在界说时只需加上 @Protected
即可,lock/unlock 之类的问题一概不用操心:
@Protected var validators: [() -> Void] = []
一起,将 projectedValue
指向 self
,并供给了 read
、write
辅佐办法,能够将「一大块」代码维护起来,如:
//
$multipartFormData.read { multipartFormData in
//
urlRequest.headers.add(.contentType(multipartFormData.contentType))
}
$mutableState.write { state in
state.listenerQueue = queue
state.listener = listener
}
Codable
Swift 供给了原生的 JSON 解析能力 Codable
,但也有一些约束,如不能供给默认值、Lossless value 转换 (如 JSON 里是个 String
,但 Model 中是个 Int
)、Array 解析时只需有一个元素解析失利整个 Array 解析就失利等。
这些问题都需求经过手动方式解决,不够友好
GitHub – BetterCodable 经过 Property Wrapper 较好地解决了这些问题,如:
struct Response: Codable {
@LosslessValue var sku: String
@LosslessValue var isAvailable: Bool
}
let json = #"{ "sku": 12345, "isAvailable": "true" }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Response.self, from: json)
print(result) // Response(sku: "12355", isAvailable: true)
User defaults
开发中有不少场景需求运用 UserDefaults
存储信息,相关的读写操作能够封装到 Property Wrapper 中,如:
@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
enum GlobalSettings {
@UserDefault(key: "FOO_FEATURE_ENABLED", defaultValue: false)
static var isFooFeatureEnabled: Bool
@UserDefault(key: "BAR_FEATURE_ENABLED", defaultValue: false)
static var isBarFeatureEnabled: Bool
}
Clamping
如上介绍的 RGBValue
,日常开发中有很多值有有用取值区间,如:RGB、age、 weekday、fps 等
能够将有用取值区间封装到一个 Property Wrapper 中,如:
@propertyWrapper
struct Clamping<WrappedValue: Comparable> {
let range: ClosedRange<WrappedValue>
var value: WrappedValue
init(wrappedValue value: WrappedValue, _ range: ClosedRange<WrappedValue>) {
self.value = value
self.range = range
}
var wrappedValue: WrappedValue {
get { value }
set { value = min(max(range.lowerBound, newValue), range.upperBound)}
}
}
struct RGB {
@Clamping(0...100) var r: Int = 0
@Clamping(0...255) var g: Int = 0
@Clamping(100...255) var b: Int = 255
}
…
约束
用 Property Wrapper 符号的特点,有一些约束:
- 只能是 var,不能是 let
- 不能是 lazy
- 不能是 weak
小结
合理的封装 Property Wrapper ,能够提高代码的复用性,以及简化事务运用。
参考资料
Swift Dosc Property-Wrappers
Swift-Evolution 0258 Property Wrappers
Swift Property Wrappers – NSHipster
Property wrappers in Swift
What is a Property Wrapper in Swift