You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -7,128 +7,163 @@ Effortless modular dependency injection for Swift.
7
7
8
8
___
9
9
10
-
Sometimes during the app development process we need to replace instances of classes or actors we use in production code with instances that emulate their work e.g. tests, SwiftUI previews, demo apps etc.
11
-
12
-
Ususally that requires additional changes in the code that in turn opens up a whole new layer of errors. handling of theese errors is on your shoulders.
10
+
Effortless modular dependency injection for Swift.
13
11
14
-
Inject lets you express your intent in a way that enables compile-time checking that you have all the instances required for the production.
15
-
At the same time it let's you replace the instance on any object with a single line of code.
12
+
Inject is an opinionated approach to dependency inversion, aiming to make dependency injection in Swift projects effortless and error-free. It allows you to invert dependencies in your application to the degree that you can freely move injection points across your modules without any issues. Inject's API is focused on simplicity and modularity while ensuring thread safety with @MainActor.
16
13
14
+
## Features
17
15
18
-
## How to Use
19
-
Here's an example of a class that needs networking and parser instances to fetch items from the server:
20
-
```swift
21
-
finalclassBackendSDK {
16
+
- Thread safety using @MainActor
17
+
- Simple, modular, and flexible API
18
+
- Compile-time checks for provided instances, eliminating an entire layer of errors
19
+
- Supports shared, singleton and on demand [injection strategies](#strategies)
And here's an example of replacing one of the services with a mock in SwiftUI preview:
24
+
Consider a protocol Service with a function doWork. This service is extracted into the Service package, along with its current implementation, ServiceImplementation.
36
25
37
26
```swift
27
+
// ServicePackage
28
+
publicprotocolService {
29
+
funcdoWork()
30
+
}
38
31
39
-
structSomeView: View {
40
-
@Injected(\.networking) var network
41
-
...
32
+
publicfinalclassServiceImplementation: Service {
33
+
funcdoWork() { ... }
42
34
}
35
+
```
43
36
44
-
extensionSomeView: Injectable {}
37
+
In your application, you want to use ServiceImplementation but have the option to replace it with a mock implementation for tests or previews. To do this, you need to publish the injection of the implementation:
With this convenient property wrapper `@Injected` you define a dependency requirement in a declarative way:
55
-
-`\.networking` is the `KeyPath` to the instance to be obtained at [`\DefaultValues.networking`. ](#default-values)
56
-
57
-
-`network` is the name of our injection point that we use to inject in preview `.injecting(MockNetworking(), for: \.network)`.
58
-
It behaves just like a normal variable would, with one exception, instead of providing an instance it provides a `Dependency<T>` wrapper, that has a computed property `.instance` to obtain an actual instance of type `T`.
46
+
This creates an injection point that instantiates ServiceImplementation once and deallocates it once no one is using the instance.
59
47
60
-
-**MockNetworking** - A class that we use only in tests or previews, that might simulate the network.
48
+
To use it in your app, you need the `Instance` property wrapper:
61
49
62
-
- Note: *We have to mark our view with an empty protocol `Injectable` to enable the `injecting(_:_:)` function.*
50
+
```swift
51
+
importServicePackage
63
52
64
-
**That's it, you are done with dependency injection without a need to know what exactly that is.**
53
+
@Instance(ServicePackage.serviceShared) var service
65
54
55
+
...
56
+
{
57
+
service.doWork()
58
+
}
59
+
}
60
+
```
66
61
67
-
The only thing that is missing is to tell the compiler what are the default values for our `\.networking` and `\.parser` dependencies. And that's where `DefaultValues` come in handy.
62
+
Anywhere you import the package that published the Injection, you can have the appropriate instance of the dependency.
68
63
69
-
## Default Values
64
+
## Strategies
70
65
71
-
**Unlike other popular solutions Inject doesn't have a container** instead it provides you with a `DefaultValues` class to extend with computed properties.
66
+
In the new version of Inject, there are three strategies available for managing dependency injection:
72
67
73
-
**You never need to create an instance of this class**, all you need to do is to extend it with the variable of the type of the dependency it represents and return a default implementation for it:
68
+
**Shared:** This strategy creates a shared instance that is reused across all objects using the dependency. The instance will be deallocated when the last object referencing it is deallocated.
**Singleton:** This strategy creates a shared instance that is reused across all objects using the dependency. The instance will remain in memory until the app is terminated.
If you noticed `networking` and `parser` are the names we referred to earlier.
86
+
**On Demand:** This strategy creates a new instance for each object using the dependency. Each instance will be deallocated when the object that holds the dependency is deallocated.
You might wonder, what is the lifespan of the instances provided? Do they stay in memory forever like singletons or they are destroyed when the object that has them `@Injected` is destroyed?
95
+
Choose the appropriate strategy based on your specific use case and requirements for dependency management.
94
96
95
-
And what is the scope, are all instances for all classes the same, or each class will have a new instance for its `@Injected`?
97
+
# Overriding
96
98
97
-
The answer is, by default, all the instances are created for each `@Injected` and are destroyed once the object that holds `@Injected` is destroyed.
99
+
There are two cases for overriding dependencies:
98
100
99
-
But you can change that with the `Scope` and `Lifespan`, default values would be:
101
+
## Global override
102
+
103
+
Use global override when you have multiple implementations and want to use different instances depending on the target. In this case, you can globally override the injection with a new strategy, instance, or injection.
100
104
101
105
```swift
102
-
@Injected(\.networking, scope: .local, lifespan: .temporary) var network
- Inject doesn't resolve instances using a container, it doesn't have a container in the first place. Which is a huge advantage over other popular DI solutions for which it is the biggest bottleneck.
125
-
- Compile-time check that all the instances provided, which removes a whole layer of errors.
126
-
- Inject's API operates simple concepts like instance, injection/replacement, scope, and lifespan.
127
-
- It's extremely modular, you one-line it anywhere you want.
There is much more than that, I will soon provide a couple of examples using inject in the app and in another library to showcase how powerful and flexible Inject is.
145
+
## Local override
130
146
131
-
Meanwhile, it's a good candidate for you to try and make your dependency injection a breeze.
147
+
You can also directly override the dependency for the instance only, by directly assigning the override:
148
+
149
+
```swift
150
+
importServicePackage
151
+
classConsumer {
152
+
@Instance(ServicePackage.serviceShared) var service
153
+
154
+
funcperform() ->String {
155
+
service.doWork()
156
+
}
157
+
}
158
+
159
+
functest_Override_Local() {
160
+
let sut =Consumer()
161
+
sut.service=MockService()
162
+
163
+
// now MockService will be used by consumer
164
+
XCTAssertEqual(sut.perform(), "test")
165
+
}
166
+
```
132
167
133
168
134
169
# Installation
@@ -141,5 +176,43 @@ Inject is designed for Swift 5. To depend on the Inject package, you need to dec
Inject 1.0.0 is now deprecated. We made a mistake by placing the injection configuration on the consumer side with @Injected. This approach leads to moving the responsibility of injection onto the client, which doesn't make sense because you would have to synchronize all the injections on your own. If you use this approach, move the configuration to the injection. Replace @Injected property wrappers with @Instance and reference the appropriate published Injection:
182
+
183
+
**For example, if you had:**
184
+
185
+
```swift
186
+
extensionDefaultValues {
187
+
var networking: Networking {
188
+
HTTPNetworking()
189
+
}
190
+
}
191
+
...
192
+
@Injected(\.networking, scope: .local, lifespan: .temporary) var network
193
+
```
194
+
195
+
**Change it to:**
196
+
197
+
```swift
198
+
// next to the implementation
199
+
@MainActorlet networking = Injection<Networking>(
200
+
strategy: .onDemand,
201
+
instance: NetworkingImplementation()
202
+
)
203
+
204
+
...
205
+
206
+
// usage
207
+
@Instance(networking) var network
208
+
```
209
+
210
+
This way, you can have a single source of truth for the injection configuration.
211
+
212
+
There are three new strategies and their respective scope and lifetime:
0 commit comments