
Good day to all. In our difficult time, constantly have to deal with stressful situations and writing code is no exception. Everyone copes with stress in different ways: someone goes to the bar, someone meditates on the contrary in silence, but everyone wants this stress to be as little as possible, and tries to avoid obviously stressful situations.
When I started writing in Swift, I had to face many problems, and one of them is the lack of competition in IoC containers in this language. In fact, there are only two: Typhoon and Swinject. Swinject has few features, and Typhoon is written for Obj-C, which is a problem, and working with him turned out to be a lot of stress for me.
And then Ostap suffered, I decided to write my IoC container for Swift, what came out of it to read under the cut:
So, get acquainted -
DITranquility , IoC container for iOS on Swift, integrated with Storyboard.
')
An interesting story about the name - after hundreds of different ideas, stopped at the "calm". When I came up with the name, I was repelled by the fact that the main reason for writing the IoC container was
Typhoon . Initially, there were thoughts to call the library a natural disaster stronger than a typhoon, but I understood that it was necessary to think differently: a typhoon is stress, and my library should provide the opposite, that is, calm.
It was planned to check everything statically (unfortunately, it wasn’t completely possible), and not to fall in the middle of the application for unknown reasons
that when using a typhoon in large applications it doesn’t happen so rarely, and xcode sometimes doesn’t collect a project because of a typhoon, but falls during assembly .
Typhoon lovers may be a little upset, but, in my opinion, the naming of certain entities is different from the typhoon's view. It is the same as
Autofac , but taking into account the peculiarities of the language.
Special features
I will begin with a description of the features of the library:
- The library works with pure Swift classes. No need to inherit from NSObject and declare protocols as Obj-C, with the help of this library, you can write in pure Swift;
- Nativeness - the description of dependencies occurs in the native language, which allows for easy refactoring and ...
- Most of the compile checks are a bit like paradise after Typhoon, since many typos are revealed at compile time, not at runtime. Unfortunately, they cannot boast that during execution, the library cannot fail, but be sure that some of the problems will be cut off;
- Support for all patterns of Dependency Injection: Initializer Injection, Property Injection, and Method Injection.
I do not know why this is cool, but everyone writes about it ; - Support for cyclic dependencies - the library supports many different variants of cyclic dependencies, without the intervention of a programmer;
- Integration with Storyboard - allows you to embed dependencies directly in ViewControllers.
And:
- Support for the lifetime of objects;
- Specifying alternative types;
- Resolving dependencies by type and name;
- Multiple registration;
- Resolving dependencies with initialization parameters;
- A short entry to resolve dependencies;
- Special mechanisms for "modularity";
- CocoaPods support;
- Documentation in Russian (in fact, it is more correct to say the rough documentation, there are many errors).
And all this in 1500 lines of code, with about 400 lines of them, this is automatically generated code for typed dependency resolution with a different number of initialization parameters.
And what to do with all this?
Briefly
I
will begin with a small example of syntax:
may Autofac forgive me for writing an example adapted for my library .
And now in order
Basic integration into the project
Unlike Typhoon, the library does not support “automatic” initialization from plist, or similar “features”. In principle, despite the fact that the typhoon supports such opportunities, I am not sure about their expediency.
To integrate with a project that is planned more or less large, we need:
- Integrate the library itself into the project. This can be done using Cocoapods:
pod 'DITranquillity'
- Declare the base assembly using the library (optional):
import DITranquillity class AppAssembly: DIAssembly {
- Declare the base module (optional):
import DITranquillity class AppModule: DIModule {
- Register the types in the module (see the first example above).
- Register the base assembly in the builder and collect the container:
import DITranquillity @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { public func applicationDidFinishLaunching(_ application: UIApplication) { ... let builder = DIContainerBuilder() builder.register(assembly: AppAssembly()) try! builder.build()
Storyboard
The next step, after writing a pair of classes, is created by the Storyboard
, if it hasn’t been there before . We integrate it into our dependencies. To do this, we will need to edit the base module a bit:
class AppModule: DIModule { func load(builder: DIContainerBuilder) { builder.register(UIStoryboard.self) .asName("Main")
And change the AppDelegate:
public func applicationDidFinishLaunching(_ application: UIApplication) { .... let container = try! builder.build()
ViewControllers on Storyboard
And so we launched our code
, we were glad that nothing had fallen and we were convinced that we had created our ViewController. It's time to create some class and embed it in the ViewController.
Create a Presenter:
class YourPresenter { ... }
We will also need to give a name (type) to our ViewController, and add an injection through properties or a method, but in our code we will use an injection through properties:
class YourViewController: UIViewController { var presenter: YourPresenter! ... }
Also do not forget to indicate in the Storyboard that the ViewController is not just a UIViewController, but YourViewController.
And now you need to register our types in our module:
func load(builder: DIContainerBuilder) { ... builder.register(YourPresenter.self) .instancePerScope()
Run the program, and see that our ViewController has a Presenter.
But, wait a minute, what is the strange time of an instancePerRequest, and where did the initializer go? Unlike all other types of ViewControllers that are not placed on the Storyboard, we create the Storyboard, so we do not have an initializer and they do not support injection through the initialization method. Since the presence of initializer is one of the check points when trying to create a container, we need to declare that this type is not created by us, but by someone else - for this we have the `instancePerRequest` modifier.
We add work with data
Further, the project must do something, and for frequent, mobile applications will receive information from the network, process it and display it. For the sake of simplicity, we omit the data processing step and will not go into the details of receiving data from the network. Just assume that we have the Server protocol, with the `get` method and, accordingly, there is an implementation of this protocol. That is, the following code appears in our program:
protocol Server { func get(method: String) -> Data? } class ServerImpl: Server { init(domain: String) { ... } func get(method: String) -> Data? { ... } }
Now we can write another module that would register our new class. Of course, you can go ahead and create a new assembly, and transfer the work with the server to another project, but this will complicate the example, although it will show more aspects and possibilities of the library. Or, conversely, embed already in the existing module.
import DITranquillity class ServerModule: DIModule { func load(builder: DIContainerBuilder) { builder.register(ServerImpl.self) .asSelf() .asType(Server.self) .instanceSingle() .initializer { ServerImpl(domain: "https://your_site.com/") } } }
We have registered the ServerImpl type, while in the program it will be known by two types: ServerImpl and Server. This is some peculiarity of registration behavior - if an alternative type is specified, then the main type is not used, unless you explicitly indicate this. We also indicated that the server in our program is one.
We also slightly modify our build so that it knows about the new module:
class AppAssembly: DIAssembly { var publicModules: [DIModule] = [ ServerModule() ] }
The difference between publicModules and internalModulesThere are two levels of module visibility: Internal and Public. Public - means that this module will be visible, and in other assemblies that use this assembly, the Internal - module will be visible only inside our assembly. However, it is necessary to clarify that since the assembly is just an announcement, this rule on the visibility of modules applies to the container, according to the principle: all modules from assemblies that were directly added to the builder will be included in the container they assembled, and the module from dependent assemblies be included in the container only if it is declared public.
Now let's fix a little Presenter - let's add him information that he needs a server:
class YourPresenter { private let server: Server init(server: Server) { self.server = server } }
We implemented the dependency through the initialization method, but could do it, as in the ViewController, through properties, or a method.
And we add the registration of our Presenter - we say that we will implement the Server in the Presenter:
builder.register(YourPresenter.self) .instancePerScope()
Here, to get dependencies, we used the “fast” syntax `*!` Which is equivalent to the record: `try! scope.resolve () `
We start our program and see that our Presenter has a Server. Now you can use it.
Implement a logger
Our program works, but for some users it suddenly began to work incorrectly. We cannot reproduce the problem in ourselves and solve it - all the time, we need a logger. But since we have already awakened faith in the paranormal, the logger must write data to a file, to the console, to the server, and to the sea of ​​places, and all this should be easily turned on / off and used.
And so, we create the basic protocol `Logger`, with the function` log (message: String) `and implement several implementations: ConsoleLogger, FileLogger, ServerLogger ... Create a basic logger that pulls everyone else, and we call it MainLogger. Then we are in those classes in which we are going to log add a line on the similarity: `var log: Logger? = nil`, and ... And now we need to register all the actions that we performed.
First, create a new module `LoggerModule`:
import DITranquillity class LoggerModule: DIModule { func load(builder: DIContainerBuilder) { builder.register(ConsoleLogger.self) .asType(Logger.self) .instanceSingle() .initializer { ConsoleLogger() } builder.register(FileLogger.self) .asType(Logger.self) .instanceSingle() .initializer { FileLogger(file: "file.log") } builder.register(ServerLogger.self) .asType(Logger.self) .instanceSingle() .initializer { ServerLogger(server: "http://server.com/") } builder.register(MainLogger.self) .asType(Logger.self) .asDefault() .instanceSingle() .initializer { scope in MainLogger(loggers: **!scope) } } }
And do not forget, add the introduction of our logger, to all classes where we declared it, for example, like this:
builder.register(YourPresenter.self) .instancePerScope()
And after we add it to our assembly. Now it is worth analyzing what we have just written.
Initially, we registered 3 of our loggers, which will be accessible by the name of the Logger - that is, we performed multiple registrations. Moreover, if we remove MainLogger, then the program will not have a single logger, since if we want to get one logger, the library will not be able to understand what kind of logger the programmer wants from it. Next to MainLogger, we do two things:
- We say that this is a standard logger. That is, if we need a single logger, it will be MainLogger, and not some other.
- In the MainLogger, we transfer the list of all our loggers, except for ourselves (this is one of the library's capabilities, recursive calls are excluded in case of multiple dependency resolution. But if we do the same in the dependency block, then all loggers will be issued, including MainLogger). This uses the quick syntax `**!`, which is the equivalent of `try! scope.resolveMany () `
Results
With the help of the library, we were able to build dependencies between several layers: Router, ViewController, Presenter, Data. Such things were shown: dependency injection through properties, dependency injection through initializer, alternative types, modules, a little touched the lifetime and assemblies.
Many opportunities were missed: cyclic dependencies, getting dependencies by name, lifetime, build. You can see them in the
documentation.This example is available at
this link .
Plans
- Adding detailed logging, with the ability to specify external functions, in which logs come
- Support for other systems (MacOS, WatchOS)
Alternatives
- Typhoon - does not support pure swift types, and its syntax is bulky in my opinion
- Swinject - the lack of alternative types, and multiple registration. Less developed mechanisms for "modularity", but this is a good alternative.
PSAt the moment, the project is in a prerelease state, and I would like to, before giving it version 1.0.0, know the opinions of other people, since after the “official” release, it will become more difficult to change something drastically.