Dependency management in Swift iOS apps with peace of mind

library iconGood day to all. In our difficult times, we constantly have to deal with stressful situations and writing program 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 low as possible, and tries to avoid deliberately stressful situations.

When I started writing in Swift, I had to face many problems, and one of them is the lack of competition among IoC containers in this language. In fact, there are only two of them: Typhoon and Swinject. Swinject has few features, and Typhoon is written for Obj-C, which is a problem, and working with it turned out to be a lot of stress for me.

And then Ostap suffered, I decided to write my IoC container for Swift, which turned out to be read under the cat:

So, get acquainted - DITranquillity , IoC container for iOS on Swift, integrated with Storyboard.

An interesting story about the name - after hundreds of different ideas, I settled on "calm". When I came up with the name, I started from the fact that the main reason for writing the IoC container was Typhoon . At first, there were thoughts about calling a library a natural disaster stronger than a typhoon, but I realized that I had to think differently: a typhoon is stress, and my library should provide the opposite, that is, peace.

It was planned to check everything statically (unfortunately, everything failed completely), and not crash in the middle of the application for unknown reasonsthat when using a typhoon in large applications, it’s not so rare, and xcode sometimes doesn’t build the project because of the typhoon, but crashes during the build .

Typhoon lovers may be a little upset, but, in my opinion, the naming of some entities is different from that of a typhoon. It is the same as Autofac , but taking into account the features of the language.

Features


I'll start by describing the features of the library:

  • The library works with pure Swift classes. No need to inherit from NSObject and declare protocols as Obj-C, using this library you can write in pure Swift;
  • Nativeness - the description of dependencies takes place in the native language, which allows easy refactoring and ...
  • Most of the checks at the compilation stage - after Typhoon, this may seem like a paradise, since many typos are detected at the compilation stage, and not at runtime. Unfortunately, the library cannot boast of errors during execution, but some of the problems, be sure, will be cut off;
  • Support for all Dependency Injection patterns: Initializer Injection, Property Injection, and Method Injection. I don’t know why this is cool, but everyone writes about it ;
  • Support for cyclic dependencies - the library supports many different options for 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;
  • Indication of alternative types;
  • Dependency resolution by type and name;
  • Multiple registration;
  • Dependency resolution with initialization parameters;
  • A short entry to resolve the dependency;
  • Special mechanisms for "modularity";
  • CocoaPods support;
  • Documentation in Russian (in fact, it is more correct to say draft documentation, there are many errors).

And all this in 1500 lines of code, with about 400 lines of them, this is an automatically generated code for typed resolution of dependencies with a different number of initialization parameters.

And what to do with all this?


Briefly


I'll start with a small syntax example: Autofac forgive me for writing an example adapted to my library .

// ClassesclassTaskRepository: TaskRepositoryProtocol {
...
}
classLogManager: LoggerProtocol {
...
}
classTaskController {
 var logger: LoggerProtocol? = nil
 private let repository: TaskRepositoryProtocol
 init(repository:TaskRepository) {
   self.repository = repository
 }
 ...
}
// Register
let builder = DIContainerBuilder()
builder.register(TaskRepository.self)
 .asType(TaskRepositoryProtocol.self)
 .initializer { TaskRepository() }
builder.register(LogManager.self)
 .asType(LoggerProtocol.self)
 .initializer { LogManager(Date()) }
builder.register(TaskController.self)
 .initializer { (scope) in TaskController(repository: *!scope) }
 .dependency { (scope, taskController) in taskController.logger = try? scope.resolve() }
let container = try! builder.build()
// Resolve
let taskController: TaskController = container.resolve()

And now, in order


Basic project integration


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 of their expediency.

