iOS数据持久化UserDefaults封装器使用详解
庄周晓梦 人气:0使用属性封装器来完美创建UserDefaults封装器
想象一下,你有一个应用想实现自动登录功能。你用UserDefaults封装了关于UserDefaults的读与写逻辑。你会用UserDefaults封装来保持对自动登录”On/Off“状态、userName的跟踪。你可能会以下面这种方式来封装UserDefaults
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对于属性封装器的介绍,我们可以对上面的代码进行精简,如下
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
的属性封装器,如下:
@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属性封装器
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
属性封装器转化为在读写操作期间进行读写的属性封装器。
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
。
struct AppData { @Storage(key: "username_key", defaultValue: "") static var username: String }
一切就绪之后,UserDefaults
封装器就可以使用了
AppData.username = "swift-senpai" print(AppData.username)
同时,我们来添加enableAutoLogin
变量到我们的UserDefaults
封装器中
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
代码块
@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类型的数据了
// 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
中读取的数据转化成对应的数据类型。 如下:
@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
中
struct User: Codable { var firstName: String var lastName: String var lastLogin: Date? }
接下来,在UserDefaults
封装器中声明一个User
对象
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
封装器现在可以存储自定义对象了
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
加载全部内容