CI codecov

一种在不同存储(如 Keychain 或 UserDefaults)中持久化数据的简单方法,并且可以轻松扩展或测试。

动机

我们可以使用不同类型的存储(UserDefaults、NSCache、Keychain 等)来通过键存储一些简单的数据。 但是,这些存储都有自己的 API。

这时,PersistedStoragePersistedValue 可以提供帮助。 该协议的调用方不知道存储的类型,因为 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

AnyPersistedValuePersistedValue 协议提供了一个类型擦除的包装器,这有助于在某些情况下(比如这样)避免在代码中引入泛型。

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
  }
}

模拟(Mocks)

对于测试,您可以使用 PersistedValueTestingUtilities target 中的 MockPersistedStorageMockPersistedValue

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 项目中。

  1. 文件 › 添加包…
  2. 在包 URL 文本字段中输入“https://github.com/DimaKoroliov/PersistedValue

替代方案