To integrate with the project, which is planned more or less large, we need:

  1. Integrate the library itself into the project. This can be done using Cocoapods:

    pod 'DITranquillity'

  2. Declare the base assembly using the library (optional):

    import DITranquillity
    classAppAssembly: DIAssembly { // Объявляем нашу сборку
     var publicModules: [DIModule] = [ ]
     var intermalModules: [DIModule] = [ AppModule() ]
     var dependencies: [DIAssembly] = [
       // YourAssembly2(), YourAssembly3() - зависимости на другие сборки
     ]
    }

  3. Declare the base module (optional):

    import DITranquillity
    classAppModule: DIModule { // Объявляем наш модульfunc load(builder: DIContainerBuilder){ // Согласно протоколу реализуем метод// Регистрируем типы
     }
    }
    

  4. Register types in the module (see the first example above).

  5. Register the base assembly in the builder and assemble the container:

    import DITranquillity
    @UIApplicationMain
    classAppDelegate: UIResponder, UIApplicationDelegate {
    public func applicationDidFinishLaunching(_ application: UIApplication){
       ...
       let builder = DIContainerBuilder()
       builder.register(assembly: AppAssembly())
       try! builder.build() // Собираем контейнер// Если во время сборки произошла ошибка, то программа упадет, с описанием всех ошибок, которые нужно поправить
     }
    }
    

Storyboard


The next step, after writing a couple of classes, is to create a Storyboard , if it has not been before . We integrate it into our dependencies. To do this, we will need to slightly edit the base module:

classAppModule: DIModule {
 func load(builder: DIContainerBuilder){
   builder.register(UIStoryboard.self)
     .asName("Main") // Даем типу имя по которому мы сможем в будущем его получить
     .instanceSingle() // Говорим что он должен быть единственный в системе
     .initializer { scope in DIStoryboard(name: "Main", bundle: nil, container: scope) }
   // Регистрируем остальные типы
 }
}

And change AppDelegate:

public func applicationDidFinishLaunching(_ application: UIApplication){
 ....
 let container = try! builder.build() // Собираем наш контейнер
window = UIWindow(frame: UIScreen.main.bounds)
 let storyboard: UIStoryboard = try! container.resolve(Name: "Main") // Получаем наш Main storyboard
 window!.rootViewController = storyboard.instantiateInitialViewController()
 window!.makeKeyAndVisible()
}

ViewControllers on the Storyboard


And so we ran our code, were glad that nothing had fallen, and made sure that we had our ViewController created. It's time to create some class, and implement it in the ViewController.

Create a Presenter:

classYourPresenter {
 ...
}

We also need to give a name (type) to our ViewController, and add an injection through the properties or method, but in our code we will use the injection through the properties:

classYourViewController: 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 we need to register our types in our module:

func load(builder: DIContainerBuilder){
   ...
   builder.register(YourPresenter.self)
     .instancePerScope() // Говорим, что на один scope нужно создавать один Presenter
     .initializer { YourPresenter() }
   builder.register(YourViewController.self)
     .instancePerRequest() // Специальное время жизни для ViewController'ов
     .dependency { (scope, self) in self.presenter = try! scope.resolve() } // Объявляем зависимость
 }

We start the program, and we see that our ViewController has a Presenter.

But hey, what a strange lifetime for instancePerRequest, and where did the initializer go? Unlike all other types of ViewControllers that are placed on the Storyboard, we do not create it, but 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 created not by us, but by someone else - for this we have the `instancePerRequest` modifier.

Add work with data


Then the project has to do something and often for mobile devices, applications receive information from the network, process it and display it. For simplicity, we omit the data processing step and will not go into the details of receiving data from the network. Just suppose we have a 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 you can write another module that would register our new class. Of course, you can go further 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 capabilities of the library. Or, conversely, embed it in an existing module.

import DITranquillity
classServerModule: DIModule {
 func load(builder: DIContainerBuilder){
   builder.register(ServerImpl.self)
     .asSelf()
     .asType(Server.self)
     .instanceSingle()
     .initializer { ServerImpl(domain: "https://your_site.com/") }
 }
}

