回顾 Codable

先来看一个常见的 Decode 场景。

场景一、

struct NormalDecodableStruct: Decodable {
    var title: String = ""
    var isNew: Bool = false
}
func testNormalDecodableStruct() {
    let json = """
    {
        "isNew": 0
    }
    """
    guard let data = json.data(using: .utf8) else { return }
    do {
        let decoder = JSONDecoder()
        let normalStruct = try decoder.decode(NormalDecodableStruct.self, from: data)
        XCTAssertEqual(normalStruct.title, "")
        XCTAssertEqual(normalStruct.isNew, false)
    } catch {
        print(error)
        XCTAssertThrowsError(error)
    }
}

Log: keyNotFound(CodingKeys(stringValue: “title”, intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: “No value associated with key CodingKeys(stringValue: “title”, intValue: nil) (“title”).”, underlyingError: nil))

经过 Log 发现首先是报错了短少 title key 的过错, 尝试改下代码。

改善 +1

struct NormalDecodableStruct2: Decodable {
    var title: String?
    var isNew: Bool?
}
func testNormalDecodableStruct2() {
    let json = """
    {
        "isNew": 0
    }
    """
    guard let data = json.data(using: .utf8) else { return }
    do {
        let decoder = JSONDecoder()
        let normalStruct = try decoder.decode(NormalDecodableStruct2.self, from: data)
        XCTAssertEqual(normalStruct.title, nil)
        XCTAssertEqual(normalStruct.isNew, nil)
    } catch {
        print(error)
        XCTAssertThrowsError(error)
    }
}

Log: typeMismatch(Swift.Bool, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: “isNew”, intValue: nil)], debugDescription: “Expected to decode Bool but found a number instead.”, underlyingError: nil))

发现直接报错 isNew, 而没有先报错 title, 阐明短少 key 是能够经过可选值的办法来进行解码的, 可是类型不匹配的问题仍然没有处理。

假如是接口返回的,咱们只好和后台同学去约好好类型一定要严格依照文档给出,否则咱们这儿会发生 Crash 的问题。可是即便是约好好了,发现使用起来会极度的不方便,因为每个字段在使用时都需求解包处理。

改善 +2

struct NormalDecodableStruct3: Decodable {
    var title: String = ""
    var isNew: Bool = false
    enum CodingKeys: CodingKey {
        case title
        case isNew
    }
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.title = try container.decodeIfPresent(String.self, forKey: .title) ?? ""
        self.isNew = try container.decodeIfPresent(Bool.self, forKey: .isNew) ?? false
    }
}
func testNormalDecodableStruct3() {
    let json = """
    {
        "isNew": 0
    }
    """
    guard let data = json.data(using: .utf8) else { return }
    do {
        let decoder = JSONDecoder()
        let normalStruct = try decoder.decode(NormalDecodableStruct3.self, from: data)
        XCTAssertEqual(normalStruct.title, "")
        XCTAssertEqual(normalStruct.isNew, false)
    } catch {
        print(error)
        XCTAssertThrowsError(error)
    }
}

Log: typeMismatch(Swift.Bool, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: “isNew”, intValue: nil)], debugDescription: “Expected to decode Bool but found a number instead.”, underlyingError: nil))

发现虽然类型不匹配的问题仍然存在, 不过至少在类型匹配时不用写可选 ? 了。。。

改善 +3

struct NormalDecodableStruct4: Decodable {
    var title: String = ""
    var isNew: Bool = false
    enum CodingKeys: CodingKey {
        case title
        case isNew
    }
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.title = (try? container.decode(String.self, forKey: .title)) ?? ""
        self.isNew = (try? container.decode(Bool.self, forKey: .isNew)) ?? false
    }
}
func testNormalDecodableStruct4() {
    let json = """
    {
        "isNew": 0
    }
    """
    guard let data = json.data(using: .utf8) else { return }
    do {
        let decoder = JSONDecoder()
        let normalStruct = try decoder.decode(NormalDecodableStruct4.self, from: data)
        XCTAssertEqual(normalStruct.title, "")
        XCTAssertEqual(normalStruct.isNew, false)
    } catch {
        print(error)
        XCTAssertThrowsError(error)
    }
}

这次终于看到了久别的 Test Succeed, 短少 key 或许 value 类型不匹配的问题得到了处理。

可是冷静下来再看下, 怎么说呢, 一点也不高雅 咱们想要的是像 NormalDecodableStruct 一样单纯的。

struct NormalDecodableStruct: Decodable {
    var title: String = ""
    var isNew: Bool = false
}

那么有更好的方案吗,有!!!

接下来看下经过 @propertyWrapper 如何来高雅的处理上边遇到的问题。

@propertyWrapper

首先来看下经过 @propertyWrapper 改写后的样子。

示例

