UserDefaults 与 Codable

以更 Swift 的方式使用 UserDefaults。


前言

从文档简介来看,UserDefaultsFloat / Double / Int / Bool / URL? 类型提供了具体的 set 方法,其他类型则调用 Any?set 方法,实际上接受的是 NSData / NSString / NSNumber / NSDate / NSArray / NSDictionary。如果非以上类型,Apple 建议转为 Data 再保存。

从 API 设计来看,set 方法相对友好,除了 Any? / URL? 使用了 Optional,其他的都是具体类型。而 get 方法则较为混乱,bool(forKey:) / integer(forKey:) / float(forKey:) / double(forKey:) 的返回值是具体类型,即找不到就返回 false / 0array(forKey:) / dictionary(forKey:) 的返回值是 Any,需要额外转换类型;stringArray(forKey:) 的返回值是 [String],其他类型却没有便利方法。

以下内容基于 Xcode 12.1,iPhone 12 Pro Max Simulator with iOS 14.1。

准备

1
2
3
4
5
6
7
8
struct Defaults<Value> {

typealias Key = String
typealias Value = Value

let key: Key
let value: Value
}

首先定义 Defaults 结构体,保存 keyvalue 变量。

1
2
3
4
5
6
7
8
9
let bool = Defaults(key: "u_bool", value: Bool(true))
let date = Defaults(key: "u_date", value: Date(timeIntervalSince1970: 0))
let double = Defaults(key: "u_double", value: Double(1000))
let float = Defaults(key: "u_float", value: Float(100))
let int = Defaults(key: "u_int", value: Int(10))
let string = Defaults(key: "u_string", value: String("@Nuomi1"))
let url = Defaults(key: "u_url", value: URL(string: "https://nuomi1.github.io/")!)
let array = Defaults(key: "u_array", value: [10])
let dictionary = Defaults(key: "u_dictionary", value: ["int": 10])

根据 UserDefaults 的默认支持类型,选择了 Bool / Date / Double / Float/ Int / String / URL / Array<Int> / Dictionary<String, Int> 进行测试,对应的桥接类型和自定义类型暂时忽略。

第一版

get-set-1

1
2
3
4
5
6
7
8
9
10
11
12
extension UserDefaults {

func set1<Value>(with defaults: Defaults<Value>) {
set(defaults.value, forKey: defaults.key)
}

func value1<Value>(with defaults: Defaults<Value>) -> Value? {
guard let object = value(forKey: defaults.key) else { return nil }

return object as? Value
}
}

直接调用 UserDefaultssetget 方法。

test-1a

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func test1a<Value: Equatable>(with defaults: Defaults<Value>) {
UserDefaults.standard.set1(with: defaults)
let value = UserDefaults.standard.value1(with: defaults)

assert(defaults.value == value)
}

test1a(with: bool)
test1a(with: date)
test1a(with: double)
test1a(with: float)
test1a(with: int)
test1a(with: string)
test1a(with: url)
test1a(with: array)
test1a(with: dictionary)
Key Type Value
u_bool Number 1
u_date Date 1970-01-01 08:00:00
u_double Number 1000
u_float Number 100
u_int Number 10
u_string String @Nuomi1
u_url ERROR Thread 1: “Attempt to insert non-property list object https://nuomi1.github.io/ for key u_url”
u_array Array<Number> [10]
u_dictionary Dictionary<String, Number> [“int”: 10]

所有测试类型都是调用 Any? 的版本,而不是具体类型的版本。同时 URL 无法使用 Any? 的版本进行写入。

test-1b

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func test1b(with defaults: Defaults<URL>) {
UserDefaults.standard.set(defaults.value, forKey: defaults.key)
let object = UserDefaults.standard.value(forKey: defaults.key)

let url1 = object as? URL
let url2 = (object as? Data).flatMap { String(data: $0, encoding: .utf8) }
let url3 = (object as? Data).flatMap { NSKeyedUnarchiver.unarchiveObject(with: $0) as? URL }

assert(url1 == nil)
assert(url2 == nil)
assert(defaults.value == url3)
}

