Skip to content

Commit 76e5d0d

Browse files
authored
Update README with a better intro (#4)
1 parent ec75006 commit 76e5d0d

File tree

1 file changed

+82
-78
lines changed

1 file changed

+82
-78
lines changed

README.md

Lines changed: 82 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -14,104 +14,122 @@ Ususally that requires additional changes in the code that in turn opens up a wh
1414
Inject lets you express your intent in a way that enables compile-time checking that you have all the instances required for the production.
1515
At the same time it let's you replace the instance on any object with a single line of code.
1616

17-
## Usage
1817

19-
A very common problem would be `Image` like component, that takes an `URL` as a parameter instead of an image. For this we would need two parts:
20-
21-
A downloader, with code something like:
18+
## How to Use
19+
Here's an example of a class that needs networking and parser instances to fetch items from the server:
2220
```swift
23-
protocol DownloaderInterface {
24-
func downloadImage(url: URL) async throws -> UIImage
25-
}
21+
final class BackendSDK {
22+
23+
@Injected(\.networking) var network
24+
@Injected(\.parser) var parser
2625

27-
actor URLSessionDownloader: DownloaderInterface {
28-
func downloadImage(url: URL) async throws -> UIImage {
29-
// URLSession/dataTask ...
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)
3031
}
3132
}
3233
```
3334

34-
and a component itself
35+
And here's an example of replacing one of the services with a mock in SwiftUI preview:
3536

3637
```swift
37-
struct RemoteImage: View, Injectable {
38-
private var downloader = URLSessionDownloader()
39-
@State private var image: UIImage?
40-
41-
let url: URL
42-
43-
var body: some View {
44-
Image(uiImage: image ?? placeholder)
45-
.resizable()
46-
.frame(width: 200, height: 200)
47-
.onAppear {
48-
downloadImage()
49-
}
50-
}
5138

52-
func downloadImage() {
53-
Task { [url] in
54-
self.image = try? await downloader.downloadImage(url: url)
55-
}
56-
}
39+
struct SomeView: View {
40+
@Injected(\.networking) var network
41+
...
5742
}
58-
```
5943

60-
I used SwiftUI here for simplicity, Inject doesn't require anything but `Swift 5.7`.
44+
extension SomeView: Injectable {}
6145

62-
Now the first problem is preview, now it will need an actual `URL` and a network connection, which is not what we want.
46+
struct SomeView_Previews: PreviewProvider {
47+
static var previews: some View {
48+
SomeView()
49+
.injecting(MockNetworking(), for: \.network)
50+
}
51+
}
52+
```
6353

64-
To enable injection we need to tell the compiler that we use `URLSessionDownloader` as a default instance for the protocol `DownloaderInterface`:
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`.
6559

66-
```swift
67-
import Inject
60+
- **MockNetworking** - A class that we use only in tests or previews, that might simulate the network.
6861

69-
extension DefaultValues {
70-
var imageDownloader: DownloaderInterface { URLSessionDownloader() }
71-
}
72-
```
62+
- Note: *We have to mark our view with an empty protocol `Injectable` to enable the `injecting(_:_:)` function.*
7363

74-
We also can now refer to it with a `KeyPath` `\DefaultValues.imageDownloader` which is very handy since we would need to replace it once if we changed from `URLSession` to something else.
64+
**That's it, you are done with dependency injection without a need to know what exactly that is.**
7565

76-
That's all we need to use the instance in our view:
7766

78-
```swift
79-
import Inject
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.
8068

81-
struct RemoteImage: View, Injectable {
82-
@Injected(\.imageDownloader) var downloader
83-
```
69+
## Default Values
8470

85-
We have to mark our view as `Injectable` which is an empty protocol enabling the `injecting(_:_:)` function. And tell which instance we need, `\.imageDownloader` is a short syntax for `\.DefaultValues.imageDownloader`.
71+
**Unlike other popular solutions Inject doesn't have a container** instead it provides you with a `DefaultValues` class to extend with computed properties.
8672

87-
Now in our preview, we can easily replace the production instance of the downloader with our mock that provides a static test image, error, and other cases we want to test.
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:
8874

8975
```swift
90-
actor MockDownloader: DownloaderInterface {
91-
func downloadImage(url: URL) async throws -> UIImage {
92-
return testImage
76+
extension DefaultValues {
77+
/// Default instance for Networking
78+
var networking: Networking {
79+
HTTPNetworking()
9380
}
94-
}
9581

96-
struct RemoteImage_Previews: PreviewProvider {
97-
static var previews: some View {
98-
RemoteImage(url: tesImageURL)
99-
.injecting(MockDownloader(), for: \.downloader)
82+
/// Default instance for Parser
83+
var parser: Parser {
84+
JSONParser()
10085
}
10186
}
10287
```
10388

104-
## Dependency Scope and Lifetime
89+
If you noticed `networking` and `parser` are the names we referred to earlier.
90+
91+
## Dependency configuration
92+
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?
94+
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`?
96+
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.
98+
99+
But you can change that with the `Scope` and `Lifespan`, default values would be:
105100

106-
By default, all the dependencies are **providing a new instance** (`.temporary`)
107-
and **for each injection point** (`.local`)
108-
and deallocated once an injection point is deallocated.
109101
```swift
110-
@Injected(\.networking, .temporary, .local) var network
102+
@Injected(\.networking, scope: .local, lifespan: .temporary) var network
111103
```
112104

113-
But you can alter it with `.shared` ``Dependency/Scope`` to provide the same instance to all consumers with `.shared` ``Dependency/Scope`` preferred.
114-
Also, you can configure a `.permanent` ``Lifespan`` to hold it until the termination of the app.
105+
here are the possible values:
106+
107+
### Scope
108+
109+
- `.local` - new instance for each `@Injected`
110+
- `.shared` - same instance for all `@Injected`
111+
112+
### Lifespan
113+
114+
- `.permanent` - instance stays till the app is deallocated.
115+
- `.temporary` - instance deallocated when the **last** `@Injected` referencing it is deallocated.
116+
117+
## Why yet another DI framework?
118+
119+
This is the question I asked the most.
120+
121+
Here are some of the reasons to try *Inject* and decide for yourself:
122+
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.
128+
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.
130+
131+
Meanwhile, it's a good candidate for you to try and make your dependency injection a breeze.
132+
115133

116134
# Installation
117135

@@ -123,19 +141,5 @@ Inject is designed for Swift 5. To depend on the Inject package, you need to dec
123141
.package(url: "https://github.com/MaximBazarov/Inject.git", from: "1.0.0")
124142
```
125143

126-
## Why yet another DI framework?
127-
This is the question I asked the most and I share your frustration with other DI solutions.
128-
In fact that's the very reason Inject was created.
129-
130-
Here are some of the reasons, and I'm not saying you should use Inject.
131-
I'm saying you have to try it and decide for yourself and that's why:
132-
133-
- Thread safety using `@MainActor`
134-
- Inject doesn't introduce a container instance.
135-
- No need to register instance in a container, defininition of a computed property with the instance instead.
136-
- Compile-time check that all the instances provided, which removes a whole layer of errors.
137-
- Inject's API operates simple concepts like instance, injection/replacement, scope and lifetime.
138-
- Enables you to keep your code modular for free.
139144

140-
I believe all that makes it a good candidate to try and make your opinion.
141145

0 commit comments

Comments
 (0)