struct WrapperDecodableStruct: Decodable {
    @Default<String> var title: String = ""
    @Default<Bool> var isNew: Bool = false
}
func testWrapperDecodableStruct() {
    let json = """
    {
        "isNew": 0
    }
    """
    guard let data = json.data(using: .utf8) else { return }
    do {
        let decoder = JSONDecoder()
        let normalStruct = try decoder.decode(WrapperDecodableStruct.self, from: data)
        XCTAssertEqual(normalStruct.title, "")
        XCTAssertEqual(normalStruct.isNew, false)
    } catch {
        print(error)
        XCTAssertThrowsError(error)
    }
}

Test Succeed

这已经十分挨近咱们所希望的样子了,也不需求为每个 类/结构体 重写 decode 办法了。

那么问题来了,@Default<T> 做了什么呢?

@Default<T>

// MARK: - DefaultValue Protocol
protocol DefaultValue {
    associatedtype Result: Codable
    static var defaultValue: Result { get }
}
extension String: DefaultValue {
    static let defaultValue = ""
}
extension Bool: DefaultValue {
    static let defaultValue = false
}
// MARK: - @propertyWrapper
@propertyWrapper
struct Default<T: DefaultValue> {
    var wrappedValue: T.Result
}
extension Default: Codable {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        wrappedValue = (try? container.decode(T.Result.self)) ?? T.defaultValue
    }
}
extension KeyedDecodingContainer {
    func decode<T>(_ type: Default<T>.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> Default<T> where T: DefaultValue {
        try decodeIfPresent(type, forKey: key) ?? Default(wrappedValue: T.defaultValue)
    }
}

这儿呈现了 singleValueContainer(), 并经过拓宽对 Default<T> 类型进行了支持。

看下官方对 SingleValueDecodingContainer 的解释:

A container that can support the storage and direct decoding of a single nonkeyed value.

支持存储和直接解码单个非键值的容器。

在断点调试的时分你或许会发现特点从 String -> Default<String>

<img src="https://www.6hu.cc/wp-content/uploads/2024/02/232714-BuaA6o.png” loading=”lazy”>

可是获取的时分却是能够直接取到想要的值的, 原因是什么呢?

原理

propertyWrapper 能够理解成一组特别的 gettersetter, 其本身是一个特别的盒子,把本来值的类型包装了进去。被 propertyWrapper 声明的特点,实际上在存储时的类型是 propertyWrapper 这个盒子的类型,只不过编译器施了一些魔法,让它对外暴露的类型仍然是被包装的本来的类型。

ps: wrappedValue 并不是随意界说的特点称号, 而是必须完成的。

看下官方文档中供给的示例:

@propertyWrapper
struct TwelveOrLess {
    private var number = 0
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}
struct SmallRectangle {
    @TwelveOrLess var height: Int
    @TwelveOrLess var width: Int
}

当包装器应用于特点时,编译器会合成为包装器供给存储的代码和经过包装器供给对特点的拜访的代码。

咱们现在已经经过 @propertyWrapper 处理了首要用于接口数据解析的典型问题, 或许你也已经想到了其实还遗漏了一个枚举类型。接下来看下场景二。

场景二、

struct EnumDecodableStruct: Decodable {
  enum State: String, Codable {
    case prepare, playing
  }
  var state: State = .prepare
}
func testEnumDecodableStruct() {
    let json = """
    {
        "state": "loading"
    }
    """
    guard let data = json.data(using: .utf8) else { return }
    do {
        let decoder = JSONDecoder()
        let normalStruct = try decoder.decode(EnumDecodableStruct.self, from: data)
        XCTAssertEqual(normalStruct.state, .prepare)
    } catch {
        print(error)
        XCTAssertThrowsError(error)
    }
}

Log: dataCorrupted(Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: “state”, intValue: nil)], debugDescription: “Cannot initialize State from invalid String value loading”, underlyingError: nil))

咱们希望将接收到的 state 字符串直接关联到枚举特点对应的 case 上, 可是接收到的是一个未声明的 case 类型, 这种场景在迭代进程中是比较简单呈现的。

经过 Log 发现 state 没有被成功解析。与上边相同的代码改善办法这儿就不再展示了, 有爱好的能够下载工程去做测试

改善 +1

struct EnumDecodableStruct4: Decodable {
  enum State: String, Codable {
    case prepare, playing
    init(rawValue: String) {
      switch rawValue {
      case "prepare": self = .prepare
      case "playing": self = .playing
      default: self = .prepare
      }
    }
  }
  var state: State = .prepare
}
func testEnumDecodableStruct4() {
    let json = """
    {
        "state": "loading"
    }
    """
    guard let data = json.data(using: .utf8) else { return }
    do {
        let decoder = JSONDecoder()
        let normalStruct = try decoder.decode(EnumDecodableStruct4.self, from: data)
        XCTAssertEqual(normalStruct.state, .prepare)
    } catch {
        print(error)
        XCTAssertThrowsError(error)
    }
}

Test Succeed

这种办法是彻底没有问题的, 可是坏处也很明显, 假如 case 较多, 对应起来仍是比较费事的。

改善 +2

