iOS数据持久化——UserDefaults

运用特点封装器来完美创立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相同。但是,当界说一个特点封装器的时分,有必要要有一个wrapppedValuewrapppedValue 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,一个是defaultValuekey将作为UserDefaults读写时的键,而defaultValue则作为UserDefaults无值时分的回来值。

Storage特点封装器准备就绪后,咱们就能够开始完成UserDefaults封装器了。直截了当,咱们只需要创立一个被Storage特点封装器封装的‘username’变量。这儿要留意的是,你能够经过keydefaultValue初始化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

感谢咱们的阅读,给个赞呗