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!