Skip to content

Commit e002069

Browse files
committed
update docs
1 parent ab1e2da commit e002069

File tree

3 files changed

+189
-84
lines changed

3 files changed

+189
-84
lines changed

README.md

Lines changed: 150 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -7,128 +7,163 @@ Effortless modular dependency injection for Swift.
77

88
___
99

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.
1311

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.
1613

14+
## Features
1715

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-
final class BackendSDK {
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)
2220

23-
@Injected(\.networking) var network
24-
@Injected(\.parser) var parser
2521

26-
func fetchItems() async throws -> [Item] {
27-
guard let url = URL(string: baseURL + "/items")
28-
else { throw InvalidURL() }
29-
let data = try await network.instance.fetch(url)
30-
return try await parser.instance.parse(data, as: [Item].self)
31-
}
32-
}
33-
```
22+
# Usage
3423

35-
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.
3625

3726
```swift
27+
// ServicePackage
28+
public protocol Service {
29+
func doWork()
30+
}
3831

39-
struct SomeView: View {
40-
@Injected(\.networking) var network
41-
...
32+
public final class ServiceImplementation: Service {
33+
func doWork() { ... }
4234
}
35+
```
4336

44-
extension SomeView: 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:
4538

46-
struct SomeView_Previews: PreviewProvider {
47-
static var previews: some View {
48-
SomeView()
49-
.injecting(MockNetworking(), for: \.network)
50-
}
51-
}
39+
```swift
40+
@MainActor public let serviceShared = Injection<Service>(
41+
strategy: .shared,
42+
instance: ServiceImplementation()
43+
)
5244
```
5345

54-
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.
5947

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:
6149

62-
- Note: *We have to mark our view with an empty protocol `Injectable` to enable the `injecting(_:_:)` function.*
50+
```swift
51+
import ServicePackage
6352

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
6554

55+
...
56+
{
57+
service.doWork()
58+
}
59+
}
60+
```
6661

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.
6863

69-
## Default Values
64+
## Strategies
7065

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:
7267

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.
7469

7570
```swift
76-
extension DefaultValues {
77-
/// Default instance for Networking
78-
var networking: Networking {
79-
HTTPNetworking()
80-
}
71+
@MainActor let sharedDependency = Injection<DependencyType>(
72+
strategy: .shared,
73+
instance: DependencyImplementation()
74+
)
75+
```
8176

82-
/// Default instance for Parser
83-
var parser: Parser {
84-
JSONParser()
85-
}
86-
}
77+
**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.
78+
79+
```swift
80+
@MainActor let singletonDependency = Injection<DependencyType>(
81+
strategy: .singleton,
82+
instance: DependencyImplementation()
83+
)
8784
```
8885

89-
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.
9087

91-
## Dependency configuration
88+
```swift
89+
@MainActor let onDemandDependency = Injection<DependencyType>(
90+
strategy: .onDemand,
91+
instance: DependencyImplementation()
92+
)
93+
```
9294

93-
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.
9496

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
9698

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:
98100

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.
100104

101105
```swift
102-
@Injected(\.networking, scope: .local, lifespan: .temporary) var network
103-
```
106+
// Overriding the instance
107+
ServicePackage.serviceShared.override(with: { _ in MockService() })
104108

105-
here are the possible values:
109+
// Overriding the instance and strategy
110+
PackageA.sharedService.override(with: { _ in MockService() }, strategy: .onDemand)
106111

107-
### Scope
112+
// Overriding the instance and strategy, using the instance provided before override
113+
PackageA.sharedService.override(
114+
with: { service in
115+
service.setKey("someapikey")
116+
return service
117+
},
118+
strategy: .onDemand
119+
)
108120

109-
- `.local` - new instance for each `@Injected`
110-
- `.shared` - same instance for all `@Injected`
121+
// Overriding with another injection
122+
@MainActor public let serviceLocal = Injection<Service>(strategy: .onDemand, instance: ServiceImplementation())
111123

112-
### Lifespan
124+
PackageA.sharedService.override(with: serviceLocal)
125+
```
113126

114-
- `.permanent` - instance stays till the app is deallocated.
115-
- `.temporary` - instance deallocated when the **last** `@Injected` referencing it is deallocated.
127+
In your tests, you can also **rollback** the override when you're done testing:
116128

117-
## Why yet another DI framework?
129+
```swift
130+
func test_Override_WithInjection_Rollback_Global() {
131+
var sut = Consumer()
132+
let override = Injection<PackageA.Service>(strategy: .shared, instance: self.testInjection)
133+
PackageA.sharedService.override(with: override)
118134

119-
This is the question I asked the most.
135+
XCTAssertEqual(sut.perform(), "test")
120136

121-
Here are some of the reasons to try *Inject* and decide for yourself:
137+
XCTAssertEqual(ObjectIdentifier(sut.service), ObjectIdentifier(testInjection))
122138

123-
- Thread safety using `@MainActor`
124-
- 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.
139+
PackageA.sharedService.rollbackOverride()
140+
sut = Consumer()
141+
XCTAssertNotEqual(ObjectIdentifier(sut.service), ObjectIdentifier(testInjection))
142+
}
143+
```
128144

129-
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
130146

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+
import ServicePackage
151+
class Consumer {
152+
@Instance(ServicePackage.serviceShared) var service
153+
154+
func perform() -> String {
155+
service.doWork()
156+
}
157+
}
158+
159+
func test_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+
```
132167

133168

134169
# Installation
@@ -141,5 +176,43 @@ Inject is designed for Swift 5. To depend on the Inject package, you need to dec
141176
.package(url: "https://github.com/MaximBazarov/Inject.git", from: "1.0.0")
142177
```
143178

179+
# Deprecation Notice and Migration Guide
180+
181+
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+
extension DefaultValues {
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+
@MainActor let 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:
144213

214+
`.shared`: scope: `.shared`, lifespan: `.temporary`
215+
`.singleton`: scope: `.shared`, lifespan: `.permanent`
216+
`.onDemand`: scope: `.local`, lifespan: `.temporary`
145217

218+
Please review and adjust your code according to these updated strategies and scopes.

Sources/Instance.swift

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,26 @@
1515
import Foundation
1616

1717

18-
/// Provides an instance of the `Injection`.
18+
/// A property wrapper that enables dependency injection for a specified instance.
19+
///
20+
/// The Instance property wrapper retrieves an instance of the specified type from its
21+
/// associated ``Injection``.
22+
/// The retrieved instance is determined by the injection's configuration.
23+
///
24+
/// Usage:
25+
/// ```swift
26+
/// @Instance(networking) var network
27+
/// ```
28+
/// where `networking` is the ``Injection`` instance.
1929
@MainActor @propertyWrapper public final class Instance<O> {
2030

31+
// The associated Injection instance responsible for providing the instance of type O.
2132
let injection: Injection<O>
33+
34+
/// The context in which the property wrapper is used.
2235
let context: Context
2336

37+
// Local storage of the instance.
2438
var localInstance: O?
2539

2640
public var wrappedValue: O {
@@ -33,6 +47,15 @@ import Foundation
3347
}
3448
}
3549

50+
/// Initializes a new `Instance` property wrapper with the given `Injection` instance.
51+
///
52+
/// - Parameters:
53+
/// - injection: The associated `Injection` instance responsible for providing the instance of type `O`.
54+
/// - file: The source file where the operation takes place. Defaults to the calling function's file.
55+
/// - fileID: The unique identifier of the source file. Defaults to the calling function's file ID.
56+
/// - line: The line number within the source file where the operation takes place. Defaults to the calling function's line number.
57+
/// - column: The column number within the source file where the operation takes place. Defaults to the calling function's column number.
58+
/// - function: The name of the function where the operation takes place. Defaults to the calling function's name.
3659
public init(
3760
_ injection: Injection<O>,
3861
file: String = #file,

Sources/Telemetry/Context.swift

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,27 @@
1414

1515
import Foundation
1616

17-
/// Represents an operation source context. Intended to be used in logging.
17+
/// Represents the source context for an operation, such as a function call or a value read.
18+
/// The context is primarily used for logging purposes.
1819
///
19-
/// *Example:* `MyClass` reads value in one of its functions,
20-
/// the context for the reading operation would be that function call of the reading function.
21-
///
22-
/// **Parent context**: You can also link the parent context for the operation that require
23-
/// that information.
20+
/// For example, if MyClass reads a value within one of its functions, the context for
21+
/// the reading operation would be the function call of the reading function.
2422
public final class Context: Sendable {
2523
public let file: String
2624
public let fileID: String
2725
public let line: Int
2826
public let column: Int
2927
public let function: String
3028

29+
/// Creates a new context with the provided information.
30+
///
31+
/// - Parameters:
32+
/// - file: The source file where the operation takes place. Defaults to the calling function's file.
33+
/// - fileID: The unique identifier of the source file. Defaults to the calling function's file ID.
34+
/// - line: The line number within the source file where the operation takes place. Defaults to the calling function's line number.
35+
/// - column: The column number within the source file where the operation takes place. Defaults to the calling function's column number.
36+
/// - function: The name of the function where the operation takes place. Defaults to the calling function's name.
37+
3138
public init(file: String = #file, fileID: String = #fileID, line: Int = #line, column: Int = #column, function: String = #function) {
3239
self.file = file
3340
self.fileID = fileID
@@ -38,6 +45,8 @@ public final class Context: Sendable {
3845
}
3946

4047
extension Context: CustomDebugStringConvertible {
48+
49+
/// A textual representation of the context, including the file, line, and column information.
4150
public var debugDescription: String {
4251
"\(file):\(line):\(column)"
4352
}

0 commit comments

Comments
 (0)