The use and adaptation of the VIPER architecture on our own example of development, part 2

This is the second part of the article about our experience of using and adapting the Viper architecture. In the first part, we discussed the basics, now it's time to talk about structuring and creating project skeleton for VIPER.

General Info

To simplify the development process and make it more clear, we usually separate classes into different folders and groups such as Constants, Extensions, Services, Models, Presentation. In addition, Extensions could be divided into smaller groups like UIExtensions, Animations, etc., and Models - into DTO, Business, etc. As for Services, we will discuss them below.

As you can see, there is nothing particularly difficult about these groups, but let's take a closer look at the project structure in terms of VIPER architecture.

1. Services

For the code clarity, services are better to be divided into several groups like Infrastructure and Business. The first group should contain essential services (API service, Settings service, etc), and the second one consists of all services working with data and business logic.

And there is also the main class called Service Assembly that is a shared instance comprising all services in the app e.g. API service, Settings service, business services, etc.

protocol ServicesAssemblyProtocol {

   var application: UIApplication { get }

   var apiService: APIService { get }

   var weatherService: WeatherService { get }

}

All services are initialized with the required instances of other ones. For example, UserService might need access to API or Database services.

class ServicesAssemblyImpl: ServicesAssemblyProtocol {

   let application: UIApplication

   let apiService: APIService

   let weatherService: WeatherService



   init(application: UIApplication) {

       self.application = application

       apiService = APIServiceImpl()

       weatherService = WeatherServiceImpl(apiService: apiService)

}

}

All setup logic for services is generally implemented in AppDelegate since we might need it at the very start.

   func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

       // Setup services

       ServicesAssembly.setup(application: application)

       

       // Setup everything else if needed

       return true

}


2. Presentation

Presentation is a group with all layout logic inside. Let’s examine its structure in depth.

PresentationAssembly is a shared class containing references to the presentation layer.

protocol PresentationAssemblyProtocol {

   var router: AppRouterProtocol! { get }

   var whisper: InAppNotificationsProvider! { get }

}



class PresentationAssembly: PresentationAssemblyProtocol {

   static let shared = PresentationAssembly()

   var router: AppRouterProtocol!

   var whisper: InAppNotificationsProvider!

   

   func setup(withNavigation navigation: UINavigationController, modules: Array<ModuleFactoryProtocol>, urlScheme: String, services: ServicesAssemblyProtocol) {

       router = AppRouterImpl(withNavigation: navigation, modules: modules, urlScheme: urlScheme)

       whisper = InAppNotificationsProviderImpl(withNavigation: navigation)

}

}

 

Let’s consider Application root more carefully.

Application root

Application root is an entry point to the app. It is called by AppDelegate and responsible for launching an application and all configuration needed before that moment. 

  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

       

       // Setup here       

       // Start application

       appRoot = ApplicationRoot(withNavigation: navigationController)

       appRoot.start()

       

       return true

}

It sets up every necessary service, router, presentation stuff and chooses a module to show first (log in or main screen, for instance). Application root also contains all existing modules inside in order to know about each of them to be able to create it.

class ApplicationRoot: NSObject {

   let modules : Array<ModuleFactoryProtocol> = [

       StartScreenFactory.shared,

       DetailsScreenFactory.shared

   ]



   // MARK:

   fileprivate let router: AppRouterProtocol

   fileprivate let whisper: InAppNotificationsProvider



   init(withNavigation navigation: UINavigationController) {

       let services =  ServicesAssembly.shared

       let presentation = PresentationAssembly.shared

       presentation.setup(withNavigation: navigation, modules: modules, urlScheme: "viper", services: services)

       router = presentation.router

       whisper = presentation.whisper   

       super.init()

}



   func start() {

       let urn = StartScreenFactory.shared.moduleURN

       router.pushModule(byUrn: urn, animated: true, completion: { (_) in

           //completion

})

}

}

Application router

AppRouter contains all routing logic. It is created by Application root and responsible for navigation between modules e.g. push/pop or present/dismiss etc.

protocol AppRouterProtocol {

   var navigationController: UINavigationController { get }

   func pushModule(byUrn urn: String, animated: Bool, completion: ModuleCompletionHandler?)    

   func presentModule(byUrn urn: String, animated: Bool, completion: ModuleCompletionHandler?)    

   func popToViewController(_ controller: UIViewController, animated: Bool)

   func dismissCurrentController(animated: Bool)

}

It’s global in general but can be moved to each module or added to some of them depending on the complexity if we want so.

As you can see Routing group also contains ModuleFactoryProtocol

protocol ModuleFactoryProtocol {

   /**

    * Module URN ( ex. profile:{userID} )

    */

   var moduleURN: String { get }    

   /**

    * Create module with arguments

    * Returns module root UIViewController, must implement ModuleInputProtocol.

    */

   func createModule(arguments: NamedValuesType, completion: ModuleCompletionHandler?) -> UIViewController

}

which defines the way how new module can be created and ModuleInputProtocol

protocol ModuleInputProtocol {

    /**

    * Configure module with arguments.

    * Calls form Module factory

    */

   func setupInitialState(withArguments args: NamedValuesType, completion: ModuleCompletionHandler?)

}

defines the way module could be configured with additional parameters.

Presentation group also contains any additional groups required by the app. For example, InAppNotifications (alerts, toast etc), StyleKit (responsible for styling application if necessary), SocialServices (Facebook, Twitter, etc), and lots of other logic your app may need.

Views

Views is created for keeping all the custom layouts which are placed just here. Besides, it can include the following groups inside such as Layouts, Cells, Collections, etc.

User stories

This part consists of all your screens and UI logic and contains every VIPER module that is located here and should include: Factory, Configurator, View, Presenter, and Interactor.

We will describe the module structure in the next article and right now just say that it may have storyboard or xib file inside if needed.

So, to conclude, to simplify finding the specific class or service let’s mention that the structure of the project is extremely clear. In addition, it’s good practice to keep the group and folder structure the same way.

In the next - third - part of our article we'll tell you about creating a module in terms of VIPER architecture: units, their responsibilities, etc.

Read our blog to know more!

 

Rate this article
0 ratings, average 0.00 of out 5
Table of contents
Get in touch
Related articles
The Use and Adaptation of the VIPER Architecture on Our Own Example of Development, part 4
The Use and Adaptation of the VIPER Architecture on Our Own Example of Development, part 4

Development

4 min read

The use and adaptation of the VIPER architecture on our own example of development, part 3
The use and adaptation of the VIPER architecture on our own example of development, part 3

Insights

4 min read

The use and adaptation of the VIPER architecture on our own example of development, part 1
The use and adaptation of the VIPER architecture on our own example of development, part 1

Insights

4 min read

The Use and Adaptation of the VIPER Architecture on Our Own Example of Development, part 4
The Use and Adaptation of the VIPER Architecture on Our Own Example of Development, part 4

Development

4 min read

The use and adaptation of the VIPER architecture on our own example of development, part 3
The use and adaptation of the VIPER architecture on our own example of development, part 3

Insights

4 min read