struct WrapperEnumDecodableStruct: Decodable {
  enum State: String, Codable {
    case prepare, playing
  }
  @Default<State> var state: State = .prepare
}
extension WrapperEnumDecodableStruct.State: DefaultValue {
  static let defaultValue: WrapperEnumDecodableStruct.State = .prepare
}
func testWrapperEnumDecodableStruct() {
    let json = """
    {
        "state": "loading"
    }
    """
    guard let data = json.data(using: .utf8) else { return }
    do {
        let decoder = JSONDecoder()
        let normalStruct = try decoder.decode(WrapperEnumDecodableStruct.self, from: data)
        XCTAssertEqual(normalStruct.state, .prepare)
    } catch {
        print(error)
        XCTAssertThrowsError(error)
    }
}

Test Succeed

这儿能够看到咱们对枚举类型的拓宽直接复用了 DefaultValue 协议, 让其像 String 一样完成 defaultValue 即可, 仍是比较简单理解的。

还能够做什么

本地 Data 数据解析

当数据模型中添加了新的字段, 但本地数据 Data 存储时并没有该字段, 进行 Decode 时会发生与上边解析时短少 key 同样的问题, 那么经过 @Default 来进行默认值设置将是一种十分好的选择。

对不支持 Codable 协议的类型做处理

UIImage 是没有恪守 Codable 协议的, 假如咱们界说了 UIImage 类型的特点时编译器是会报错的。

或许会我们首先想到的便是转化下改为界说 Data 类型, 假如有需求再界说一个仅支持 getUIImage 特点处理转化。就像下边这样:

var imageData: Data?
var image: UIImage? {
    if let imageData = imageData {
        return UIImage(data: imageData)
    }
    return nil
}

@propertyWrapper 能够帮助咱们改善吗? 当然!

func testCodableImage() {
    struct AStruct: Codable {
        @CodableImage var image: UIImage?
    }
    let image = UIImage(named: "test")
    XCTAssertNotNil(image)
    var aStruct = AStruct()
    aStruct.image = image
    do {
        let encoder = JSONEncoder()
        let data = try encoder.encode(aStruct)
        let decoder = JSONDecoder()
        let aDecodableStruct = try decoder.decode(AStruct.self, from: data)
        XCTAssertNotNil(aDecodableStruct.image)
    } catch {
        print(error)
        XCTAssertThrowsError(error)
    }
}

Test Succeed

十分好

发现做了类似前面用到的 @Default 办法即达到了想要的效果, 仅仅 @Default 替换为了@CodableImage。同样的将其马甲脱掉看看你是否还知道 ta

@propertyWrapper
struct CodableImage: Codable {
  var wrappedValue: UIImage?
    init(wrappedValue: UIImage? = nil) {
    self.wrappedValue = wrappedValue
  }
  enum CodingKeys: String, CodingKey {
    case wrappedValue
  }
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    let data = try container.decode(Data.self, forKey: CodingKeys.wrappedValue)
    guard let image = UIImage(data: data) else {
      throw DecodingError.dataCorruptedError(forKey: CodingKeys.wrappedValue, in: container, debugDescription: "Decoding image failed!")
    }
    self.wrappedValue = image
  }
  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    if let data = wrappedValue?.pngData() {
      try container.encode(data, forKey: CodingKeys.wrappedValue)
    } else if let data = wrappedValue?.jpegData(compressionQuality: 1.0) {
      try container.encode(data, forKey: CodingKeys.wrappedValue)
    } else {
      try container.encodeNil(forKey: CodingKeys.wrappedValue)
    }
  }
}

本来是将转化办法写到了 encode/decode 办法中。

你或许会对构造办法发生好奇, 它也对 UIImage<->Data 转化发挥了哪些效果吗?

答案是并没有, 它的效果仅仅用于初始化赋值的:

struct AStruct: Codable {
    @CodableImage(wrappedValue: UIImage(named: "test")) var image: UIImage?
}

假如你想, 也能够是这样子:

init() {
    self.wrappedValue = nil
}

范围限定

一个风趣的例子, 摘自 Swift Property Wrappers

@propertyWrapper
struct Clamping<Value: Comparable> {
    var value: Value
    let range: ClosedRange<Value>
    init(initialValue value: Value, _ range: ClosedRange<Value>) {
        precondition(range.contains(value))
        self.value = value
        self.range = range
    }
    var wrappedValue: Value {
        get { value }
        set { value = min(max(range.lowerBound, newValue), range.upperBound) }
    }
}
struct Solution {
    @Clamping(0...14) var pH: Double = 7.0
}
let carbonicAcid = Solution(pH: 4.68) // at 1 mM under standard conditions
let superDuperAcid = Solution(pH: -1)
superDuperAcid.pH // 0

总结

这片关于 @propertyWrapper 的介绍仅是冰山一角, 咱们或许经常在官方 API 或第三方库中看到以 @ 最初的润饰, 不出意外的话都是特点包装器。

希望这篇文章能够成为你了解学习特点包装器路上的基石。

Demo

查看完整版 DefaultValue 或想进行测试, 能够拜访我的 GitHub


更多链接: