回顾 Codable
先来看一个常见的 Decode
场景。
场景一、
Swift
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
Swift
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
Swift
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
Swift
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
一样单纯的。
Swift
struct NormalDecodableStruct: Decodable {
var title: String = ""
var isNew: Bool = false
}
那么有更好的方案吗,有!!!
接下来看下通过 @propertyWrapper
如何来优雅的解决上边遇到的问题。
@propertyWrapper
首先来看下通过 @propertyWrapper
改写后的样子。
示例
Swift
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>
Swift
// 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>
但是获取的时候却是可以直接取到想要的值的, 原因是什么呢?
原理
propertyWrapper
可以理解成一组特别的 getter
和 setter
, 其本身是一个特殊的盒子,把原来值的类型包装了进去。被 propertyWrapper
声明的属性,实际上在存储时的类型是 propertyWrapper
这个盒子的类型,只不过编译器施了一些魔法,让它对外暴露的类型依然是被包装的原来的类型。
ps: wrappedValue
并不是随意定义的属性名称, 而是必须实现的。
看下官方文档中提供的示例:
less
@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
解决了主要用于接口数据解析的典型问题, 可能你也已经想到了其实还遗漏了一个枚举类型。接下来看下场景二。
场景二、
Swift
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
Swift
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
Swift
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
类型, 如果有需要再定义一个仅支持 get
的 UIImage
属性处理转换。就像下边这样:
Swift
var imageData: Data?
var image: UIImage? {
if let imageData = imageData {
return UIImage(data: imageData)
}
return nil
}
@propertyWrapper
可以帮助我们改进吗? 当然!
Swift
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
Swift
@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
转换发挥了哪些作用吗?
答案是并没有😄, 它的作用只是用于初始化赋值的:
Swift
struct AStruct: Codable {
@CodableImage(wrappedValue: UIImage(named: "test")) var image: UIImage?
}
如果你想, 也可以是这样子:
Swift
init() {
self.wrappedValue = nil
}
范围限定
一个有趣的例子, 摘自 Swift Property Wrappers。
Swift
@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) }
}
}
Swift
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。
更多链接: