-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathDependencies.swift
312 lines (258 loc) · 9 KB
/
Dependencies.swift
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
import Foundation
/// Simple mechanism for managing dependencies.
///
/// Supports static and dynamic registration of dependencies,
/// while both can be resolved via `@Dependency` property wrapper.
///
public struct Dependencies {}
// MARK: - Static
/// Key for accessing dependencies in different contexts.
///
/// Conform custom types to this protocol for static dependency registration.
/// Only `liveValue` is required to implement (used when running the app), while
/// `previewValue` and `testValue` will fallback to it if not explicitly defined.
///
/// Usage example:
///
/// // custom dependency type
/// protocol CustomDependency {}
///
/// // different implementations
/// final class LiveCustomDependency: CustomDependency {}
/// final class PreviewCustomDependency: CustomDependency {}
/// final class TestCustomDependency: CustomDependency {}
///
/// // make dependency accessible in different contexts
/// struct CustomDependencyKey: DependencyKey {
/// static var liveValue: CustomDependency = LiveCustomDependency()
/// static var previewValue: CustomDependency = PreviewCustomDependency()
/// static var testValue: CustomDependency = TestCustomDependency()
/// }
///
/// // register custom dependency using its `DependencyKey`
/// extension Dependencies {
/// var custom: CustomDependency {
/// get { Self[CustomDependencyKey.self] }
/// set { Self[CustomDependencyKey.self] = newValue }
/// }
/// }
///
/// // resolve custom dependency using a property wrapper
/// final class CustomViewModel {
/// @Dependency(\.custom) var custom
/// }
///
public protocol DependencyKey {
associatedtype Value
/// Dependency instance used when running the app.
static var liveValue: Value { get set }
/// Dependency instance used in Xcode Previews.
static var previewValue: Value { get set }
/// Dependency instance used in Xcode Simulator.
static var simulatorValue: Value { get set }
/// Dependency instance used when running tests.
static var testValue: Value { get set }
}
extension DependencyKey {
public static var previewValue: Value {
get { Self.liveValue }
set { Self.previewValue = newValue }
}
public static var simulatorValue: Value {
get { Self.liveValue }
set { Self.simulatorValue = newValue }
}
public static var testValue: Value {
get { Self.liveValue }
set { Self.testValue = newValue }
}
/// Dependency instance for the current context.
internal static var contextValue: Value {
get {
switch Dependencies.context {
case .live:
Self.liveValue
case .preview:
Self.previewValue
case .simulator:
Self.simulatorValue
case .test:
Self.testValue
}
}
set {
switch Dependencies.context {
case .live:
Self.liveValue = newValue
case .preview:
Self.previewValue = newValue
case .simulator:
Self.simulatorValue = newValue
case .test:
Self.testValue = newValue
}
}
}
}
extension Dependencies {
/// Environment context
public enum Context: String {
case live, preview, test, simulator
}
/// Current context.
public static var context: Context {
if ProcessInfo.isXcodePreview {
.preview
} else if ProcessInfo.isXcodeUnitTest || ProcessInfo.isXcodeUITest {
.test
} else if ProcessInfo.isXcodeSimulator {
.simulator
} else {
.live
}
}
private static var shared = Dependencies()
/// A static subscript for accessing dependency value in current context.
public static subscript<K>(_ key: K.Type) -> K.Value where K: DependencyKey {
get { key.contextValue }
set { key.contextValue = newValue }
}
/// A static subscript for direct access to dependency reference.
public static subscript<T>(_ keyPath: WritableKeyPath<Dependencies, T>) -> T {
get { shared[keyPath: keyPath] }
set { shared[keyPath: keyPath] = newValue }
}
}
// MARK: - Property Wrapper
/// A property wrapper for accessing / resolving dependencies.
///
/// Statically registered dependencies can be accessed by using their keyPath:
///
/// @Dependency(\.apiClient) var apiClient
///
/// Dynamically registered dependencies can be resolved by using their type:
///
/// @Dependency(ApiClient.self) var apiClient
///
@propertyWrapper
public struct Dependency<T> {
public var wrappedValue: T {
get {
switch kind {
case .static(let keyPath):
return Dependencies[keyPath]
case .dynamic(let type):
return Dependencies.resolve(type)
}
}
set {
switch kind {
case .static(let keyPath):
Dependencies[keyPath] = newValue
case .dynamic:
assertionFailure("⚠️ dynamic dependency does not support property wrapper setter")
}
}
}
public init(_ keyPath: WritableKeyPath<Dependencies, T>) {
self.init(.static(keyPath))
}
public init(_ type: T.Type) {
self.init(.dynamic(type))
}
internal init(_ kind: Kind) {
self.kind = kind
}
internal let kind: Kind
internal enum Kind {
case `static`(WritableKeyPath<Dependencies, T>)
case `dynamic`(T.Type)
}
}
// MARK: - Dynamic
public extension Dependencies {
// MARK: Facade
/// Registers a custom dependency which is resolved by creating a new instance.
///
/// - Parameters:
/// - type: type of the instance
/// - factory: closure called to create a new instance
///
/// Example usage:
///
/// Dependencies.registerFactory(CustomDependency.self, LiveCustomDependency())
///
static func registerFactory<T>(_ type: T.Type, _ factory: @autoclosure @escaping () -> T) {
container.register(.factory(type), factory: factory())
}
/// Registers a custom dependency which is resolved by returning the same instance.
///
/// - Parameters:
/// - type: type of the instance
/// - factory: closure called to create a new instance
///
/// Example usage:
///
/// Dependencies.registerSingleton(CustomDependency.self, LiveCustomDependency())
///
static func registerSingleton<T>(_ type: T.Type, _ factory: @autoclosure @escaping () -> T) {
container.register(.singleton(type), factory: factory())
}
static func resolve<T>(_ type: T.Type) -> T {
container.resolve(type)
}
// MARK: Implementation
private static let container = Container()
/// A simple `Container` used for registering and resolving dependencies dynamically.
private final class Container {
enum Dependency<T> {
case factory(T.Type)
case singleton(T.Type)
}
private var factories = [ObjectIdentifier: () -> Any]()
private var singletons = [ObjectIdentifier: Any]()
private var lock = NSRecursiveLock()
internal func register<T>(_ dependency: Dependency<T>, factory: @autoclosure @escaping () -> T) {
lock.lock()
defer { lock.unlock() }
let key = ObjectIdentifier(T.self)
switch dependency {
case .factory:
factories[key] = factory
case .singleton:
singletons[key] = factory()
}
}
internal func resolve<T>(_ type: T.Type) -> T {
lock.lock()
defer { lock.unlock() }
let key = ObjectIdentifier(T.self)
if let singleton = singletons[key] as? T {
return singleton
} else if let factory = factories[key], let newInstance = factory() as? T {
return newInstance
} else {
fatalError("❌ could not find instance for type: \"\(String(describing: type))\"")
}
}
}
}
// MARK: - Helpers
public extension ProcessInfo {
/// A flag which determines if code is run in the context of Xcode's "Live Preview"
static var isXcodePreview: Bool {
processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
}
/// A flag which determines if code is run in the context of Xcode's "Simulator"
static var isXcodeSimulator: Bool {
processInfo.environment["SIMULATOR_DEVICE_NAME"] != nil
}
/// A flag which determines if code is run in the context of Xcode's "Unit Tests"
static var isXcodeUnitTest: Bool {
processInfo.environment.keys.contains("XCTestConfigurationFilePath")
}
/// A flag which determines if code is run in the context of Xcode's "UI Tests"
static var isXcodeUITest: Bool {
processInfo.arguments.contains("UITests")
}
}