Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

New iOS Software Architecture: 4V Engine

DZone's Guide to

New iOS Software Architecture: 4V Engine

Learn about the structure of 4V Engine, a new, clean, more testable software architecture for iOS mobile apps written in Swift.

· Mobile Zone
Free Resource

Download this comprehensive Mobile Testing Reference Guide to help prioritize which mobile devices and OSs to test against, brought to you in partnership with Sauce Labs.

Should You Read This Article?

This article is about a new software architecture which has more layers than VIPER and MVVM-C. It means that it may be more complex than other known architectures.

If you want to continue reading this article, you must accept my point of view that if we want a clean and testable architecture, we should have several layers with just a responsibility.

I don’t want to sell it as the perfect architecture which can solve all your problems. It may not suit your needs perfectly. For this reason, I would suggest you read this article thoroughly and judge by yourself if this architecture makes sense for your projects.

If you are wondering why MVC is not enough for iOS development, I would suggest you to jump to "Why MVC Is Not Enough."

Introduction

I created this blog writing an article about MVVM-C. Then, I wrote an article about SOLID principles. At this point, you may be thinking that I don’t practice what I preach. If I speak about the “Single Responsibility Principle,” why do I want to use MVVM-C even if the Coordinator layer has more than one responsibility? It creates the stack (View Model, View and Service), adds View in a parent UIKit component, and decides the routing adding/removing child coordinators. MVVM-C must be refactored a little bit to avoid breaking the Single Responsibility Principle.

In this article, I will explain an alternative to the main iOS software architectures: 4V Engine.

Happy reading!

Why MVC Is Not Enough

I’ve already covered this point in my previous article about MVVM-C but I want to write it again because I think it’s very important.

A common approach to become an iOS developer is looking at the documentation and following the patterns suggested by Apple to write some plain projects. It means that the majority of us have begun using MVC as software architecture to create our first applications. Step by step, we started to get used to MVC. At this point of the learning process, we think that MVC is the right way. It works, why should we move to another architecture which adds complexity to our code?

There are, mainly, two reasons:

  1. SOLID principles: The view controller has too many responsibilities.
  2. Testability: The view controller is difficult to test since it’s tightly coupled with UIKit.

If we want to solve the points above, we should move to another architecture. Unfortunately, nothing comes for free. This change has a cost: complexity. If we look at VIPER and MVVM-C, we notice that there are several layers to manage and let communicate together. It may be overkill if we have a plain application or we don’t really care about SOLID and testability.

My personal point of view is: I like having well-written code which follows the SOLID principles and is properly tested to avoid bugs as much as possible. For this reason, I wanted to spend time and effort to create a new clean software architecture.

Problems of MVVM-C

As said previously, MVVM-C is not enough. The Coordinator layer breaks the Single Responsibility Principle and must be split into three components with the following responsibilities:

  • Manage the app routing.
  • Create the stack (View Model, View and Service).
  • Show View in a parent UIKit component.

This idea to split the Coordinator led me to 4V Engine.

I want to thank my friend Ennio Masi for pointing out this Coordinator problem, which motivated me to find a solution.

Problems of VIPER

Another famous iOS architecture is VIPER. It has several layers which are very similar to MVVM-C with different naming.

If we analyze VIPER, we find the same problem of MVVM-C. The guilty layer is called Wireframe and it’s the router of the architecture.

Let’s use, for the sake of the explanation, the following code copied from VIPER-SWIFT:

class AddWireframe: NSObject, UIViewControllerTransitioningDelegate {

    var addPresenter : AddPresenter?
    var presentedViewController : UIViewController?

    func presentAddInterfaceFromViewController(_ viewController: UIViewController) {
        let newViewController = addViewController()
        newViewController.eventHandler = addPresenter
        newViewController.modalPresentationStyle = .custom
        newViewController.transitioningDelegate = self

        viewController.present(newViewController, animated: true, completion: nil)

        presentedViewController = newViewController
    }

    //.....

We notice that presentAddInterfaceFromViewController has too many responsabilities:

