PersistedValue

main

Solution that makes persisted value API as easy as possible
DimaKoroliov/PersistedValue

CI codecov

A simple way to have persisted data in different storages (like Keychain or UserDefaults) that can be easily extended or tested.

Motivation

We can have different types of storages (UserDefaults, NSCache, Keychain etc) for storing some simple data by key. But each of these stores has its own API.

This is the moment when PersistedStorage and PersistedValue can help. Caller side of this protocol knows nothing about the type of storage as the API will be the same. It also solved a problem with persisted type and you can store any type you want without changing API.

Usage

PersistedValue has a number of operators that can simplify usage. Here an example some of them:

 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()

Highly recommend wrapping persisted values in separate service. It'll be single source of truth and can be easily injected.

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

The AnyPersistedValue provides a type-erasing wrapper for the PersistedValue protocol, that helps to avoid introducing generics in your code in some cases like this:

class SomeViewModel<PV> where PV: PersistedValue, Value == String? {
  private let userId: PV

  init(userId: PV) {
    self.userId = userId
  }

  func logOut() {
    self.userId.wrappedValue = nil
  }
}

This generic SomeViewModel<PersistedValue> will be hard to have as a property and you don't get benefits by this generic model. So it can be refactored with AnyPersistedValue in this way:

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

For testing proposals you can use MockPersistedStorage and MockPersistedValue from PersistedValueTestingUtilities target.

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

Requirements

  • Xcode 12.4 and higher
  • Swift 5.3 and higher

Installation

You can add PersistedValue to an Xcode project by adding it as a package dependency.

  1. File › Add Packages…
  2. Enter "https://github.com/DimaKoroliov/PersistedValue" into the package URL text field

Alternatives

Description

  • Swift Tools 5.1.0
View More Packages from this Author

Dependencies

  • None
Last updated: Tue Mar 19 2024 04:08:43 GMT-0900 (Hawaii-Aleutian Daylight Time)