Giter Site home page Giter Site logo

ios-viper-xcode-templates's Introduction

iOS VIPER

Installation instructions

To install VIPER Xcode templates clone this repo and run the following command from root folder:

make install_templates

To uninstall Xcode template run:

make uninstall_templates

After that, restart your Xcode if it was already opened.

VIPER short introduction

How to organize all your code and not end up with a couple of Massive View Controllers with millions of lines of code? In short, VIPER (View Interactor Presenter Entity Router) is an architecture which, among other things, aims at solving the common Massive View Controller problem in iOS apps. When implemented to its full extent it achieves complete separation of concerns between modules, which also yields testability. This is good because another problem with Apple's Model View Controller architecture is poor testability.

If you search the web for VIPER architecture in iOS apps you'll find a number of different implementations and a lot of them are not covered in enough detail. At Infinum we have tried out several approaches to this architecture in real-life projects and with that we have defined our own version of VIPER which we will try to cover in detail here.

Let's go over the basics quickly - the main components of VIPER are as follows:

  • View: contains UI logic and knows how to layout and animate itself. It displays what it's told by the Presenter and it delegates user interaction actions to the Presenter. Ideally it contains no business logic, only view logic.
  • Interactor: used for fetching data when requested by the Presenter, regardless of where the data is coming from. Contains only business logic.
  • Presenter: prepares the content which it receives from the Interactor to be presented by the View. Contains business and view logic - basically it connects the two.
  • Entity: models which are handled by the Interactor. Contains only business logic, but primarily data, not rules.
  • Router: handles navigation logic. In our case we use components called Wireframes for this responsibility.

Components

Your entire app is made up of multiple modules which you organize in logical groups and use one storyboard for that group. In most cases the modules will represent screens and your module groups will represent user-stories, business-flows and so on.

Module components:

  • View
  • Presenter
  • Interactor (not mandatory)
  • Wireframe

In some simpler cases you won't need an Interactor for a certain module, which is why this component is not mandatory. These are cases where you don't need to fetch any data, which is usually not common.

Wireframes inherit from the BaseWireframe. Presenters and Interactors do not inherit any class. Views most often inherit UIViewControllers. All protocols should be located in one file called Interfaces. More on this later.

Communication and references

The following pictures shows relationships and communication for one module.

iOS VIPER GRAPH

Let's take a look at the communication logic.

  • LoginViewController communicates with LoginPresenter via a LoginPresenterInterface protocol
  • LoginPresenter communicates with LoginViewController via a LoginViewInterface protocol
  • LoginPresenter communicates with LoginInteractor via a LoginInteractorInterface protocol
  • LoginPresenter communicates with LoginWireframe via a LoginWireframeInterface protocol

The communication between most components of a module is done via protocols to ensure scoping of concerns and testability. Only the Wireframe communicates directly with the Presenter since it actually instantiates the Presenter, Interactor and View and connects the three via dependency injection.

Now let's take a look at the references logic.

  • LoginPresenter has a strong to LoginInteractor
  • LoginPresenter has a strong to LoginWireframe
  • LoginPresenter has a weak reference to LoginViewController
  • LoginViewController has a strong reference to LoginPresenter

The reference types might appear a bit counter-intuitive, but they are organized this way to assure all module components are not deallocated from memory as long as one of its Views is active. In this way the Views lifecycle is also the lifecycle of the module - which actually makes perfect sense.

The creation and setup of module components is done in the Wireframe. The creation of a new Wireframe is almost always done in the previous Wireframe. More details on this later in the actual code.

Before we go into detail we should comment one somewhat unusual decision we made naming-wise and that's suffixing protocol names with "Interface" (LoginWireframeInterface, RegisterViewInterface, ...). A common way to do this would be to omit the "Interface" part but we've found that this makes code somewhat less readable and the logic behind VIPER harder to grasp, especially when starting out.

1. Base classes and interfaces

The module generator tool will generate five files - but in order for these to work you will need a couple of base protocols and classes. These are also available in the repo. Let's start by covering these base files: WireframeInterface, BaseWireframe, ViewInterface, InteractorInterface and PresenterInterface:

WireframeInterface and BaseWireframe

enum Transition {
    case root
    case push
    case present(fromViewController: UIViewController)
}

protocol WireframeInterface: class {
    func popFromNavigationController(animated: Bool)
    func dismiss(animated: Bool)
}

class BaseWireframe {

    unowned var navigationController: UINavigationController

    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }

    func show(_ viewController: UIViewController, with transition: Transition, animated: Bool) {
        switch transition {
        case .push:
            navigationController.pushViewController(viewController, animated: animated)
        case .present(let fromViewController):
            navigationController.viewControllers = [viewController]
            fromViewController.present(navigationController, animated: animated, completion: nil)
        case .root:
            navigationController.setViewControllers([viewController], animated: animated)
        }
    }
}

extension BaseWireframe: WireframeInterface {

    func popFromNavigationController(animated: Bool) {
        let _ = navigationController.popViewController(animated: animated)
    }

    func dismiss(animated: Bool) {
        navigationController.dismiss(animated: animated)
    }
}

The Wireframe is used in 3 steps:

  1. Initialization using a UINavigationController (see the init method). Since the Wireframe is in charge of performing the navigation it needs access to the actual UINavigationController with which it will do so.
  2. Navigation to a screen (see the show method). For this we've defined 3 types of transitions which are pretty self explanatory (see the Transition enum). The present case is different because here we are actually presenting the UINavigationController over a presenter UIViewController (see fromViewController associated value in .present Transition). This might seem confusing at first but it's a more robust solution as opposed to using the UINavigationController as a presenter.
  3. Navigation from a screen (see the popFromNavigationController and dismiss methods).

PresenterInterface

protocol PresenterInterface: class {
    func viewDidLoad()
    func viewWillAppear(animated: Bool)
    func viewDidAppear(animated: Bool)
    func viewWillDisappear(animated: Bool)
    func viewDidDisappear(animated: Bool)
}

extension PresenterInterface {

    func viewDidLoad() {
        fatalError("Implementation pending...")
    }

    func viewWillAppear(animated: Bool) {
        fatalError("Implementation pending...")
    }

    func viewDidAppear(animated: Bool) {
        fatalError("Implementation pending...")
    }

    func viewWillDisappear(animated: Bool) {
        fatalError("Implementation pending...")
    }

    func viewDidDisappear(animated: Bool) {
        fatalError("Implementation pending...")
    }
}

The PresenterInterface offers only optional methods which are used for the Presenter to performa tasks based on View events. For methods you use without implementing them you'll get a nice big fatal error.

ViewInterface and InteractorInterface

protocol ViewInterface: class {
}

extension ViewInterface {
}
protocol InteractorInterface: class {
}

extension InteractorInterface {
}

These two interfaces are initially empty. They exists just to make it simple to insert any and all functions needed in all views/interactors in you project. Both protocols need to be class bound because the Presenter will hold them via a weak reference.

Ok, let's get to the actual module. First we'll cover the files you get when creating a new module via the module generator.

2. What you get when generating a module

When running the module generator you will get five files. Say we wanted to create a Login module, we would get the following: LoginInterfaces, LoginWireframe, LoginPresenter, LoginView and LoginInteractor. Let's go over all five.

Interfaces

enum LoginNavigationOption {
}

protocol LoginWireframeInterface: WireframeInterface {
    func navigate(to option: LoginNavigationOption)
}

protocol LoginViewInterface: ViewInterface {
}

protocol LoginPresenterInterface: PresenterInterface {
}

protocol LoginInteractorInterface: InteractorInterface {
}

This interface file will provide you with a nice overview of your entire module at one place. Since most components communicate with each other via protocols we found very useful to put all of these protocols for one module in one place. That way you have a very clean overview of the entire behavior of the module. The LoginNavigationOption enum is used for all navigation actions which involve creating a new wireframe and navigating to it in which ever way possible. This will become clearer when we go over a concrete example.

Wireframe