We registered the ServerImpl type, and in the program it will be known under 2 types: ServerImpl and Server. This is some feature of the registration behavior - if an alternative type is specified, then the main type is not used, unless this is specified explicitly. We also indicated that there is only one server in our program.

We also slightly modify our assembly so that she knows about the new module:

classAppAssembly: DIAssembly {
   var publicModules: [DIModule] = [ ServerModule() ]
}

The difference between publicModules and internalModules
Существует два уровня видимости модулей: Internal и Public. Public — означает, что данный модуль будет виден, и в других сборках, которые используют эту сборку, Internal — модуль будет виден только внутри нашей сборки. Правда, надо уточнить, что так как сборка является всего лишь объявлением, то данное правило о видимости модулей распространяется на контейнер, по принципу: все модули из сборок которые были напрямую добавлены в builder, будут включены в собранный им контейнер, а модуля из зависимых сборок включаться в контейнер, только если он объявлены публичными.

Now let's fix Presenter a little bit - add him the information that he needs a server:

classYourPresenter {private let server: Server
 init(server: Server){
   self.server = server
 }
}

We implemented the dependency through the initialization method, but we could do it, as in the ViewController, through the properties, or the method.

And we are completing the registration of our Presenter - we say that we will implement Server in Presenter:

   builder.register(YourPresenter.self)
     .instancePerScope() // Говорим, что на один scope нужно создавать один Presenter
     .initializer { (scope) in YourPresenter(server: *!scope) }

Here we used the “fast” syntax `*!` To get the dependency, which is the equivalent of the notation: `try! scope.resolve () `

We start our program and see that our Presenter has a Server. Now it can be used.

We implement a logger


Our program works, but for some users it suddenly began to work incorrectly. We can’t reproduce the problem at home and decide - it’s all time, we need a logger. But since our belief in the paranormal has already awakened, the logger should write data to a file, to the console, to the server and even in a 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 all the others, and call it MainLogger. Further we in those classes in which we are going to log add a line on similarity: `var log: Logger? = nil`, and ... And now we need to register all the actions that we performed.

First, create a new LoggerModule module:

import DITranquillity
classLoggerModule: 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 to add the introduction of our logger to all the classes where we declared it, for example, like this:

builder.register(YourPresenter.self)
     .instancePerScope() // Говорим, что на один scope нужно создавать один Presenter
     .initializer { scope in tryYourPresenter(server: *!scope) }
     .dependency { (scope, obj) in obj.log = *?scope }

And then we add it to our assembly. Now it’s worth making out what we just wrote.

First, we registered 3 of our loggers, which will be available by the name Logger - that is, we have carried out 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 for MainLogger we do two things:

  1. We say that this is a standard logger. That is, if we need a single logger then it will be MainLogger, and not some other.

  2. In MainLogger, we transfer a list of all our loggers, except for ourselves (this is one of the library's capabilities, recursive calls are excluded when dependencies are resolved multiple times. But if we do the same in the dependency block, we will get all the loggers, including MainLogger). This uses the fast syntax `**!`, which is the equivalent of `try! scope.resolveMany () `

Summary


Using the library, we were able to build dependencies between several layers: Router, ViewController, Presenter, Data. Things were shown such as: injecting dependencies through properties, injecting dependencies through an initializer, alternative types, modules, slightly touched the lifetime and assemblies.

Many opportunities were missed: cyclic dependencies, obtaining dependencies by name, lifetime, assembly. 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 to which logs come
  • Support for other systems (MacOS, WatchOS)

Alternatives


  • Typhoon - does not support pure swift types, and its syntax, in my opinion, is cumbersome
  • Swinject - lack of alternative types, and multiple registration. Less developed mechanisms for “modularity”, but this is a good alternative

PS
На данный момент проект находится, на пререлизном состоянии, и мне бы хотелось, прежде чем давать ему версию 1.0.0 узнать мнения других людей, так как после “официального” выхода, менять что-то кардинально станет сложнее.

Also popular now: