|
1 | 1 | # Astroject |
2 | | -Library for Dependency Injection with Swift concurrency in mind. |
| 2 | + |
| 3 | +A lightweight and flexible dependency injection container for Swift. |
| 4 | + |
| 5 | +[](https://swift.org) |
| 6 | +[](LICENSE) |
3 | 7 |
|
4 | 8 | ## Overview |
5 | | -Astroject is an advanced dependency injection library designed to help make dependency trees manageable and sufficient from the smallest to the largest scaled software project. |
| 9 | +Astroject is designed to simplify dependency management in your Swift projects. It provides a clean and intuitive API for registering and resolving dependencies, supporting both synchronous and asynchronous factories, various instance scopes (singleton, prototype, weak), and extensible behaviors. Many ideas came from a Sister Library [Swinject](https://github.com/Swinject/Swinject) |
| 10 | + |
| 11 | +## API Documentation |
| 12 | +Coming Soon... |
6 | 13 |
|
7 | 14 | ## Features |
8 | | -- **Thread Safety** |
9 | | -- **Swift Concurrency** |
10 | | -- **Ease of Use & Adaptation** |
| 15 | +- **Synchronous and Asynchronous Registrations:** Register dependencies with both synchronous and asynchronous factory closures. |
| 16 | +- **Instance Scopes:** Supports singleton, prototype, and weak instance scopes. |
| 17 | +- **Named Registrations:** Register dependencies with optional names for disambiguation. |
| 18 | +- **Circular Dependency Detection:** Prevents and reports circular dependency issues. |
| 19 | +- **Extensible Behaviors:** Add custom behaviors to the container's registration process. |
| 20 | +- **Assemblies:** Organize registrations into reusable modules. |
| 21 | +- **Thread-Safe Operations:** Designed for safe concurrent access. |
| 22 | + |
| 23 | +## Requirements |
| 24 | +- iOS 16.0+ / MacOS 13.0+ / WatchOS ??.?+ / tvOS ??.?+ |
| 25 | +- Swift 5.5+ |
| 26 | +- Xcode 16.0+ |
| 27 | + |
| 28 | +## Installation |
| 29 | + |
| 30 | +### Swift Package Manager |
| 31 | + |
| 32 | +Add Astroject as a dependency to your `Package.swift` file: |
| 33 | + |
| 34 | +```swift |
| 35 | +dependencies: [ |
| 36 | + .package(url: "https://github.com/astro-bytes/Astroject", from: "1.0.0") |
| 37 | +] |
| 38 | +``` |
| 39 | +Then, import Astroject in your Swift files: |
| 40 | +```swift |
| 41 | +import Astroject |
| 42 | +``` |
| 43 | + |
| 44 | +## Usage |
| 45 | +### Basic Registration and Resolution |
| 46 | +This process is the most common usage of Astroject. Registering and resolving synchronous objects. |
| 47 | + |
| 48 | +```swift |
| 49 | +import Astroject |
| 50 | + |
| 51 | +protocol Service { |
| 52 | + func doSomething() |
| 53 | +} |
| 54 | + |
| 55 | +class ConcreteService: Service { |
| 56 | + func doSomething() {} |
| 57 | +} |
| 58 | + |
| 59 | +let container = Container() |
| 60 | + |
| 61 | +// Register a dependency |
| 62 | +try container.register(Service.self) { _ in |
| 63 | + ConcreteService() |
| 64 | +} |
| 65 | + |
| 66 | +// Resolve the dependency |
| 67 | +let service: Service = try container.resolve(Service.self) |
| 68 | +service.doSomething() |
| 69 | +``` |
| 70 | + |
| 71 | +### Asynchronous Registration |
| 72 | +Asynchronous registration primary supports any initializer that needs to run asynchronously. This tends to happen often with classes wrapped with @MainActor. |
| 73 | + |
| 74 | +```swift |
| 75 | +import Astroject |
| 76 | + |
| 77 | +protocol AsyncService { |
| 78 | + func doWork() |
| 79 | +} |
| 80 | + |
| 81 | +@MainActor |
| 82 | +class ConcreteAsyncService: AsyncService { |
| 83 | + func doWord() {} |
| 84 | +} |
| 85 | + |
| 86 | +let container = Container() |
| 87 | + |
| 88 | +// Register an asynchronous dependency |
| 89 | +try container.registerAsync(AsyncService.self) { _ in |
| 90 | + await ConcreteAsyncService() |
| 91 | +} |
| 92 | + |
| 93 | +// Resolve the dependency asynchronously |
| 94 | +let asyncService: AsyncService = try await container.resolveAsync(AsyncService.self) |
| 95 | +asyncService.doWork() |
| 96 | +``` |
| 97 | + |
| 98 | +### Named Registrations |
| 99 | +Additionally with Synchronous or Asynchronous Registrations you can provide a name attribute to ensure the registration is uniquely stored when storing the same type more than once. |
| 100 | +```swift |
| 101 | +import Astroject |
| 102 | + |
| 103 | +protocol Database { |
| 104 | + func connect() |
| 105 | +} |
| 106 | + |
| 107 | +class MySQLDatabase: Database { |
| 108 | + func connect() {} |
| 109 | +} |
| 110 | + |
| 111 | +class PostgreSQLDatabase: Database { |
| 112 | + func connect() {} |
| 113 | +} |
| 114 | + |
| 115 | +let container = Container() |
| 116 | + |
| 117 | +try container.register(Database.self, name: "mysql") { _ in |
| 118 | + MySQLDatabase() |
| 119 | +} |
| 120 | + |
| 121 | +try container.register(Database.self, name: "postgresql") { _ in |
| 122 | + PostgreSQLDatabase() |
| 123 | +} |
| 124 | + |
| 125 | +let mysql: Database = try container.resolve(Database.self, name: "mysql") |
| 126 | +mysql.connect() |
| 127 | + |
| 128 | +let postgresql: Database = try container.resolve(Database.self, name: "postgresql") |
| 129 | +postgresql.connect() |
| 130 | +``` |
| 131 | + |
| 132 | +### Intuitive Registration |
| 133 | +Coming Soon... |
| 134 | + |
| 135 | +### Resource File Registration |
| 136 | +Coming Soon... |
| 137 | + |
| 138 | +### Overriding Registrations |
| 139 | +Astroject naturally supports overridable registrations, meaning that when one registration is registered under the same type multiple times the latest registration will be used. |
| 140 | + |
| 141 | +Here we will first register 42 as an Int in container and then immediately register 99 under the same key. |
| 142 | +```swift |
| 143 | +let container = Container() |
| 144 | +try container.register(Int.self) { _ in 42 } |
| 145 | +try container.register(Int.self) { _ in 99 } |
| 146 | +let value = container.resolve(Int.self) // Returns 99 |
| 147 | +``` |
| 148 | +As you can see the value will no longer only return 99. |
| 149 | +This is a powerful natural methodology. Allowing for your code to automatically override objects if you need to inject a Mocked Version of your class when running unit tests. |
| 150 | + |
| 151 | +Additionally we also provide a way to prevent this natural overriding if you ever want to enforce only one registration at a time. This can be done during the registration process like such. |
| 152 | +```swift |
| 153 | +let container = Container() |
| 154 | +container.register(Int.self, overridable: false) { _ in 42} |
| 155 | +``` |
| 156 | +Now if we try to register another Int anywhere else during our registration process the register will fail and throw a `RegistrationError.alreadyRegistered` error. This gives you as the developer more control over registrations and their flows. |
| 157 | +```swift |
| 158 | +try container.register(Int.self) { _ in 99 } // Fails and throws an error |
| 159 | +``` |
| 160 | + |
| 161 | +### Arguments |
| 162 | +Coming soon... |
| 163 | + |
| 164 | +### Instance Scopes |
| 165 | +Astroject out of the box supports the following instance scopes. |
| 166 | +```swift |
| 167 | +class MyClass { |
| 168 | + init() { |
| 169 | + print("MyClass initialized") |
| 170 | + } |
| 171 | +} |
| 172 | + |
| 173 | +let container = Container() |
| 174 | +``` |
| 175 | +- Prototype(default) - A new instance is generated through every resolve for that object. |
| 176 | + ```swift |
| 177 | + try container.register(MyClass.self, name: "prototype") { _ in |
| 178 | + MyClass() |
| 179 | + } |
| 180 | + |
| 181 | + // Output: MyClass initialized |
| 182 | + let prototypeInstance1: MyClass = try container.resolve(MyClass.self, name: "prototype") |
| 183 | + |
| 184 | + // Output: MyClass initialized (new instance) |
| 185 | + let prototypeInstance2: MyClass = try container.resolve(MyClass.self, name: "prototype") |
| 186 | + ``` |
| 187 | +- Singleton - Instance maintained and kep throughout the life time of the container. |
| 188 | + ```swift |
| 189 | + try container.register(MyClass.self) { _ in |
| 190 | + MyClass() |
| 191 | + } |
| 192 | + .asSingleton() |
| 193 | + |
| 194 | + // Output: MyClass initialized |
| 195 | + let instance1: MyClass = try container.resolve(MyClass.self) |
| 196 | + |
| 197 | + // No output (same instance) |
| 198 | + let instance2: MyClass = try container.resolve(MyClass.self) ``` |
| 199 | +- Weak - As long as you retain an instance to the object the instance remains in the container when asked for. If you have no references to the class then container will deallocate its reference. |
| 200 | + ```swift |
| 201 | + try container.register(MyClass.self) { _ in |
| 202 | + MyClass() |
| 203 | + } |
| 204 | + .asWeak() |
| 205 | + |
| 206 | + // Output: MyClass initialized |
| 207 | + var instance1: MyClass? = try container.resolve(MyClass.self) |
| 208 | + // No output (same instance) |
| 209 | + var instance2: MyClass? = try container.resolve(MyClass.self) |
| 210 | + |
| 211 | + // Release instance1 reference |
| 212 | + instance1 = nil |
| 213 | + |
| 214 | + // No output (same instance) because instance2 was not deallocated |
| 215 | + var instance3: MyClass? = try container.resolve(MyClass.self) |
| 216 | + |
| 217 | + // Release instance 2 & 3 reference |
| 218 | + instance2 = nil |
| 219 | + instance3 = nil |
| 220 | + |
| 221 | + // Output: MyClass initialized |
| 222 | + let instance4: MyClass = try container.resolve(MyClass.self) |
| 223 | + ``` |
| 224 | +#### Custom Scopes |
| 225 | +Additionally you can create your own Scopes through utilization of the `Instance` protocol and the `as` function on `any Registrable`. |
| 226 | +```swift |
| 227 | +class ExampleInstance: Instance { |
| 228 | + // Conform to Instance methods |
| 229 | +} |
| 230 | + |
| 231 | +// Insert through `as` function |
| 232 | +container.register(Int.self) { _ in 42 }.as(ExampleInstance()) |
| 233 | +``` |
| 234 | +Convenience functions can also be created by extending `Registrable` |
| 235 | +```swift |
| 236 | +extension Registrable { |
| 237 | + @discardableResult |
| 238 | + func exampleInstance() -> Self { |
| 239 | + self.as(ExampleInstance()) |
| 240 | + } |
| 241 | +} |
| 242 | +``` |
| 243 | + |
| 244 | +If you need a combination of multiple scopes just create the scopes you need then add them to our `Composite` Instance object. `Composite` takes the first instance not nil from a list of `Instance` objects. |
| 245 | +```swift |
| 246 | +let composite = Composite([ExampleInstance(), Weak()]) |
| 247 | +container.register(Int.self) { _ in 42 }.as { composite } |
| 248 | +``` |
| 249 | + |
| 250 | +### Behaviors |
| 251 | +Behaviors allow for additional functionality in the container. They are applied to each registration as they are registered. |
| 252 | + |
| 253 | +```Swift |
| 254 | +import Astroject |
| 255 | + |
| 256 | +class LoggingBehavior: Behavior { |
| 257 | + func didRegister<Product>( |
| 258 | + type: Product.Type, |
| 259 | + to container: Container, |
| 260 | + as registration: Registration<Product>, |
| 261 | + with name: String? |
| 262 | + ) { |
| 263 | + print("Registered \(type) with name: \(name ?? "nil")") |
| 264 | + } |
| 265 | +} |
| 266 | + |
| 267 | +let container = Container() |
| 268 | +container.add(LoggingBehavior()) |
| 269 | + |
| 270 | +// Output: Registered Int with name: 42 |
| 271 | +try container.register(Int.self, name: "42") { _ in 42 } |
| 272 | +``` |
| 273 | + |
| 274 | +### Nested Containers |
| 275 | +Coming Soon... |
| 276 | + |
| 277 | +### Assemblies |
| 278 | +Assemblies allow for modularized code structure and can be used to assemble how ever you would like. They provide a function where registration can best be structured as well as provide a function to hook into when all registrations are complete. |
| 279 | +```Swift |
| 280 | +import Astroject |
| 281 | + |
| 282 | +class MyAssembly: Assembly { |
| 283 | + func assemble(container: Container) { |
| 284 | + try? container.register(String.self) { _ in "Hello, Astroject!" } |
| 285 | + } |
| 286 | + |
| 287 | + func loaded(resolver: Resolver) { |
| 288 | + if let message: String = try? resolver.resolve(String.self) { |
| 289 | + print("Assembly loaded with message: \(message)") |
| 290 | + } |
| 291 | + } |
| 292 | +} |
| 293 | + |
| 294 | +let container = Container() |
| 295 | +let assembler = Assembler(container: container) |
| 296 | +// Output: Assembly loaded with message: Hello, Astroject! |
| 297 | +assembler.apply(assembly: MyAssembly()) |
| 298 | + |
| 299 | +let message: String = try container.resolve(String.self) |
| 300 | +// Output: Hello, Astroject! |
| 301 | +print(message) |
| 302 | +``` |
| 303 | + |
| 304 | +### Error Handling |
| 305 | +Astroject provides detailed error handling through the `ResolutionError`, `InstanceError` and `RegistrationError` enums. |
| 306 | + |
| 307 | +## Sample Code |
| 308 | +Checkout our sample code under the [playgrounds](/Playgrounds) directory. |
| 309 | + |
| 310 | +## Looking to the Future |
| 311 | +Listed below are features maintainers plan to bring to this library. |
| 312 | +- Add Sample Code via Playgrounds |
| 313 | +- Arguments - Pass in additional arguments to a factory or resolution |
| 314 | +- Parent Containers |
| 315 | +- Intuitive Registration - Registration is based on the initializer of an object |
| 316 | +- Resource File Registration - Automatically register object from bundle and resource files |
| 317 | +- DocC - Swift Documentation |
| 318 | + |
| 319 | +## Contributing |
| 320 | +See [documentation](CONTRIBUTING.md) for more details and guidelines. |
| 321 | + |
| 322 | +## Credits |
| 323 | +Astroject was inspired by |
| 324 | +- [Swinject](https://github.com/Swinject/Swinject). |
| 325 | +- [SwinjectAutoRegistration](https://github.com/Swinject/SwinjectAutoregistration) |
| 326 | +- [SwinjectPropertyLoader](https://github.com/Swinject/SwinjectPropertyLoader) |
11 | 327 |
|
| 328 | +## License |
| 329 | +Astroject is released under the MIT License. See [LICENSE](LICENSE) for details. |
0 commit comments