final class LoginWireframe: BaseWireframe {

    // MARK: - Private properties -

    private let _storyboard: UIStoryboard = UIStoryboard(name: <#Storyboard name#>, bundle: nil)

    // MARK: - Module setup -

    func configureModule(with viewController: LoginViewController) {
        let interactor = LoginInteractor()
        let presenter = LoginPresenter(wireframe: self, view: viewController, interactor: interactor)
        viewController.presenter = presenter
    }

    // MARK: - Transitions -

    func show(with transition: Transition, animated: Bool = true) {
        let moduleViewController = _storyboard.instantiateViewController(withIdentifier: "LoginViewController") as! LoginViewController
        configureModule(with: moduleViewController)

        show(moduleViewController, with: transition, animated: animated)
    }
}

// MARK: - Extensions -

extension LoginWireframe: LoginWireframeInterface {

    func navigate(to option: LoginNavigationOption) {
    }
}

If you've created a storyboard which contains a LoginViewController, all you need to do is enter the storyboard name (see _storyboard var) here and the code will compile. We've made the assumption that you use the class name for an identifier but you can of course change this at any point in the future.

Presenter

final class LoginPresenter {

    // MARK: - Private properties -

    fileprivate weak var _view: LoginViewInterface?
    fileprivate var _interactor: LoginInteractorInterface
    fileprivate var _wireframe: LoginWireframeInterface

    // MARK: - Lifecycle -

    init(wireframe: LoginWireframeInterface, view: LoginViewInterface, interactor: LoginInteractorInterface) {
        _wireframe = wireframe
        _view = view
        _interactor = interactor
    }
}

// MARK: - Extensions -

extension LoginPresenter: LoginPresenterInterface {
}

This is the skeleton of a Presenter which will get a lot more meat on it once you start implementing the business logic.

View

final class LoginViewController: UIViewController {

	// MARK: - Public properties -

    var presenter: LoginPresenterInterface!

    // MARK: - Life cycle -

    override func viewDidLoad() {
        super.viewDidLoad()
    }

}

// MARK: - Extensions -

extension LoginViewController: LoginViewInterface {
}

Like the Presenter above, this is only a skeleton which you will populate with IBOutlets, animations and so on.

Interactor

final class LoginInteractor {
}

extension LoginInteractor: LoginInteractorInterface {
}

When generated your Interactor is also a skeleton which you will in most cases use to perform fetching of data from remote API services, Database services, etc.

3. How it really works

Here's an example of a wireframe for a Login screen which uses two types of navigation to navigate to a login and registration screen. Let's start with the Presenter

final class LoginPresenter {

    // MARK: - Private properties -
    static private let minimumPasswordLength: UInt = 6

    fileprivate weak var _view: LoginViewInterface?
    fileprivate var _interactor: LoginInteractorInterface
    fileprivate var _wireframe: LoginWireframeInterface
    fileprivate let _authorizationManager = AuthorizationAdapter.shared

    fileprivate let _emailValidator: StringValidator
    fileprivate let _passwordValidator: StringValidator

    // MARK: - Lifecycle -
    init (wireframe: LoginWireframeInterface, view: LoginViewInterface, interactor: LoginInteractorInterface) {
        _wireframe = wireframe
        _view = view
        _interactor = interactor

        _emailValidator = EmailValidator()
        _passwordValidator = PasswordValidator(minLength: LoginPresenter.minimumPasswordLength)
    }

}

// MARK: - Extensions -
extension LoginPresenter: LoginPresenterInterface {

    func didSelectLoginAction(with email: String?, password: String?) {
        guard let _email = email, let _password = password else {
            _showLoginValidationError()
            return
        }
        guard _emailValidator.isValid(_email) else {
            _showEmailValidationError()
            return
        }
        guard _passwordValidator.isValid(_password) else {
            _showPasswordValidationError()
            return
        }

        _view?.showProgressHUD()
        _interactor.loginUser(with: _email, password: _password) { [weak self] (response) -> (Void) in
            self?._view?.hideProgressHUD()
            self?._handleLoginResult(response.result)
        }
    }