  1. Manage the routing.
  2. Create the View and its properties.
  3. Show View in the parent UIViewController.

We notice that—with these three points—we have the same problems of the MVVM-C Coordinator.

4V Engine

We’ve analyzed the common iOS software architectures and we have found some problems. If we are willing developers and we want to improve our code, we would need an alternative which refactors the previous ones. With this goal in mind, I created this new software architecture:

Don’t be afraid, it may be confusing, but we are going through the explanation of each layer soon.

As we can see in the diagram above, the core of this architecture is made by View Presenter, View Factory, View and View Model, for this reason, this architecture is called 4V Engine.

Getting Started

Now, it’s time to explain each single layer. Since I think that jumping into the code is the best way to learn something, we’ll use a sample app to cover each layer with an example.

You can find the GitHub repo here.

It’s a very plain application with two components:

  • Users List: a users list fetched from a remote API.
  • User Details: a view with the name of the user selected in the users list- we can select a user tapping the info button of a UITableViewCell in the users list.

Layers

I think the best way to explain the layers is starting from the bottom (Model) to top (Router). Let’s start.

Model

The model represents the data of our application.

In our sample app, we have a model User:

struct User {
    let name: String
} 

which represents the single user parsed from the API response.

Interactor

The Interactor is the same used in VIPER.

This layer manages the Model to prepare the data for the View Model. The View Model shouldn’t perform any operations directly on the model, but it should delegate Interactor for any data manipulations.

In our sample app, we have an interactor which fetches the users from a remote API—thanks to the service HTTPClient—and then transforms the JSON data received in an array of User—thanks to the helper class UsersParser—to be used inside our View Model:

final class UsersListInteractor {
    private let httpClient: HTTPClientType

    init(httpClient: HTTPClientType = HTTPClient()) {
        self.httpClient = httpClient
    }

    func fetchUsers(completion: @escaping ([User]) -> Void) {

        guard let url = URL(string: "https://jsonplaceholder.typicode.com/users") else {
            completion([])
            return
        }

        let httpCompletionHandler: (Data) -> Void = { data in
            guard let jsonData = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [[String: Any]] else {
                completion([])
                return
            }

            let users = UsersParser.parse(jsonData)
            completion(users)
        }

        httpClient.get(at: url, completionHandler: httpCompletionHandler)
    }
}

View Model

We can consider View Model the most important layer of this architecture. Its responsibility is to interact with the UI to decide what to show and how to behave after a UI action.

This layer shouldn’t have any UIKit references. If we want a communication between View and View Model, we should use a UI binding mechanism. I’ve already shown the main mechanisms in my previous article. For this sample app, I’ve decided to avoid RxSwift for the binding since it would have increased the complexity of the examples. To keep everything as plain as possible, the binding has been achieved with the delegation pattern.

We can use View Model with an Interactor to get the data to show in the UI, as we can see in the sample app:

// Delegate used to bind the UI and the View Model
protocol UsersListViewModelDelegate: class {
    func usersListUpdated()
}

final class UsersListViewModel {

    // Value used in View to know how many table rows to show
    var usersCount: Int {
        return users.count
    }

    private weak var delegate: UsersListViewModelDelegate?
    private weak var navigationDelegate: UsersListNavigationDelegate?

    private let usersListInteractor: UsersListInteractor
    private var users = [User]()

    init(usersListInteractor: UsersListInteractor, navigationDelegate: UsersListNavigationDelegate) {
        self.navigationDelegate = navigationDelegate
        self.usersListInteractor = usersListInteractor

        loadUsers()
    }

    // Asks the interactor the list of users to show in the UI
    private func loadUsers() {
        usersListInteractor.fetchUsers { [unowned self] users in
            self.users = users

            DispatchQueue.main.async {
                // Method used to ask the View to update the table view with the new data
                self.delegate?.usersListUpdated()
            }
        }
    }

    private func user(at indexPath: IndexPath) -> User {
        return users[indexPath.row]
    }

    // Sets the delegate to bind the UI
    func bind(_ delegate: UsersListViewModelDelegate) {
        self.delegate = delegate
    }

    // Method used in View to know which user name to show in the cell
    func userName(at indexPath: IndexPath) -> String {
        let user = self.user(at: indexPath)
        return user.name
    }

    // Method called in View when the user taps a cell detail button
    func userDetailsSelected(at indexPath: IndexPath) {
        let user = self.user(at: indexPath)

        // Method used to notify the router that a user has been selected
        navigationDelegate?.usersListSelected(for: user)
    }
}

With MVC, we are used to keeping the business logic inside the view controller. With 4V Engine, we can move the business logic inside the View Model and test it easily, since we don’t have dependencies with UIKit.

Note:

  • navigationDelegate is used to communicate with Router. We’ll see it in the Router section.

  • The method bind is used for the UI binding between View and View Model. We’ll see it in the View section.

View

The View layer represents any UIKit components used to show something on the device screen.

In the sample app, the View is a UIViewController for the user details and a UITableViewController for the users list.

The advantage of a good architecture is that we can test easily our layers. View is usually the most difficult to test because it’s coupled with its dependency UIKit. For this reason, we must keep this layer as plain as possible and move the business logic into a testable layer. The “testable” layer is the View Model. As we’ve seen in the View Model, the UI data is driven by the View Model. In this way, we can move the business logic inside View Model. View becomes a dumb layer, which is used merely to show something on the device screen.

The important concept to understand with the View is the UI binding, which allows us to set the communication between View Model and View. If you don’t know what is the UI Binding, please have a look at my previous article.

Here an example of View from the sample app:

class UsersListTableViewController: UITableViewController {

    // The view model used for the binding
    private unowned let viewModel: UsersListViewModel

    init(viewModel: UsersListViewModel) {
        self.viewModel = viewModel

        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        let nib = UINib.init(nibName: "UsersListTableViewCell", bundle: nil)
        tableView.register(nib, forCellReuseIdentifier: "Cell")

        // Binds View and View Model
        viewModel.bind(self)
    }

    // MARK: - Table view data source

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // Asks the View Model how many users are available
        return viewModel.usersCount
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)

        // If it's the custom cell, configure it
        if let usersListCell = cell as? UsersListTableViewCell {
            // Asks the View Model the user name for a specific index path
            let userName = viewModel.userName(at: indexPath)
            // Sets the user name
            usersListCell.configure(userName: userName)
        }

        return cell
    }

    override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
        // Notifies the View Model that a detail button has been tapped
        viewModel.userDetailsSelected(at: indexPath)
    }
}

As we can see in this example, the UI binding is often bidirectional; sometimes we ask some data to View Model and sometimes we get notified by View Model to update the UI—like with usersListUpdated.

Note: The property viewModel has the keyword unowned. It’s required to avoid a retain cycle. Since View Factory already keeps a strong reference of View Model—as we’ll see in the View Factory—View doesn’t need to keep a strong reference of its View Model.

View Factory

So far, the layers have been very similar to VIPER and MVVM-C. Now, it’s time to explain the layers which may be confusing at first glance.

The responsibility of View Factory is creating the core of the architecture: View, View Model and Interactor.

View Factory alone may not make a lot of sense, we must see it in the right context. We’ll understand its usage in the View Presenter section.

Let’s see an example from the sample app:

final class UsersListViewFactory {

    let viewController: UsersListTableViewController
    private let viewModel: UsersListViewModel

    init(navigationDelegate: UsersListNavigationDelegate) {
        let interactor = UsersListInteractor()
        viewModel = UsersListViewModel(usersListInteractor: interactor, navigationDelegate: navigationDelegate)
        viewController = UsersListTableViewController(viewModel: viewModel)
    }
}

Note:

  • We are exposing viewController to be used in View Presenter. We may also expose the View Model for specific reasons. I think it can be private most of the time. The decision depends on what you have to achieve.
  • We are injecting UsersListNavigationDelegate in UsersListViewModel to let the Router communicate with the View Model in an abstract way. We’ll see the details of this delegate in the Router section.

View Presenter

The name of this layer may be a little bit confusing. We’re used to calling Presenter the layer which updates the View—we have this layer in VIPER and MVP. In this architecture, the presenter is called View Model and this layer is not a presenter but a View presenter. Keep reading to understand its responsibility.

The View Presenter is the last piece of the puzzle of a component written with 4V Engine.

This layer has the responsibility to show the component in the device screen.

To achieve this goal, it must know what and where to show. The View to add is provided by the View Factory and the parent is injected from outside.

Let’s see an example from the sample app:

final class UsersListViewPresenter: ViewPresenter {

    private let viewFactory: UsersListViewFactory

