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.
Getting Started
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, target and output field is specified.
If the above code is written, you can use the UserService or UserServiceImpl type to load the dependency instance.
2. Autowired
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
}
}
3. LazyAutowired
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() {}
}
4. Configuration
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!@#quot;
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.)
5. Inject
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] = Inject([Book].self, qualifier: "Novel")
init() {}
}
6. Factory
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:
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.
7. ModuleFactory
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.
7.1. Multi-Container
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:
It’s often profit to use different configuration values depending on the running environment.
For example, you can specific that save the file log at development build and not save the file log at the Release build.
deli build \
--property "Configurations/Common/*.yml" \
--property "Configurations/application-dev.yml"
If the same configuration information, it’s overwritten with the last specified information.
9.2. Group Value
You can use ConfigProperty to safe retrieve the specified value in the configuration file.
struct ServerConfig: ConfigProperty {
let target: String = "server"
let url: String
let isDebug: Bool
}
When implementing the model as above, ServerConfig is registered in IoC Container.
One thing to keep in mind when defining the model, need to set the target value. This property represents the path to retrieve in the configuration file using JSONPath style.
If you do not have the required configuration values at build time, will occurred a compile error.
final class NetworkManager: Autowired {
let info: ServerConfig
required init(_ config: ServerConfig) {
info = config
}
}
9.3. Single Value
When get a bundle value as above, implement the ConfigProperty protocol. So how to get a single value? You can use the InjectProperty.
final class NetworkManager: Inject {
let serverUrl = InjectProperty("server.url")
}
InjectProperty is similar to ConfigProperty. It checks the configuration value at build time and inject data as String type.
If you want to retrieve configuration value optionally without validation, this is not a proper way.
In this case, recommend using the AppContext#getProperty() method.
final class NetworkManager {
let serverUrl = AppContext.getProperty("server.url", type: String.self) ?? "https://wtf.example.com"
}
9.4. Qualifier by Property
To enhance usability of configuration property, Deli provides a way of injection using qualifier as configuration value.
There are two ways to use it. let’s look first that constructor injection like Autowired.
As mentioned in the Autowired paragraph, you can not use . for parts that specify qualifier. Unfortunately, swift do not has an annotation-like features. So I implemented to use comment as an alternative.
How it works:
final class UserService: Autowired {
required init(_/*logger.storage*/ logger: Logger) {
}
}
When using the Inject method:
final class UserService: Inject {
func getLogger() -> Logger {
return Inject(Logger.self, qualifierBy: "logger.storage")
}
}
10. PropertyWrapper
For easier use, supports the @propertyWrapper added in Swift 5.1.
There are two main features to be supported: dependency injection and Configuration Property.
10.1. Dependency
There are @Dependency and @DependencyArray for injection of dependencies.
class Library {
@Dependency(qualifier "logger.storage")
var logger: Logger
@DependencyArray(qualifier: "novel")
var novels: [Book]
}
$ 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
Deli is an easy-to-use Dependency Injection Container that creates DI containers with all required registrations and corresponding factories.
Table of Contents
Overview
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.
Getting Started
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
,target
andoutput
field is specified.You’ll have to make your scheme
Shared
. To do thisManage Schemes
and check theShared
areas:Alternatively, you can specify
target
instead ofscheme
. In this case, Deli will find the Build Target.Then build with the provided binaries.
Dependency Graph is configured through source code analysis. It is saved as the file you specified earlier.
File contents as below:
Add the generated file to the project and call it from the app’s launch point.
AppDelegate.swift:
Build Phases
Integrate Deli into an Xcode scheme to get warnings and errors displayed in the IDE. Just add a new “Run Script Phase” with:
Alternatively, if you’ve installed Deli via CocoaPods the script should look like this:
Features
1. Component
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:If the above code is written, you can use the
UserService
orUserServiceImpl
type to load the dependency instance.2. Autowired
The
Autowired
protocol is registered automatically, same asComponent
protocol. A difference, you can load the required dependencies from DI container.Autowired
can be used as below:Easy right? So let’s look at the code below.
This code arranged the books through inheritance. You can get all of
Book
instances like below:Furthermore, What should do to get the books with the “Novel” qualifier? In Deli, can be constructor injection in the below:
3. LazyAutowired
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:If you try to inject a MessageService, Circular Dependency will occurred.
What if UserService extends
LazyAutowired
?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:
4. Configuration
The
Configuration
protocol makes the user can registerResolver
directly.Let’s look at the code:
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.)
5. Inject
As written,
Autowired
is registered in DI container. But you may want to use without registration. That’s anInject
.6. Factory
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:To pass a user-argument, you must implement a
Payload
protocol. (Naturally, factories work by prototype scope)Implemented
FriendInfoViewModel
can be used as below:Next
LazyAutowiredFactory
protocol: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.
7. ModuleFactory
When injecting the dependency, required blueprint. As above, This blueprint is generated at
build
(ex. DeliFactory). When callingAppContext#load()
, load container of generated class that inheritedModuleFactory
.Deli supports Multi-Container. Can be used
ModuleFactory
as below.7.1. Multi-Container
When calling
AppContext#load()
, also load theModuleFactory
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:7.2. Unit Test
Priority loading that same as 7.1 used be Unit Test, too.
An example of a test code is
Deli.xcodeproj
.8. Struct
Support for Struct has been added since version
0.7.0
.The basic behavior is the same as Class, but one difference is that cannot use
weak
Scope.Below is an example of Moya‘s plugin implementation.
9. Configuration Property
It’s often profit to use different configuration values depending on the running environment. For example, you can specific that save the file log at development build and not save the file log at the Release build.
application-dev.yml:
application-prod.yml:
9.1. Usage
Two ways solution to use the Configuration Property created above.
deli.yml
.Change the configuration file as below:
Build script can do this:
If the same configuration information, it’s overwritten with the last specified information.
9.2. Group Value
You can use
ConfigProperty
to safe retrieve the specified value in the configuration file.When implementing the model as above,
ServerConfig
is registered in IoC Container.One thing to keep in mind when defining the model, need to set the
target
value. This property represents the path to retrieve in the configuration file using JSONPath style.If you do not have the required configuration values at build time, will occurred a compile error.
9.3. Single Value
When get a bundle value as above, implement the
ConfigProperty
protocol. So how to get a single value? You can use theInjectProperty
.InjectProperty
is similar toConfigProperty
. It checks the configuration value at build time and inject data as String type.If you want to retrieve configuration value optionally without validation, this is not a proper way.
In this case, recommend using the
AppContext#getProperty()
method.9.4. Qualifier by Property
To enhance usability of configuration property, Deli provides a way of injection using
qualifier
as configuration value.There are two ways to use it. let’s look first that constructor injection like
Autowired
.As mentioned in the Autowired paragraph, you can not use
.
for parts that specifyqualifier
. Unfortunately, swift do not has an annotation-like features. So I implemented to usecomment
as an alternative.How it works:
When using the
Inject
method:10. PropertyWrapper
For easier use, supports the @propertyWrapper added in Swift 5.1.
There are two main features to be supported: dependency injection and Configuration Property.
10.1. Dependency
There are
@Dependency
and@DependencyArray
for injection of dependencies.10.2. PropertyValue
@PropertyValue
is the same as Configuration Property and the usage as below:Installation
Cocoapods:
Simply add the following line to your Podfile:
Carthage:
Command Line
Examples
Contributing
Any discussions and pull requests are welcomed.
If you want to contribute, submit a pull request.
Requirements
Attributions
This project is powered by
License
Deli is under MIT license. See the LICENSE file for more info.