一种在不同存储(如 Keychain 或 UserDefaults)中持久化数据的简单方法,并且可以轻松扩展或测试。
我们可以使用不同类型的存储(UserDefaults、NSCache、Keychain 等)来通过键存储一些简单的数据。 但是,这些存储都有自己的 API。
这时,PersistedStorage
和 PersistedValue
可以提供帮助。 该协议的调用方不知道存储的类型,因为 API 将是相同的。 它还解决了持久化类型的问题,您可以存储任何您想要的类型而无需更改 API。
PersistedValue
具有许多可以简化用法的运算符。 这是一个例子:
let persistedValue = storage.persistedValue(forKey: "key")
// store Codable types
.codable(Model.self)
// provides default value
.default(Model())
// print setters and getter in console
.print()
// reads current value
let model = persistedValue.wrappedValue
// writes a new value
persistedValue.wrappedValue = Model()
强烈建议将持久化值包装在单独的服务中。 这将是单一的数据源,并且可以轻松注入。
protocol PersistedValuesServiceProtocol {
var userToggle: AnyPersistedValue<Bool> { get }
var userId: AnyPersistedValue<String?> { get }
var token: PersistedValues.Subject<Token?> { get }
}
final class PersistedValuesService: PersistedValuesServiceProtocol {
let userToggle: AnyPersistedValue<Bool>
let userId: AnyPersistedValue<String?>
let token: PersistedValues.Subject<Token?>
init(
keychain: PersistedStorage,
userDefaults: PersistedStorage
) {
self.userToggle = userDefaults.persistedValue(forKey: "user_toogle")
.bool()
.default(false)
.eraseToAnyPersistedValue()
self.userId = keychain.persistedValue(forKey: "user_id")
.string()
.eraseToAnyPersistedValue()
self.token = keychain.persistedValue(forKey: "token")
.codable(Token.self)
.subject(didChage: keychain.didChange(forKey: "token"))
}
}
struct Token: Codable {
let access: String
}
struct SomeViewModel {
private let persistedValues: PersistedValuesServiceProtocol
init(persistedValues: PersistedValuesServiceProtocol) {
self.persistedValues = persistedValues
}
func logOut() {
self.persistedValues.userId.wrappedValue = nil
}
}
AnyPersistedValue
为 PersistedValue
协议提供了一个类型擦除的包装器,这有助于在某些情况下(比如这样)避免在代码中引入泛型。
class SomeViewModel<PV> where PV: PersistedValue, Value == String? {
private let userId: PV
init(userId: PV) {
self.userId = userId
}
func logOut() {
self.userId.wrappedValue = nil
}
}
这个泛型 SomeViewModel<PersistedValue>
很难作为属性存在,并且你无法从这个泛型模型中获得好处。 所以可以用 AnyPersistedValue
以这种方式重构它:
class SomeViewModel {
private let userId: AnyPersistedValue<String?>
init<PV>(userId: PV) where PV: PersistedValue, Value == String? {
self.userId = userId.eraseToAnyPersistedValue()
}
func logOut() {
self.userId.wrappedValue = nil
}
}
对于测试,您可以使用 PersistedValueTestingUtilities
target 中的 MockPersistedStorage
和 MockPersistedValue
。
import PersistedValueTestingUtilities
final class PersistedValuesServiceTests: XCTestCase {
private var keychain: MockPersistedStorage!
private var userDefaults: MockPersistedStorage!
private var service: PersistedValuesService!
override func setUp() {
self.keychain = .init()
self.userDefaults = .init()
self.service = .init(keychain: keychain, userDefaults: userDefaults)
}
override func tearDown() { ... }
func testUserId() {
self.serive.userId.wrappedValue = "ID"
XCTAssertEqual(self.storage.sets["user_id"], 1)
}
}
您可以通过将 PersistedValue 添加为包依赖项来将其添加到 Xcode 项目中。