Saving and Restoring AppState

What's an elegant approach to saving and loading state?

It might make more sense to just serialise the whole state and save it manually (by calling an action?) or even automatically on each state change.

It would be also nice to be able to cherry pick which part of the state would go in:

  1. UserDefaults
  2. iCloud's NSUbiquitousKeyValueStore
  3. NSUserActivity for Siri Suggestions, scene restoration, etc.

I guess I could just scope different view and pass that to an environment function... or just use a shared state that conforms to codable:

   struct UserDefaultsState: Codable {
    public var counter: Counter
  }

  var userDefaults: UserDefaultsState {
    get { UserDefaultsState(counter: self.counter) }
    set { self.counter = newValue.counter }
  }

then just encode and save that to userDefaults using the environment.

What does everyone think and how did you approach it in your own projects?

1 Like

It seems a high order reducer is the simplest way to go:

public extension Reducer {
  func userDefaults<LocalState>(
    _ key: String = "co.pointfree.ComposableArchitecture.UserDefaults",
    state toLocalState: @escaping (State) -> LocalState,
    environment toUserDefaultsEnvironment: @escaping (Environment) -> UserDefaultsEnvironment = { _ in
    UserDefaultsEnvironment()
    }
  ) -> Reducer where LocalState:Equatable, LocalState:Encodable {
    return .init { state, action, environment in
      let previousState = toLocalState(state)

      let effects = self.run(&state, action, environment)
      let nextState = toLocalState(state)

      guard previousState != nextState else { return effects }

      let userDefaultsEnvironment = toUserDefaultsEnvironment(environment)

      return .merge(
        .fireAndForget {
          userDefaultsEnvironment.queue.async {
            guard let data = try? JSONEncoder().encode(nextState) else { return }
            userDefaultsEnvironment.userDefaults.set(data, forKey: key)
          }
        },
        effects
      )
    }
  }
}

Usage:

public struct AppState: Equatable, Encodable {
  [...]

  public var counter = CounterState()

  // Cherry-pick what to save to userDefaults
 public struct UserDefaultsState: Equatable, Codable {
    var counter: CounterState

    public init(counter: CounterState) {
      self.counter = counter
    }
  }

  var userDefaults: UserDefaultsState { UserDefaultsState(counter: self.counter) }
  
  enum CodingKeys: String, CodingKey {
    case counter
  }
  
  public init(userDefaults: UserDefaultsState? = nil) {
    self.counter = userDefaults?.counter ?? .init()
  }
}


  let userDefaultsKey = "com.example.Example.appState"

  var userDefaults: AppState.UserDefaultsState? {
    UserDefaults.standard.data(forKey: userDefaultsKey)
    .flatMap { try? JSONDecoder().decode(AppState.UserDefaultsState.self, from: $0) }
  }

 lazy var store = {
    Store(
      initialState: AppState(userDefaults: userDefaults),
      reducer: appReducer.debug().userDefaults(
        userDefaultsKey,
        state: { state in state.userDefaults }
      ),
 ;
 [...]

1 Like