Learning from Open Source Managing dependencies

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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

1
2
3
4
5
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

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

}

And we can mock in tests

1
2
3
4
5
6
7
8
9
AppEnvironment.replaceCurrentEnvironment(
apiService: MockService(
fetchDiscoveryResponse: .template |> DiscoveryEnvelope.lens.projects .~ [
.todayByScottThrift,
.cosmicSurgery,
.anomalisa
]
)
)

Comments