    init(navigationDelegate: UsersListNavigationDelegate) {
        viewFactory = UsersListViewFactory(navigationDelegate: navigationDelegate)
    }

    // Method to add the component in a parent view controller
    func present(in parentViewController: UIViewController) {
        // Method of UIViewControllerExtension.swift to add a child view controller filling the parent view with
        // autolayout.
        // Look at UIViewControllerExtension.swift for more details
        parentViewController.addFillerChildViewController(viewFactory.viewController)
    }

    // Method to remove the component from the device screen
    func remove() {
        viewFactory.viewController.view.removeFromSuperview()
        viewFactory.viewController.removeFromParentViewController()
    }
}

In this example, present() is a very plain method to add a child view controller. If you have fancy UIViewControllertransitions, this method is the right place to manage them.

You'll notice that we are propagating UsersListNavigationDelegate through the layers to use it in the View Model. This is the downside of splitting the architecture into several layers.

Router

We’ve just finished seeing the layers of a single component. At this point, we have a component almost ready to be shown on the screen. We need one last step: to decide when to show the component. This is the responsibility of Router.

We usually have a Router per story. In this context, my definition of story is: The set of components which, together, define a flow in our application.

In our sample app, we have the story Users which is the set of users list and user details together. Other stories can be:

  • Onboarding: set of views to show how to use the application.
  • Registration: set of views to create a new account, accept terms of use, validate email, etc.
  • Items Purchase: set of views to show the basket, add delivery address, add card details for the payment, etc

Let’s see how to use Router for the story Users in the sample app:

// Delegate used to navigate from users list to user details
protocol UsersListNavigationDelegate: class {
    func usersListSelected(for user: User)
}

// Delegate used to close the user details
protocol UserDetailsNavigationDelegate: class {
    func userDetailsCloseDidTap()
}

final class UsersRouter {

    // Parent view controller to add the components
    fileprivate let parentViewController: UIViewController

    // Dictionary of presenters used
    fileprivate var presenters = [String: ViewPresenter]()

    init(parentViewController: UIViewController) {
        self.parentViewController = parentViewController
    }
}

extension UsersRouter: Router {
    // Shows first component, the users list
    func showInitial() {
        let usersListPresenter = UsersListViewPresenter(navigationDelegate: self)
        usersListPresenter.present(in: parentViewController)

        presenters["UsersList"] = usersListPresenter
    }

    // Closes the router removing all its components
    func close() {
        presenters.keys.forEach { [unowned self] in
            self.removePresenter(for: $0)
        }
    }

    fileprivate func removePresenter(for key: String) {
        let userDetailsPresenter = presenters[key]
        userDetailsPresenter?.remove()

        presenters[key] = nil
    }
}

extension UsersRouter: UsersListNavigationDelegate {
    func usersListSelected(for user: User) {
        let userDetailsPresenter = UserDetailsViewPresenter(user: user, navigationDelegate: self)
        userDetailsPresenter.present(in: parentViewController)

        presenters["UserDetails"] = userDetailsPresenter
    }
}

extension UsersRouter: UserDetailsNavigationDelegate {
    func userDetailsCloseDidTap() {
        // Removes user details components from the parent view controller
        removePresenter(for: "UserDetails")
    }
}

Note:

  • Router has a dictionary with the presenters used. In this way, if we want to remove a component like in userDetailsCloseDidTap, we can easily get the right presenter using a key.

We can have a look at AppDelegate to understand how to use the router:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    var usersRouter: Router?

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

        let rootViewController = UIViewController()

        window = UIWindow()
        window?.rootViewController = rootViewController

        let usersRouter = UsersRouter(parentViewController: rootViewController)
        usersRouter.showInitial()

        window?.makeKeyAndVisible()

        self.usersRouter = usersRouter

        return true
    }
}

Conclusion

I consider this article a presentation of the version 1.0.0 of 4V Engine. I changed it a lot of times and I’m sure that there is still room for improvement. For this reason, I would like some comments with your opinions, it would be greatly appreciated. Thank you.

Analysts agree that a mix of emulators/simulators and real devices are necessary to optimize your mobile app testing - learn more in this white paper, brought to you in partnership with Sauce Labs.

Topics:
mobile ,mobile app development ,ios ,architecture ,swift ,mvvm

Published at DZone with permission of Marco Santarossa, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}