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))
  }
}

因为 rgb 的有用取值规模为:[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
}

如上,对 rgb 取值维护的逻辑封装到了 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)"
  }
}

如上,RGBValueprojectedValue 界说为同类型的特点,并返回 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,并供给了 readwrite 辅佐办法,能够将「一大块」代码维护起来,如:

//                  
$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