DependencyFetcher
World's simplest dependency injection framework for Swift.
- Thread-safe.
- Just ~60 source lines of code.
- Works on any platform, including Linux, doesn't depend on Foundation.
- Doesn't use global state.
- Doesn't require any configuration at all.
Requirements
- macOS / iOS / watchOS / tvOS / Linux
- Swift 4.2+
Usage
Define the services
protocol Logger {
// ...
}
protocol Database {
// ...
}
class StdOutLogger: Logger {
init() { /*...*/ }
}
class CoreDataDatabase: Database {
static func setup() -> CoreDataDatabase { /*...*/ }
}
Service
class
Extend the extension Service where T == Logger {
static let logger = Service(StdOutLogger.init)
static let database = Service(CoreDataDatabase.setup)
}
Fetch the dependencies in application code
Create an instance of Fetcher
somewhere in your code:
let fetcher = Fetcher.createDefault()
You can then pass this instance around using, e. g., injection via initializer.
That's right, you don't have to register your services anywhere except defining them as static properties of the Service
class.
Then, when you need your dependencies, just call:
let logger = self.fetcher.fetch(.logger) // logger has the type Logger
let database = self.fetcher.fetch(.database) // database has the type Database
Mock services in tests
Your application code doesn't need to (and even should not) be aware of whether it is being executed during testing or not. When you need to mock your dependencies, just create a Fetcher
instance and override the needed services with their mocks:
let fetcher = Fetcher
.create()
.addOverride(for: Service.logger, instance: MockLogger())
.addOverride(for: Service.database, instance: MockDatabase())
.done()
And then pass this instance to the units you're testing.
Factoring out hard-coded dependencies
Often you have a large codebase that uses hard-coded dependencies (singletons etc.) all over the place, and you just can't refactor all your code at once. DependencyFetcher
can be introduced to your codebase incrementally.
Suppose you have a view controller that does this (quite common MVC code):
class ViewController: UIViewControlelr {
var products: [Product] = []
override func viewDidLoad() {
super.viewDidLoad()
APIEndpoint.default.getProducts(onSuccess: { self.products = $0 },
onError: {error in /*...*/ })
}
}
This is really hard to test. We can refactor a little bit:
protocol APIEndpointProtocol {
func getProducts(onSuccess: @escaping ([Product]) -> Void,
onError: @escaping (Error) -> Void)
}
extension APIEndpoint: APIEndpointProtocol {}
extension Service where T == APIEndpointProtocol {
static let apiEndpoint = Service(APIEndpoint.default)
}
class ViewController: UIViewControlelr {
var products: [Product] = []
var fetcher = Fetcher.createDefault()
private lazy var apiEndpoint = self.fetcher.fetch(.apiEndpoint)
override func viewDidLoad() {
super.viewDidLoad()
self.apiEndpoint.getProducts(onSuccess: { self.products = $0 },
onError: { error in /*...*/ })
}
}
And now we can easily test our ViewController
:
class MockAPIEndpoint: APIEndpointProtocol {
let testProducts: [Product] = [
// ...
]
func getProducts(onSuccess: @escaping ([Product]) -> Void,
onError: @escaping (Error) -> Void) {
onSuccess(self.testProducts)
}
}
func testPopulatingViewControllerWithProducts() {
let vc = ViewController()
let mock = MockAPIEndpoint()
vc.fetcher = Fetcher
.create()
.addOverride(for: Service.apiEndpoint, instance: mock)
.done()
vc.viewDidLoad()
XCTAssertEqual(vc.products, mock.testProducts)
}
Installation
Swift Package Manager
Add the following to your Package.swift
:
dependencies: [
.package(url: "https://github.com/broadwaylamb/DependencyFetcher.git",
from: "1.0.0"),
]
CocoaPods
Add the following to your Podfile
:
target 'MyApp' do
pod 'DependencyFetcher', '~> 1.0'
end