test1b(with: url)
Key Type Value
u_url Data <62706c69 73743030 d4010203 04050607 0a582476 65727369 6f6e5924 61726368 69766572 5424746f 7058246f 626a6563 74731200 0186a05f 100f4e53 4b657965 64417263 68697665 72d10809 54726f6f 748001a4 0b0c1314 55246e75 6c6cd30d 0e0f1011 12574e53 2e626173 65562463 6c617373 5b4e532e 72656c61 74697665 80008003 80025f10 19687474 70733a2f 2f6e756f 6d69312e 67697468 75622e69 6f2fd215 1617185a 24636c61 73736e61 6d655824 636c6173 73657355 4e535552 4ca21719 584e534f 626a6563 7408111a 24293237 494c5153 585e656d 74808284 86a2a7b2 bbc1c400 00000000 00010100 00000000 00001a00 00000000 00000000 00000000 0000cd>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// u_url-1b.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>$archiver</key>
<string>NSKeyedArchiver</string>
<key>$objects</key>
<array>
<string>$null</string>
<dict>
<key>$class</key>
<dict>
<key>CF$UID</key>
<integer>3</integer>
</dict>
<key>NS.base</key>
<dict>
<key>CF$UID</key>
<integer>0</integer>
</dict>
<key>NS.relative</key>
<dict>
<key>CF$UID</key>
<integer>2</integer>
</dict>
</dict>
<string>https://nuomi1.github.io/</string>
<dict>
<key>$classes</key>
<array>
<string>NSURL</string>
<string>NSObject</string>
</array>
<key>$classname</key>
<string>NSURL</string>
</dict>
</array>
<key>$top</key>
<dict>
<key>root</key>
<dict>
<key>CF$UID</key>
<integer>1</integer>
</dict>
</dict>
<key>$version</key>
<integer>100000</integer>
</dict>
</plist>

不封装函数直接处理时,调用 URL? 的版本,但存储的类型是 Data。把 object 对象进行类型转换,前面两个是失败的,只有第三个是成功的。

疑问

UserDefaults.swift#LL123 中,URL 可以被 Any? 的版本处理,且保存 absoluteURL.path,实际上只能被 URL? 的版本处理。这里的原因可能是私有实现与开源实现并不一致。

小结

自带的 set 方法无法在一个方法里处理多种类型,下面我们借助 Codable 来实现。

第二版

get-set-2a

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
extension UserDefaults {

func set2a<Value: Encodable>(with defaults: Defaults<Value>) {
do {
let encoder = JSONEncoder()
let data = try encoder.encode(defaults.value)

set(data, forKey: defaults.key)
} catch {
assertionFailure("\(error)")
return
}
}

func value2a<Value: Decodable>(with defaults: Defaults<Value>) -> Value? {
guard let object = value(forKey: defaults.key) else { return nil }
guard let data = object as? Data else { assertionFailure(); return nil }

do {
let decoder = JSONDecoder()
let value = try decoder.decode(Value.self, from: data)

return value
} catch {
assertionFailure("\(error)")
return nil
}
}
}

借助 JSONEncoderJSONDecodervaluejsonData 的形式进行写入与读取。

test-2a

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func test2a<Value: Codable & Equatable>(with defaults: Defaults<Value>) {
UserDefaults.standard.set2a(with: defaults)
let value = UserDefaults.standard.value2a(with: defaults)

assert(defaults.value == value)
}

test2a(with: bool)
test2a(with: date)
test2a(with: double)
test2a(with: float)
test2a(with: int)
test2a(with: string)
test2a(with: url)
test2a(with: array)
test2a(with: dictionary)
Key Type Value
u_bool Data <74727565>
u_date Data <2d393738 33303732 3030>
u_double Data <31303030>
u_float Data <313030>
u_int Data <3130>
u_string Data <22404e75 6f6d6931 22>
u_url Data <22687474 70733a5c 2f5c2f6e 756f6d69 312e6769 74687562 2e696f5c 2f22>
u_array Data <5b31305d>
u_dictionary Data <7b22696e 74223a31 307d>
1
2
// u_bool-2a.json
true
1
2
// u_date-2a.json
-978307200
1
2
// u_double-2a.json
1000
1
2
// u_float-2a.json
100
1
2
// u_int-2a.json
10
1
2
// u_string-2a.json
"@Nuomi1"
1
2
// u_url-2a.json
"https://nuomi1.github.io/"
1
2
// u_array-2a.json
[10]
1
2
// u_dictionary-2a.json
{"int": 10}

jsonData 的内容实际上是 String,可以看到 Double / Float / Int 的前半段是一样的值,都是 10Array<Int> / Dictionary<String, Int> 的中间也有一样的值,都是 10

get-set-2b

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
extension UserDefaults {

func set2b<Value: Encodable>(with defaults: Defaults<Value>) {
do {
let encoder = PropertyListEncoder()
let data = try encoder.encode(defaults.value)

set(data, forKey: defaults.key)
} catch {
assertionFailure("\(error)")
return
}
}

func value2b<Value: Decodable>(with defaults: Defaults<Value>) -> Value? {
guard let object = value(forKey: defaults.key) else { return nil }
guard let data = object as? Data else { assertionFailure(); return nil }

do {
let decoder = PropertyListDecoder()
let value = try decoder.decode(Value.self, from: data)

return value
} catch {
assertionFailure("\(error)")
return nil
}
}
}

改用 PropertyListEncoderPropertyListDecodervalueplistData 的形式进行写入与读取。

test-2b

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func test2b<Value: Codable & Equatable>(with defaults: Defaults<Value>) {
UserDefaults.standard.set2b(with: defaults)
let value = UserDefaults.standard.value2b(with: defaults)

assert(defaults.value == value)
}

test2b(with: bool)
test2b(with: date)
test2b(with: double)
test2b(with: float)
test2b(with: int)
test2b(with: string)
test2b(with: url)
test2b(with: array)
test2b(with: dictionary)
Key Type Value
u_bool ERROR Thread 1: Fatal error: invalidValue(true, Swift.EncodingError.Context(codingPath: [], debugDescription: “Top-level Bool encoded as number property list fragment.”, underlyingError: nil))
u_date ERROR Thread 1: Fatal error: invalidValue(1970-01-01 00:00:00 +0000, Swift.EncodingError.Context(codingPath: [], debugDescription: “Top-level Date encoded as date property list fragment.”, underlyingError: nil))
u_double ERROR Thread 1: Fatal error: invalidValue(1000.0, Swift.EncodingError.Context(codingPath: [], debugDescription: “Top-level Double encoded as number property list fragment.”, underlyingError: nil))
u_float ERROR Thread 1: Fatal error: invalidValue(100.0, Swift.EncodingError.Context(codingPath: [], debugDescription: “Top-level Float encoded as number property list fragment.”, underlyingError: nil))
u_int ERROR Thread 1: Fatal error: invalidValue(10, Swift.EncodingError.Context(codingPath: [], debugDescription: “Top-level Int encoded as number property list fragment.”, underlyingError: nil))
u_string ERROR Thread 1: Fatal error: invalidValue(“@Nuomi1”, Swift.EncodingError.Context(codingPath: [], debugDescription: “Top-level String encoded as string property list fragment.”, underlyingError: nil))
u_url Data <62706c69 73743030 d1010258 72656c61 74697665 5f101968 74747073 3a2f2f6e 756f6d69 312e6769 74687562 2e696f2f 080b1400 00000000 00010100 00000000 00000300 00000000 00000000 00000000 000030>
u_array Data <62706c69 73743030 a101100a 080a0000 00000000 01010000 00000000 00020000 00000000 00000000 00000000 000c>
u_dictionary Data <62706c69 73743030 d1010253 696e7410 0a080b0f 00000000 00000101 00000000 00000003 00000000 00000000 00000000 00000011>
1
2
3
4
5
6
7
8
9
// u_url-2b.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>relative</key>
<string>https://nuomi1.github.io/</string>
</dict>
</plist>
1
2
3
4
5
6
7
8
// u_array-2b.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
<integer>10</integer>
</array>
</plist>
1
2
3
4
5
6
7
8
9
// u_dictionary-2b.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>int</key>
<integer>10</integer>
</dict>
</plist>

除了 URL / Array<Int> / Dictionary<String, Int> 可以被编码,其余的都失败了。

疑问

JSONEncoderPropertyListEncoder 都是借助 Codable 进行编码,但是行为并不相同。

URL 为例,在 URL.swift#L1220 中,encode 时使用了 KeyedEncodingContainer<Key> 容器,这么看来 PropertyListEncoder 的行为是符合预期的,但是 JSONEncoder 直接编码 absoluteString 不符合预期。具体细节在 JSONEncoder.swift#L909 中。这里的猜想是,在 Swift 中 URL 是一个比较复杂的结构体,从其 encode 方法可以看到内部分为 baserelative,拆分两部分可以更为完整地描述 URL。但是在 JSON 中习惯使用 String,而且没有单独的 URL 类型,所以 JSONEncoder 沿用了这一习惯直接编码 absoluteString

JSONPropertyList 中,顶层的数据结构必须为数组或者字典,即 UnkeyedEncodingContainer / KeyedEncodingContainer<Key> 容器,PropertyListEncoder 的行为是符合预期的,但是 JSONEncoder 可以直接编码 Bool / Date / Double / Float / Int / String,不符合 JSON 的规范。具体的原因是开启了 fragmentsAllowed,允许非 Array / Dictionary 的类型作为顶层元素。

小结

JSON 中所有测试类型都可以成功编码,但在 PropertyList 中只有部分类型可以成功编码,下面我们借助 Wrapper 来实现。

第三版

1
2
3
4
5
6
7
8
9
10
11
extension UserDefaults {

struct Wrapper<T> {

let wrapped: T
}
}

extension UserDefaults.Wrapper: Encodable where T: Encodable {}

extension UserDefaults.Wrapper: Decodable where T: Decodable {}

先在 UserDefaults 里面定义一个 Wrapper<T> 结构体,把 UserDefaults 当做一个命名空间。然后声明当 T 遵循 Encodable / Decodable 时自身也遵循,更容易进行类型推导。

get-set-3a

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
extension UserDefaults {

func set3a<Value: Encodable>(with defaults: Defaults<Value>) {
do {
let wrapper = Wrapper(wrapped: defaults.value)

let encoder = JSONEncoder()
let data = try encoder.encode(wrapper)

set(data, forKey: defaults.key)
} catch {
assertionFailure("\(error)")
return
}
}

func value3a<Value: Decodable>(with defaults: Defaults<Value>) -> Value? {
guard let object = value(forKey: defaults.key) else { return nil }
guard let data = object as? Data else { assertionFailure(); return nil }

do {
let decoder = JSONDecoder()
let wrapper = try decoder.decode(Wrapper<Value>.self, from: data)

return wrapper.wrapped
} catch {
assertionFailure("\(error)")
return nil
}
}
}

value 放进 Wrapper,再使用 JSONEncoderJSONDecoder 进行写入与读取。

test-3a

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func test3a<Value: Codable & Equatable>(with defaults: Defaults<Value>) {
UserDefaults.standard.set3a(with: defaults)
let value = UserDefaults.standard.value3a(with: defaults)

assert(defaults.value == value)
}

test3a(with: bool)
test3a(with: date)
test3a(with: double)
test3a(with: float)
test3a(with: int)
test3a(with: string)
test3a(with: url)
test3a(with: array)
test3a(with: dictionary)
Key Type Value Count
u_bool Data <7b227772 61707065 64223a74 7275657d> 16
u_date Data <7b227772 61707065 64223a2d 39373833 30373230 307d> 22
u_double Data <7b227772 61707065 64223a31 3030307d> 16
u_float Data <7b227772 61707065 64223a31 30307d> 15
u_int Data <7b227772 61707065 64223a31 307d> 14
u_string Data <7b227772 61707065 64223a22 404e756f 6d693122 7d> 21
u_url Data <7b227772 61707065 64223a22 68747470 733a5c2f 5c2f6e75 6f6d6931 2e676974 6875622e 696f5c2f 227d> 42
u_array Data <7b227772 61707065 64223a5b 31305d7d> 16
u_dictionary Data <7b227772 61707065 64223a7b 22696e74 223a3130 7d7d> 22

