PainlessInjection is a lightweight dependency injection framework for Swift.
Dependency injection (DI) is a software design pattern that implements Inversion of Control (IoC) for resolving dependencies. In the pattern, PainlessInjection helps your app split into loosely-coupled components, which can be developed, tested and maintained more easily. PainlessInjection is powered by the Swift generic type system and first class functions to define dependencies of your app simply and fluently.
To install PainlessInjection with Carthage, add the following line to your Cartfile.
github "yaroslav-zhurakovskiy/PainlessInjection"
CocoaPods
To install PainlessInjection with CocoaPods, add the following lines to your Podfile.
pod "PainlessInjection"
Swift Package Manager
You can use The Swift Package Manager to install PainlessInjection by adding the proper description to your Package.swift file:
```swift
// swift-tools-version:5.1
import PackageDescription
# Usage
## Simple dependencies
```swift
import PainlessInjection
// Define Services
class ServiceModule: Module {
override func load() {
define(Service.self) { InMemmoryService() }
}
}
// Load Modules
Container.load()
// Instantiate Service
let service = Container.get(type: Service.self)
// Using service
print(type(of: service))
Swift 5.1 @propertyWrapper feature
If you use Swift 5.1 you can take advantage of concise form of autowiring. Just add @Inject attribute to your class/struct property, the framework will resolve everything for you.
// You service
enum AuthServiceError: Error {
case serverIsDown
case tooManyAttempts
}
protocol AuthService: class {
func login(username: String, password: String, completion: @escaping (Result<Bool, AuthServiceError>) -> Void)
}
// Your controller that uses AuthService
class LoginController: UIViewController {
// AuthService will be resolved automatically based on loaded modules
@Inject var service: AuthService
@IBOutlet weak var usernameLabel: UILabel!
@IBOutlet weak var passwordLabel: UILabel!
@IBAction func login() {
service.login(
username: usernameLabel.text ?? "",
password: passwordLabel.text ?? ""
) { result in
// Implement the logic
// Redirect to home page
}
}
}
// Fake implementation that will enable you to test without the actual server
class FakeAuthService: AuthService {
func login(username: String, password: String, completion: @escaping (Result<Bool, AuthServiceError>) -> Void) {
guard username != "error" && password != "error" else {
completion(.failure(AuthServiceError.serverIsDown))
return
}
if username == "JohnDoe" && password == "132" {
completion(.success(true))
} else {
completion(.success(false))
}
}
}
// Module with fake services
class TestModule: Module {
func load() {
define(AuthService.self) { FakeAuthService() }
}
}
// Loading and testing
Container.load() // It will load TestModule
type(of: LoginController().service) == FakeAuthService.self // AuthService is automatically resolved to FakeAuthService
// Define Services
class ServiceModule: Module {
override func load() {
define(TaskService.self) { RestTaskService() }
}
}
// Define Controllers
class ControllersModule {
override func load() {)
define(EditTaskController.self) { args in
EditTaskController(task: args.optionalAt(0), service: self.resolve())
}
}
}
// Load Modules
Container.load()
// Passing nil
let controller2 = Container.get(type: EditTaskController.self, args: [nil as Any])
// Passing a task
let task = Task(name: "Code something")
let controller2 = Container.get(type: EditTaskController.self, args: [task])
Using singletone scope
Very often you do not want to recreate a service every time. You want to use some kind of a singletone. In order to do it you can use singletone scope.
import PainlessInjection
class ServiceModule: Module {
override func load() {
define(Service.self) { InMemmoryService() }.inSingletonScope()
}
}
// Use Service as a singletone
let service1 = Container.get(type: Service.self)
let service2 = Container.get(type: Service.self)
// service1 & service2 is the same object
service1 === service2
Using cache scope
If you want to cache an object for some time you can use cache scope.
import PainlessInjection
class ServiceModule: Module {
override func load() {
// Service will be cached for 10 minutes.
// After that it will be recreated again.
define(Service.self) { InMemmoryService() }.inCacheScope(interval: 10 * 60)
}
}
// Resolve Service type
let service1 = Container.get(type: Service.self)
let service2 = Container.get(type: Service.self)
// service1 & service2 is the same object
service1 === service2
// Wait for 10 minutes and 1 second
DispatchQueue.main.asyncAfter(deadline: .now() + 10 * 60 + 1) {
let service3 = Container.get(type: Service.self)
// service3 is different from service1 & service2
service3 != service1 && service3 != service2
}
Using decorators
You can add additional logic to the dependency creation process using decorators.
For example you can log every time a dependency is resolved.
import PainlessInjection
class PrintableDependency: Dependency {
private let original: Dependency
init(original: Dependency) {
self.original = original
}
var type: Any.Type {
return original.type
}
func create(_ args: [Any]) -> Any {
print("\(type) was created with", args, ".")
return original.create(args)
}
}
class UserModule: Module {
override func load() {
// Every time you will try to resolve User dependency it will be printed.
define(User.self) { User() }.decorate({ PrintableDependency(original: $0) })
}
}
When to load dependencies
Put Container.load() somewhere in application:didFinishLaunchingWithOptions method of your AppDelegate class.
import PainlessInjection
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
Container.load()
print(Container.loadedModules) // prints a list of loaded Modules
return true
}
}
Type inference
let service: Service = Container.get()
let user: User = Container.get("John", "Doe")
let anonymousUser: User = Container.get(nil as Any, nil as Any)
Using different dependencies for different app versions
Use “STORE_TARGET” environemnt variable to determine the app version.
import PainlessInjection
import Foundation
class StorePredicate: LoadModulePredicate {
private let value: String
init(valueMatching value: String) {
self.value = value
}
func shouldLoadModule() -> Bool {
guard let value = ProcessInfo.processInfo.environment["TARGET_STORE"] else {
assertionFailure("No TARGET_STORE env var was found!")
}
return value == self.value
}
}
This module will be loaded only if the app is running in the Canadian store.
PainlessInjection
PainlessInjection is a lightweight dependency injection framework for Swift.
Dependency injection (DI) is a software design pattern that implements Inversion of Control (IoC) for resolving dependencies. In the pattern, PainlessInjection helps your app split into loosely-coupled components, which can be developed, tested and maintained more easily. PainlessInjection is powered by the Swift generic type system and first class functions to define dependencies of your app simply and fluently.
Installation
PainlessInjection is avaialble via Carthage, CocoaPods or Swift Package Manager.
Requirements
Carthage
To install PainlessInjection with Carthage, add the following line to your Cartfile.CocoaPods
To install PainlessInjection with CocoaPods, add the following lines to your Podfile.Swift Package Manager
You can use The Swift Package Manager to install PainlessInjection by adding the proper description to your Package.swift file: ```swift // swift-tools-version:5.1 import PackageDescriptionlet package = Package( name: “YOUR_PROJECT_NAME”, dependencies: [ .package(url: “https://github.com/yaroslav-zhurakovskiy/PainlessInjection.git", from: “1.3.0”), ] )
Swift 5.1 @propertyWrapper feature
If you use Swift 5.1 you can take advantage of concise form of autowiring. Just add @Inject attribute to your class/struct property, the framework will resolve everything for you.
Dependencies with arguments and subdependencies
Dependencies with optional arguments
Using singletone scope
Very often you do not want to recreate a service every time. You want to use some kind of a singletone. In order to do it you can use singletone scope.
Using cache scope
If you want to cache an object for some time you can use cache scope.
Using decorators
You can add additional logic to the dependency creation process using decorators. For example you can log every time a dependency is resolved.
When to load dependencies
Put Container.load() somewhere in application:didFinishLaunchingWithOptions method of your AppDelegate class.
Type inference
Using different dependencies for different app versions
Using preprocessor macros.
This module will be loaded only for debug builds.
This module will be loaded only for release builds.
Using LoadingPredicate.
Use “STORE_TARGET” environemnt variable to determine the app version.
This module will be loaded only if the app is running in the Canadian store.
This module will be loaded only if the app is running in the USA store.
Loading the service