This is the third part of the article series about our experience of using and adapting the VIPER architecture. In the first part, we had discussed the basics of using and adapting the architecture of the VIPER and last time we talked about structuring and creating project skeleton for VIPER. It's high time to talk about creating a module in terms of VIPER architecture
Here goes!
Probably, the most time-consuming part of using VIPER architecture is creating lots of additional files and classes. So let’s sort it out and first of all see what we should create and why we need it.
1. Factory
The factory is a class responsible for creating a module with parameters (any data needed to be transferred to module we’re opening) and completion (a module which opens current one and which has to decide what to do with it). The controller should conform to ModuleInputProtocol.
func createModule(arguments: NamedValuesType, completion: ModuleCompletionHandler?) -> UIViewController {
let storyboard = UIStoryboard(name: storyboardId, bundle: nil)
let controller = storyboard.instantiateViewController(withIdentifier: initialControllerID)
let moduleInput = controller as? ModuleInputProtocol
if let moduleInput = moduleInput {
moduleInput.setupInitialState(withArguments: arguments, completion: completion)
} else {
fatalError("\(initialControllerID) moduleInput == nil")
}
return controller
}
It should also define storyboardId and initialControllerID
// MARK:
let storyboardId = "StartScreen"
let initialControllerID = "StartScreenViewController"
2. Storyboard
Storyboard is a very important class because all the UI elements should be placed here. Moreover, there’re a few requirements for storyboard.
-
Initial storyboard controller should have storyboard id, defined in Factory
-
Storyboard has to contain an object with custom class name acting as module initializer (e.g StartScreenModuleInitializer)
3. View
One of the VIPER module parts is the View which means not only UIView but also all the presentation layers including View, ViewController, DataSources, Delegates, Adapters etc. One more important moment is the interaction between View and other parts of the module, namely the Presenter.
And, of course, we should define input and output protocols. So input protocol (ViewInput) defines methods by which the Presenter calls the View and output protocol (ViewOutput) defines how the View calls the Presenter.
4. Presenter
This part acts as a mediator between View and the data layer. It communicates with View in two ways using ViewInput and ViewOutput protocols. And the main task is to process the data retrieved and then assign it to the View.
5. Interactor
The interactor is a class responsible for communication between VIPER module and the external “world”. In this case, Presenter asks for data by InteractorInput protocol. We don’t use InteractorOutput here to transfer data to Presenter for asynchronous operations (e.g API calls) because we usually use PromiseKit for them. We will show how we use it in the final part.
6. Configurator
This part of VIPER module is responsible for initial module setup and consists of two parts
-
Initializer (ModuleInitializer).
class StartScreenModuleInitializer: NSObject {
//Connect with object on storyboard
@IBOutlet weak var startscreenViewController: StartScreenViewController!
override func awakeFromNib() {
let configurator = StartScreenModuleConfigurator()
configurator.configureModuleForViewInput(viewInput: startscreenViewController)
}
}
StartScreenViewController should be linked to its outlet in storyboard. After loading all UI elements from nib configurator is called to setup all module units (View, Presenter, Interactor).
-
Configurator is the main setup unit.
class StartScreenModuleConfigurator {
func configureModuleForViewInput<UIViewController>(viewInput: UIViewController) {
if let viewController = viewInput as? StartScreenViewController {
configure(viewController)
}
}
private func configure(_ viewController: StartScreenViewController) {
let presenter = StartScreenPresenter()
presenter.view = viewController
let interactor = StartScreenInteractor()
interactor.weatherService = ServicesAssembly.shared.weatherService
presenter.interactor = interactor
presenter.router = PresentationAssembly.shared.router
viewController.output = presenter
}
}
As we can see, it creates Presenter and Interactor, assigning all required services to them and connecting them together. The presenter has a reference to Interactor while View refers to Presenter.
Now module is ready to be shown on the screen.
It looks clear enough, but in reality, it requires a lot of work to create the same files for each module. And on the other hand, too much time is spent for forming VIPER module, whereas similar screens could be easily created using MVC or MVP. Is it possible to find a solution to this problem? Maybe some script could simplify the whole process by doing all this work for us?
Luckily, there is a great solution, namely - Generamba (created by Rambler Digital Solutions), which allows defining folder structure, files, and the code they contain. So, by using easy command we receive all these files generated automatically! And the only thing we need to do is to implement business logic to the module. Quite easy, right?
Let’s take a look at Generamba in more details.
Generamba in practice
The first step would be Generamba installation. You can achieve this by using “gem install generamba” command that requires Ruby 2.2 version.
The next step is to set up Generamba in the project. Run “generamba setup” command in project root to create Rambafile and add required information about project and author such as author name, company name, target name, etc. It’s necessary to properly add new files to the project. And don’t worry, this file can be modified any time you want.
Now you should add all the templates you’re going to use in the project to the Rambafile. For instance:
templates:
- {name: rviper_controller}
- {name: local_template_name, local: 'absolute/file/path'}
- {name: remote_template_name, git: 'https://github.com/igrekde/remote_template'}
So you can see three different templates:
- rviper_controller is a shared template. You can see full list here
- local_template_name is template located on your computer locally
- remote_template_name is a link to template located on the web
Then run “generamba template install” command to install all templates which will be downloaded and added to “Templates” folder.
And now it’s time for the last step that includes the module generating process. Run “generamba gen [Module name] [Template name]” to generate all files specified in the template.
The script was executed successfully and now we can see what has been created:
It's a great result! We used our remote template with name swift_viper_template and got all files generated without any difficulties!
It's all for now. The final and most interesting part of these series of articles is coming soon. Follow our blog not to miss it!