所有测试类型全部通过,但是数据长度相比 test-2a 明显变大。

get-set-3b

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
extension UserDefaults {

func set3b<Value: Encodable>(with defaults: Defaults<Value>) {
do {
let wrapper = Wrapper(wrapped: defaults.value)

let encoder = PropertyListEncoder()
let data = try encoder.encode(wrapper)

set(data, forKey: defaults.key)
} catch {
assertionFailure("\(error)")
return
}
}

func value3b<Value: Decodable>(with defaults: Defaults<Value>) -> Value? {
guard let object = value(forKey: defaults.key) else { return nil }
guard let data = object as? Data else { assertionFailure(); return nil }

do {
let decoder = PropertyListDecoder()
let wrapper = try decoder.decode(Wrapper<Value>.self, from: data)

return wrapper.wrapped
} catch {
assertionFailure("\(error)")
return nil
}
}
}

value 放进 Wrapper,再使用 PropertyListEncoderPropertyListDecoder 进行写入与读取。

test-3b

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func test3b<Value: Codable & Equatable>(with defaults: Defaults<Value>) {
UserDefaults.standard.set3b(with: defaults)
let value = UserDefaults.standard.value3b(with: defaults)

assert(defaults.value == value)
}

test3b(with: bool)
test3b(with: date)
test3b(with: double)
test3b(with: float)
test3b(with: int)
test3b(with: string)
test3b(with: url)
test3b(with: array)
test3b(with: dictionary)
Key Type Value Count
u_bool Data <62706c69 73743030 d1010257 77726170 70656409 080b1300 00000000 00010100 00000000 00000300 00000000 00000000 00000000 000014> 55
u_date Data <62706c69 73743030 d1010257 77726170 70656433 c1cd27e4 40000000 080b1300 00000000 00010100 00000000 00000300 00000000 00000000 00000000 00001c> 63
u_double Data <62706c69 73743030 d1010257 77726170 70656423 408f4000 00000000 080b1300 00000000 00010100 00000000 00000300 00000000 00000000 00000000 00001c> 63
u_float Data <62706c69 73743030 d1010257 77726170 70656422 42c80000 080b1300 00000000 00010100 00000000 00000300 00000000 00000000 00000000 000018> 59
u_int Data <62706c69 73743030 d1010257 77726170 70656410 0a080b13 00000000 00000101 00000000 00000003 00000000 00000000 00000000 00000015> 56
u_string Data <62706c69 73743030 d1010257 77726170 70656457 404e756f 6d693108 0b130000 00000000 01010000 00000000 00030000 00000000 00000000 00000000 001b> 62
u_url Data <62706c69 73743030 d1010257 77726170 706564d1 03045872 656c6174 6976655f 10196874 7470733a 2f2f6e75 6f6d6931 2e676974 6875622e 696f2f08 0b13161f 00000000 00000101 00000000 00000005 00000000 00000000 00000000 0000003b> 96
u_array Data <62706c69 73743030 d1010257 77726170 706564a1 03100a08 0b131500 00000000 00010100 00000000 00000400 00000000 00000000 00000000 000017> 59
u_dictionary Data <62706c69 73743030 d1010257 77726170 706564d1 03045369 6e74100a 080b1316 1a000000 00000001 01000000 00000000 05000000 00000000 00000000 00000000 1c> 65

相比 test-2b,所有测试类型全部通过,但是数据长度相比 test-3a 变得更大。

小结

通过增加 wrapped 顶层,把数据全部转为字典,在 JSONPropertyList 中都可以正常处理。同时可以看到,PropertyList 的数据长度比 JSON 大得多。准确性已经处理好了,但是 Data 没有可读性,接下来我们处理可读性。

第四版

get-set-4a

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
extension UserDefaults {

func set4a<Value: Encodable>(with defaults: Defaults<Value>) {
do {
let wrapper = Wrapper(wrapped: defaults.value)

let encoder = JSONEncoder()
let data = try encoder.encode(wrapper)

let object = try JSONSerialization.jsonObject(
with: data
)

set(object, forKey: defaults.key)
} catch {
assertionFailure("\(error)")
return
}
}

func value4a<Value: Decodable>(with defaults: Defaults<Value>) -> Value? {
guard let object = value(forKey: defaults.key) else { return nil }

do {
let data = try JSONSerialization.data(
withJSONObject: object
)

let decoder = JSONDecoder()
let wrapper = try decoder.decode(Wrapper<Value>.self, from: data)

return wrapper.wrapped
} catch {
assertionFailure("\(error)")
return nil
}
}
}

借助 JSONSerializationdataobject 互转。

test-4a

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func test4a<Value: Codable & Equatable>(with defaults: Defaults<Value>) {
UserDefaults.standard.set4a(with: defaults)
let value = UserDefaults.standard.value4a(with: defaults)

assert(defaults.value == value)
}

test4a(with: bool)
test4a(with: date)
test4a(with: double)
test4a(with: float)
test4a(with: int)
test4a(with: string)
test4a(with: url)
test4a(with: array)
test4a(with: dictionary)

所有测试类型全部通过。

get-set-4b

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
extension UserDefaults {

func set4b<Value: Encodable>(with defaults: Defaults<Value>) {
do {
let wrapper = Wrapper(wrapped: defaults.value)

let encoder = PropertyListEncoder()
let data = try encoder.encode(wrapper)

let object = try PropertyListSerialization.propertyList(
from: data,
format: nil
)

set(object, forKey: defaults.key)
} catch {
assertionFailure("\(error)")
return
}
}

func value4b<Value: Decodable>(with defaults: Defaults<Value>) -> Value? {
guard let object = value(forKey: defaults.key) else { return nil }

do {
let data = try PropertyListSerialization.data(
fromPropertyList: object,
format: .binary,
options: 0
)

let decoder = PropertyListDecoder()
let wrapper = try decoder.decode(Wrapper<Value>.self, from: data)

return wrapper.wrapped
} catch {
assertionFailure("\(error)")
return nil
}
}
}

借助 PropertyListSerializationdataobject 互转。

test-4b

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func test4b<Value: Codable & Equatable>(with defaults: Defaults<Value>) {
UserDefaults.standard.set4b(with: defaults)
let value = UserDefaults.standard.value4b(with: defaults)

assert(defaults.value == value)
}

test4b(with: bool)
test4b(with: date)
test4b(with: double)
test4b(with: float)
test4b(with: int)
test4b(with: string)
test4b(with: url)
test4b(with: array)
test4b(with: dictionary)

所有测试类型全部通过。

小结

借助 JSONSerializationPropertyListSerialization,把 data 转为 object 直接保存。

总结

借助 Codable / Wrapper / JSONSerialization / PropertyListSerialization,我们成功地把 model 转为 object 进行保存,但是问题还是不少。

  1. Wrapper 只是为了防止 PropertyListEncoder 出错而做的折中处理,多了一层嵌套实际上浪费了处理时间和保存容量。
  2. JSONEncoderPropertyListEncoder 内部已经调用了各自的 Serializationmodel -> object -> data,再把 data 转为 object 也是一种浪费。
  3. JSONEncoderPropertyListEncoder 不一致的处理会带来困惑,可以通过增加更多的 option 进行定制处理。

实际上可以参考 JSONEncoder / JSONDecoder / PropertyListEncoder / PropertyListDecoder,实现 UserDefaultsEncoderUserDefaultsDecoder,直接把 model 转为 object 进行保存,同时根据自己的需要单独处理(例如 URL)。

当然这又是另一个完整的解决方案了,以后可以尝试。

参考