Injection
Table of Contents
Introduction
This package was developed to address one of the most challenging topics in the Swift community: dependency injection. I often found it difficult to grasp this concept and implement it effectively. Through extensive research, I discovered a straightforward method inspired by the @Environment
property wrapper in SwiftUI. A defining feature of this package is its simplicity, making it easily understandable for anyoney.
Key issues addressed by this package include:
- Simplifying the process of mocking data for tests.
- Maintaining readability by adhering closely to Swift’s standard APIs.
- Ensuring compile-time safety to prevent hidden crashes, so if the application builds, all dependencies are correctly configured.
- Avoiding large initializers caused by dependency injection.
- Relieving the AppDelegate from being the central point for defining all shared instances.
- Minimizing potential learning curves.
- Eliminating the need for force unwrapping.
- Allowing the definition of standard dependencies without exposing private or internal types within packages.
How to use
Let's say we have a protocol called MotorBike
(why motorBike because I love them 😍😅) that contains a funcation called move
:
protocol MotorBike {
func move() -> Int
}
And there is a struct called Ducati
that conforms to MotorBike
:
struct Ducati: MotorBike {
func move() -> Int {
return 5
}
}
At this point we want to inject this object to another object using dependency injection design pattern to do so, the first step is to create a key (like the @Environment
):
private struct DucatiKey: InjectionKey {
static var currentValue: MotorBike = Ducati()
}
Then you will add this key to the InjectedValues
struct:
extension InjectedValues {
var ducatiProvider: MotorBike {
get { Self[DucatiKey.self] }
set { Self[DucatiKey.self] = newValue }
}
}
To be able to use it, you will just write this where ever you need it:
struct Garage {
@Injected(\.ducatiProvider) var ducati: MotorBike
}
And that's it, simple right 🚀.
[!NOTE]
When ever you call the@Injected
you will have the same instance throught the life cycle of the app. To prevent any side effects and weird outcomes from happening due to inconsistent dependency references.
[!NOTE]
Plus you can easily change the instance by writing:let garage = Garage() garage.ducati = Ducati() //OR InjectedValues[\.ducatiProvider] = Ducati()
Using the
InjectedValues
static subscript also affects already injected properties.
Testing
Testing your code is more easier than you think, you will just have to change the injected properties with a mock one using InjectedValues
static subscript, like so:
final class MotorBikeTests: XCTestCase {
override func setUp() {
InjectedValues[\.ducatiProvider] = DuctiMock()
}
func test() {
let garge = Garage()
let result = garge.move()
XCTAssertEqual(10, result)
}
}
class DuctiMock: MotorBike {
func move() -> Int {
return 10
}
}
that's it for how to test your injected properties. Easey right ? 😎
Resources
Some helpful links for you:
- https://developer.apple.com/documentation/swiftui/environment
- https://www.avanderlee.com/swift/dependency-injection/
- https://www.avanderlee.com/swift/property-wrappers/
Author
This pacakge was created by Eng.Omar Elsayed to help the iOS comuntity and make there life easir. To contact me email me at eng.omar.elsayed@hotmail.com