    private func _handleLoginResult(_ result: Result< JSONAPIObject<User> >) {
        switch result {
        case .success(let jsonObject):
            _authorizationManager.authorizationHeader = jsonObject.object.authorizationHeader
            _wireframe.navigate(to: .home)

        case .failure(let error):
            _wireframe.showErrorAlert(with: error.message)
        }
    }

    private func _showLoginValidationError() {
        _wireframe.showAlert(with: "Error", message: "Please enter email and password")
    }

    private func _showEmailValidationError() {
        _wireframe.showAlert(with: "Error", message: "Please enter valid email")
    }

    private func _showPasswordValidationError() {
        _wireframe.showAlert(with: "Error", message: "Password should be at least 6 characters long")
    }
}

In this simple example the Presenter handles a login action selection which is delegated from the View. After that some validation is performed and then the actual login is performed using the Interactor. In the event of a successful login a navigation to a home screen is initiated. Let's take a look at the Wireframe in this example for a bit more clarity.

final class LoginWireframe: BaseWireframe {

    // MARK: - Private properties -
    private let _storyboard: UIStoryboard = UIStoryboard(name: "Login", bundle: nil)

    // MARK: - Module setup -
    func configureModule(with viewController: LoginViewController) {
        let interactor = LoginInteractor()
        let presenter = LoginPresenter(wireframe: self, view: viewController, interactor: interactor)
        viewController.presenter = presenter
    }

    // MARK: - Transitions -
    func show(with transition: Transition, animated: Bool = true) {
        let moduleViewController = _storyboard.instantiateViewController(withIdentifier: "LoginViewController") as! LoginViewController
        configureModule(with: moduleViewController)

        show(moduleViewController, with: transition, animated: animated)
    }

    fileprivate func _openHome() {
        let wireframe = HomeWireframe(navigationController: navigationController)
        wireframe.show(with: .root)
    }
}

// MARK: - Extensions -
extension LoginWireframe: LoginWireframeInterface {

    func navigate(to option: LoginNavigationOption) {
        switch option {
        case .home:
            _openHome()
        }
    }
}

This is also a simple example of a wireframe which handles only one type of navigation. You've maybe notices the showAlert Wireframe method used in the Presenter to display alerts. This is used in the BaseWireframe in this concrete project and looks like this:

func showAlert(with title: String?, message: String?) {
	let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
	showAlert(with: title, message: message, actions: [okAction])
}

This is just one example of some shared logic you'll want to put in your base class or maybe one of the base protocols.

This was just a short example of how one module can come together. Soon we'll make an entire example project available on GitHub which will contain much more use cases.

How it's organized in Xcode

Using this architecture impacted the way we organize our projects. In most cases we have four main subfolders in the project folder: Application, Common, Modules and Resources. Let's go over those a bit.

Application

Contains AppDelegate and any other app-wide components, initializers, appearance classes, managers and so on. Usually this folder contains only a few files.

Common

Used for all common utility and view components grouped in sub folders. Some common cases for these groups are Analytics, Constants, Extensions, Protocols, Views, Networking, etc. Also here is where we always have a VIPER subfolder which contains the base VIPER protocols and classes.

Resources

This folder should contain image assets, fonts, audio and video files, and so on. We use one .xcassets for images and in that folder separate images into logical folders so we don't get a long list of files in one place.

Modules

As described earlier you can think of one VIPER module as one screen. In the Modules folder we organize screens into logical groups which are basically user-stories. Each group is organized in a subfolder which contains one storyboard (containing all screens for that group) and multiple module subfolders.

iOS VIPER MODULES

Useful links

Contributing

Feedback and code contributions are very much welcome. Just make a pull request with a short description of your changes. By making contributions to this project you give permission for your code to be used under the same license.

Credits

Maintained and sponsored by [Infinum] (http://www.infinum.co).

ios-viper-xcode-templates's People

Contributors

damjanvujaklija avatar vburojevic avatar

Watchers

 avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.