Issue #96

Another cool thing about ios-oss is how it manages dependencies. Usually you have a lot of dependencies, and it’s good to keep them in one place, and inject it to the objects that need.

The Environment is simply a struct that holds all dependencies throughout the app

/**
 A collection of **all** global variables and singletons that the app wants access to.
 */
public struct Environment {
  /// A type that exposes endpoints for fetching Kickstarter data.
  public let apiService: ServiceType

  /// The amount of time to delay API requests by. Used primarily for testing. Default value is `0.0`.
  public let apiDelayInterval: DispatchTimeInterval

  /// A type that exposes how to extract a still image from an AVAsset.
  public let assetImageGeneratorType: AssetImageGeneratorType.Type

  /// A type that stores a cached dictionary.
  public let cache: KSCache
  
  /// ...
}

Then there’s global object called AppEnvironment that manages all these Environment in a stack

public struct AppEnvironment {
  /**
   A global stack of environments.
   */
  fileprivate static var stack: [Environment] = [Environment()]

  /**
   Invoke when an access token has been acquired and you want to log the user in. Replaces the current
   environment with a new one that has the authenticated api service and current user model.

   - parameter envelope: An access token envelope with the api access token and user.
   */
  public static func login(_ envelope: AccessTokenEnvelope) {
    replaceCurrentEnvironment(
      apiService: current.apiService.login(OauthToken(token: envelope.accessToken)),
      currentUser: envelope.user,
      koala: current.koala |> Koala.lens.loggedInUser .~ envelope.user
    )
  }

  /**
   Invoke when we have acquired a fresh current user and you want to replace the current environment's
   current user with the fresh one.

   - parameter user: A user model.
   */
  public static func updateCurrentUser(_ user: User) {
    replaceCurrentEnvironment(
      currentUser: user,
      koala: current.koala |> Koala.lens.loggedInUser .~ user
    )
  }

  public static func updateConfig(_ config: Config) {
    replaceCurrentEnvironment(
      config: config,
      koala: AppEnvironment.current.koala |> Koala.lens.config .~ config
    )
  }

  // Invoke when you want to end the user's session.
  public static func logout() {
    let storage = AppEnvironment.current.cookieStorage
    storage.cookies?.forEach(storage.deleteCookie)

    replaceCurrentEnvironment(
      apiService: AppEnvironment.current.apiService.logout(),
      cache: type(of: AppEnvironment.current.cache).init(),
      currentUser: nil,
      koala: current.koala |> Koala.lens.loggedInUser .~ nil
    )
  }

  // The most recent environment on the stack.
  public static var current: Environment! {
    return stack.last
  }

}

Then whenever there’s event that triggers dependencies update, we call it like

self.viewModel.outputs.logIntoEnvironment
  .observeValues { [weak self] accessTokenEnv in
    AppEnvironment.login(accessTokenEnv)
    self?.viewModel.inputs.environmentLoggedIn()
}

The cool thing about Environment is that we can store and retrieve them

// Returns the last saved environment from user defaults.
public static func fromStorage(ubiquitousStore: KeyValueStoreType,
                                 userDefaults: KeyValueStoreType) -> Environment {
  // retrieval

}

And we can mock in tests

AppEnvironment.replaceCurrentEnvironment(
  apiService: MockService(
    fetchDiscoveryResponse: .template |> DiscoveryEnvelope.lens.projects .~ [
      .todayByScottThrift,
      .cosmicSurgery,
      .anomalisa
    ]
  )
)