Country
    /
    Home   →   Blog   →   The use and adaptation of the VIPER architecture on our own example of development, part 2

    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!

    Next Posts
    D2df0f2aa3aac8b2fb42e56d66ca7f4496ed4fee The use and adaptation of the VIPER architecture on our own example of development, part 3
    This is the third part of the article series about our experience of using and adapting the VIPER architecture. Now we're talking about creating a module in terms of VIPER architecture
    Fbbaf34f88c9969f98fe9dfff084b806d2f71b69 The use and adaptation of the VIPER architecture on our own example of development, part 1
    General info about software architectures
    Dc30205f5343bab334d75f6530a7c31af4f21d44 The Use and Adaptation of the VIPER Architecture on Our Own Example of Development, part 4
    The article about using and adapting the Viper architecture