想象一下,你有一个应用想实现自动登录功能。你用UserDefaults封装了关于UserDefaults的读与写逻辑。你会用UserDefaults封装来保持对自动登录”On/Off“状态、userName的跟踪。你可能会以下面这种方式来封装UserDefaults
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
struct AppData {
private static let enableAutoLoginKey = "enable_auto_login_key"
private static let usernameKey = "username_key"
static var enableAutoLogin: Bool {
get {
return UserDefaults.standard.bool(forKey: enableAutoLoginKey)
}
set {
UserDefaults.standard.set(newValue, forKey: enableAutoLoginKey)
}
}
static var username: String {
get {
return UserDefaults.standard.string
}
set {
UserDefaults.standard.set(newValueds, forKey: usernameKey)
}
}
}
|
通过Swift5.1对于属性封装器的介绍,我们可以对上面的代码进行精简,如下
1
2
3
4
5
6
|
struct AppData {
@Storage(key: "enable_auto_login_key", defaultValue: false)
static var enableAutoLogin: Bool
@Storage(key: "username_key", defaultValue: "")
static var username: String
}
|
这样就很完美了吗?接着看
什么是属性封装器?
在我们进入详细讨论之前,我们先快速地了解一下什么是属性封装器 基本上来讲,属性封装器是一种通用数据结构,可以拦截属性的读写访问,从而允许在属性的读写期间添加自定义行为。
可以通过关键字@propertyWrapper来声明一个属性封装器。你想要有一个字符串类型的属性,每当这个属性被进行读写操作的时候,控制台就会输出。你可以创建一个名为Printable的属性封装器,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@propertyWrapper
struct Printable {
private var value: String = ""
var wrapperValue: String {
get {
print("get value:\(value)")
return value
}
set {
print("set value:\(newValue)")
value = newValue
}
}
}
|
通过上述代码我们可以看出,属性封装跟其他struct一样。然而,当定义一个属性封装器的时候,必须要有一个wrapppedValue。 wrapppedValue get set代码块就是拦截和执行你想要的操作的地方。在这个例子中,添加了打印状态的代码来输出get和set的值
接下来,我们看看,如何使用Printable属性封装器
1
2
3
4
5
|
struct Company {
@Printable static var name: String
}
Company.name = "Adidas"
Company.name
|
需要注意的是,我们如何使用@符号来声明一个用属性封装器封装的”name“变量。如果你想要在Playground中尝试敲出上述代码的话,你会看到以下输出:
Set Value: Adidas
Get Value: Adidas
什么是UserDefault封装器
在理解了什么是属性封装器以及它是如何工作的之后,我们现在开始准备实现我们的UserDefaults封装器。总结一下,我们的属性封装器需要持续跟踪自动登录的”On/Off“状态以及用户的username。 通过使用我们上述讨论的概念,我们可以很轻松的将Printable属性封装器转化为在读写操作期间进行读写的属性封装器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import Foundation
@propertyWrapper
struct Storage {
private let key: String
private let defaultValue: String
init(key: Stirng, defaultValue: String) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: String {
get {
return UserDefaults.standard.string(forKey: key) ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
|
在这里,我们将我们的属性封装器命名为Storage。有两个属性,一个是key,一个是defaultValue。key将作为UserDefaults读写时的键,而defaultValue则作为UserDefaults无值时候的返回值。
Storage属性封装器准备就绪后,我们就可以开始实现UserDefaults封装器了。直截了当,我们只需要创建一个被Storage属性封装器封装的‘username’变量。这里要注意的是,你可以通过key和defaultValue来初始化Storage。
1
2
3
4
|
struct AppData {
@Storage(key: "username_key", defaultValue: "")
static var username: String
}
|
一切就绪之后,UserDefaults封装器就可以使用了
1
2
|
AppData.username = "swift-senpai"
print(AppData.username)
|
同时,我们来添加enableAutoLogin变量到我们的UserDefaults封装器中
1
2
3
4
5
6
|
struct AppData {
@Storage(key: "username_key", defaultValue: "")
static var username: String
@Storage(key: "enable_auto_login_key", defaultValue: false)
static var username: Bool
}
|
这个时候,会报下面两种错误:
Cannot convert value of type ‘Bool’ to expected argument type ‘String’
Property type 'Bool' does not match that of lthe 'WrappedValue' property of its wrapper type 'Storage'
这是因为我们的封装器目前只支持String类型。想要解决这两个错误,我们需要将我们的属性封装器进行通用化处理
将属性封装器进行通用化处理
我们必须改变属性封装器的wrappedValue的数据类型来进行封装器的通用化处理,将String类型改成泛型T。进而,我们必须使用通用方式从UserDefaults读取来更新wrappedValue get代码块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@propertyWrapper
struct Storage<T> {
private let key: String
private let defaultValue: T
init(key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
// Read value from UserDefaults
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
// Set value to UserDefaults
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
|
好,有了通用属性封装器之后,我们的UserDefaults封装器就可以存储Bool类型的数据了
1
2
3
4
5
6
7
8
9
|
// The UserDefaults wrapper
struct AppData {
@Storage(key: "username_key", defaultValue: "")
static var username: String
@Storage(key: "enable_auto_login_key", defaultValue: false)
static var enableAutoLogin: Bool
}
AppData.enableAutoLogin = true
print(AppData.enableAutoLogin) // true
|
存储自定义对象
上面的操作都是用来基本数据类型的。但是如果我们想要存储自定义对象呢?接下来我们一起看看,如何能让UserDefaults支持自定义对象的存储
这里的内容很简单,我们将会存储一个自定义对象到UserDefaults中,为了达到这个目的,我们必须改造一下Storage属性封装器的类型T,使其遵循Codable协议
然后,在wrappedValue``set代码块中我们将使用JSONEncoder把自定义对象转化为Data,并将其写入UserDefaults中。同时,在wrappedValue``get代码块中,我们将使用JSONDecoder把从UserDefaults中读取的数据转化成对应的数据类型。 如下:
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
|
@propertyWrapper
struct Storage<T: Codable> {
private let key: String
private let defaultValue: T
init(key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
// Read value from UserDefaults
guard let data = UserDefaults.standard.object(forKey: key) as? Data else {
// Return defaultValue when no data in UserDefaults
return defaultValue
}
// Convert data to the desire data type
let value = try? JSONDecoder().decode(T.self, from: data)
return value ?? defaultValue
}
set {
// Convert newValue to data
let data = try? JSONEncoder().encode(newValue)
// Set value to UserDefaults
UserDefaults.standard.set(data, forKey: key)
}
}
}
|
为了让大家看到如何使用更新后的Storage属性封装器,我们来看一下接下来的例子。 想象一下,你需要存储用户登录成功后服务端返回的用户信息。首先,需要一个持有服务端返回的用户信息的struct。这个struct必须遵循Codable协议,以至于他能被转化为Data存储到UserDefaults中
1
2
3
4
5
|
struct User: Codable {
var firstName: String
var lastName: String
var lastLogin: Date?
}
|
接下来,在UserDefaults封装器中声明一个User对象
1
2
3
4
5
6
7
8
9
|
struct AppData {
@Storage(key: "username_key", defaultValue: "")
static var username: String
@Storage(key: "enable_auto_login_key", defaultValue: false)
static var enableAutoLogin: Bool
// Declare a User object
@Storage(key: "user_key", defaultValue: User(firstName: "", lastName: "", lastLogin: nil))
static var user: User
}
|
搞定了,UserDefaults封装器现在可以存储自定义对象了
1
2
3
4
5
6
|
let johnWick = User(firstName: "John", lastName: "Wick", lastLogin: Date())
// Set custom object to UserDefaults wrapper
AppData.user = johnWick
print(AppData.user.firstName) // John
print(AppData.user.lastName) // Wick
print(AppData.user.lastLogin!) // 2019-10-06 09:40:26 +0000
|