Deli is an easy-to-use Dependency Injection Container that creates DI containers with all required registrations and corresponding factories.
Wanna spaghetti? or not. As your project grows, will experience a complex. We can write the wrong code by mistake.
In Spring framework provides automatic registration using some code rules and throws the wrong Dependency Graph before running. I wanted these features to be in Swift.
Simple setup for the automated configuration files, deli.yml
.
If the configuration file does not exist, find the build target for a unique project in the current folders automatically. It works the same even if no scheme
or output
field is specified.
target:
- MyProject
config:
MyProject:
project: MyProject
scheme: MyScheme
include:
- Include files...
exclude:
- Exclude files...
className: DeilFactory
output: Sources/DeliFactory.swift
You’ll have to make your targets Shared
. To do this Manage Schemes
and check the Shared
areas:
Then build with the provided binaries.
$ deli build
Dependency Graph is configured through source code analysis. It is saved as the file you specified earlier.
File contents as below:
//
// DeliFactory.swift
// Auto generated code.
//
import Deli
final class DeliFactory: ModuleFactory {
override func load(context: AppContextType) {
...
}
}
Add the generated file to the project and call it from the app's launch point.
AppDelegate.swift:
import UIKit
import Deli
class AppDelegate {
var window: UIWindow?
let context = AppContext.load([
DeliFactory.self
])
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
return true
}
}
Integrate Deli into an Xcode scheme to get warnings and errors displayed in the IDE. Just add a new "Run Script Phase" with:
if which deli >/dev/null; then
deli build
else
echo "error: Deli not installed, download from https://github.com/kawoou/Deli"
fi
Alternatively, if you've installed Deli via CocoaPods the script should look like this:
"${PODS_ROOT}/DeliBinary/deli" build
The class, struct, and protocol can extend the Component
protocol and will be registered automatically in the DI container.
Component
can be used as below:
protocol UserService {
func login(id: String, password: String) -> User?
func logout()
}
class UserServiceImpl: UserService, Component {
func login(id: String, password: String) -> User? {
...
}
func logout() {
...
}
init() {}
}
If the above code is written, you can use the UserService
or UserServiceImpl
type to load the dependency instance.
The Autowired
protocol is registered automatically, same as Component
protocol. A difference, you can load the required dependencies from DI container.
Autowired
can be used as below:
class LoginViewModel: Autowired {
let userService: UserService
required init(_ userService: UserService) {
self.userService = userService
}
}
Easy right? So let's look at the code below.
protocol Book {
var name: String { get }
var author: String { get }
var category: String { get }
}
class Novel: Book {
var qualifier: String {
return "Novel"
}
var name: String {
return ""
}
var author: String {
return ""
}
var category: String {
return "Novel"
}
}
class HarryPotter: Novel, Component {
override var name: String {
return "Harry Potter"
}
override var author: String {
return "J. K. Rowling"
}
}
class TroisiemeHumanite: Novel, Component {
override var name: String {
return "Troisième humanité"
}
override var author: String {
return "Bernard Werber"
}
}
This code arranged the books through inheritance. You can get all of Book
instances like below:
class LibraryService: Autowired {
let books: [Book]
required init(_ books: [Book]) {
self.books = books
}
}
Furthermore, What should do to get the books with the "Novel" qualifier? In Deli, can be constructor injection in the below:
class LibraryService: Autowired {
let books: [Book]
required init(Novel books: [Book]) {
self.books = books
}
}
If we can remove whole Circular Dependency cases, the world will be better than before, but it cannot be ruled completely. A simple way to solve this problem is to initialize one of the dependency lazily.
Let's try LazyAutowired
protocol:
class UserService: Autowired {
let messageService: MessageService
required init(_ messageService: MessageService) {
self.messageService = messageService
}
}
class FriendService: Autowired {
let userService: UserService
required init(_ userService: UserService) {
self.userService = userService
}
}
class MessageService: Autowired {
let friendService: FriendService
required init(_ friendService: FriendService) {
self.friendService = friendService
}
}
If you try to inject a MessageService, Circular Dependency will occurred.
$ deli validate
Error: The circular dependency exists. (MessageService -> FriendService -> UserService -> MessageService)
What if UserService extends LazyAutowired
?
class UserService: LazyAutowired {
let messageService: MessageService!
func inject(_ messageService: MessageService) {
self.messageService = messageService
}
required init() {}
}
The cycle was broken and the issue was resolved!
After MessageService instance successfully created, dependencies can be injected via inject()
that UserService needed.
In addition, LazyAutowired can be specified qualifier like Autowired. Below code injects a UserService instance with the "facebook" qualifier specified:
class FacebookViewModel: LazyAutowired {
let userService: UserService!
func inject(facebook userService: UserService) {
self.userService = userService
}
required init() {}
}
The Configuration
protocol makes the user can register Resolver
directly.
Let's look at the code:
class UserConfiguration: Configuration {
let networkManager = Config(NetworkManager.self, ConfigurationManager.self) { configurationManager in
let privateKey = "1234QwEr!@#$"
return configurationManager.make(privateKey: privateKey)
}
init() {}
}
You can see privateKey is passed to ConfigurationManager on NetworkManager creation.
This NetworkManager instance is registered in DI container, and it will be managed as singleton. (However, instance behavior can be changed by updating scope argument.)
As written, Autowired
is registered in DI container. But you may want to use without registration. That's an Inject
.
class LoginView: Inject {
let viewModel = Inject(LoginViewModel.self)
init() {}
}
class NovelBookView: Inject {
let novels: [Book]!
init() {
self.novels = Inject([Book].self, qualifier: "Novel")
}
}
In the front-end, often dynamically generating a model using the user's data. Let's take an example.
You must implement a friend list. When you select a cell from friends list, you need to present modal view of friend's information.
In this case, The friend data must be passed in the Info Modal
.
This happens very often, Factory
will help them.
Let's try AutowiredFactory
protocol:
class FriendPayload: Payload {
let userID: String
let cachedName: String
required init(with argument: (userID: String, cachedName: String)) {
userID = argument.userID
cachedName = argument.cachedName
}
}
class FriendInfoViewModel: AutowiredFactory {
let accountService: AccountService
let userID: String
var name: String
required init(_ accountService: AccountService, payload: FriendPayload) {
self.accountService = accountService
self.userID = payload.userID
self.name = payload.cachedName
}
}
To pass a user-argument, you must implement a Payload
protocol.
(Naturally, factories work by prototype scope)
Implemented FriendInfoViewModel
can be used as below:
class FriendListViewModel: Autowired {
let friendService: FriendService
func generateInfo(by id: String) -> FriendInfoViewModel? {
guard let friend = friendService.getFriend(by: id) else { return nil }
return Inject(
FriendInfoViewModel.self,
with: (
userID: friend.id,
cachedName: friend.name
)
)
}
required init(_ friendService: FriendService) {
self.friendService = friendService
}
}
Next LazyAutowiredFactory
protocol:
class FriendInfoViewModel: LazyAutowiredFactory {
var accountService: AccountService!
func inject(facebook accountService: AccountService) {
self.accountService = accountService
}
required init(payload: TestPayload) {
...
}
}
The difference between an AutowiredFactory and a LazyAutowiredFactory is that it is lazy injected with the relationship between Autowired and LazyAutowired. However, payload injects by the constructor because passed by the user.
When injecting the dependency, required blueprint.
As above, This blueprint is generated at build
(ex. DeliFactory).
When calling AppContext#load()
, load container of generated class that inherited ModuleFactory
.
Deli supports Multi-Container.
Can be used ModuleFactory
as below.
When calling AppContext#load()
, also load the ModuleFactory
in the module.
Can specify LoadPriority
in this situation. This is the order for selecting the container to be used in dependency injection.
Priority are normal(500)
defaultly. Container's order for selecting as below:
- High priority first.
AppContext.shared.load([
OtherModule.DeliFactory.self,
DeliFactory.self
])
- If priority is same, In the loaded order.
AppContext.shared
.load(DeliFactory())
.load(OtherModule.DeliFactory(), priority: .high)
Priority loading that same as 7.1 used be Unit Test, too.
import Quick
import Nimble
@testable import MyApp
class UserTests: QuickSpec {
override func spec() {
super.spec()
let testModule: ModuleFactory!
testModule.register(UserService.self) { MockUserService() }
let appContext = AppContext.shared
beforeEach {
appContext.load(testModule, priority: .high)
}
afterEach {
appContext.unload(testModule)
}
...
}
}
An example of a test code is Deli.xcodeproj
.
Simply add the following line to your Podfile:
pod 'Deli'
github "kawoou/Deli"
$ deli help
Available commands:
build Build the Dependency Graph.
generate Generate the Dependency Graph.
help Display general or command-specific help
upgrade Upgrade outdated.
validate Validate the Dependency Graph.
version Display the current version of Deli
Any discussions and pull requests are welcomed.
If you want to contribute, submit a pull request.
- Swift 3.1+
This project is powered by
- SourceKitten
- MIT License
- Created by JP Simard
- Yams
- MIT License
- Created by JP Simard
- Regex
- Apache License 2.0
- Created by Crossroad Labs
- Xcproj
- MIT License
- Created by xcode.swift
- Commandant
- MIT License
- Created by Carthage
- Quick
- Apache License 2.0
- Created by Quick Team
- Nimble
- Apache License 2.0
- Created by Quick Team
Deli is under MIT license. See the LICENSE file for more info.