My journey to understanding Software Architecture as a junior iOS dev

Rolando Rodríguez
The Startup
11 min readMay 9, 2020

--

A beginner friendly approach to software architecture.

In this article I describe part of my journey as a junior iOS developer and some of the issues I faced and the solutions I came up with after some research.

Background

I began my journey as an iOS developer back in 2015 in a company that developed smart-watches and had a companion application to manage the watch both for Android and iOS. The project I inherited from “senior” iOS developers was built using Objective-C and had at least 200 files, most of them were UIViewController implementations for the multiple screens the app had. Build times were awful and the project had a really hard time scaling, objects had a lot of responsibilities and requests from upper management to add new features took way too much time and often seemed impossible. Looking for help in the iOS dev community I found that what I was facing at the time was very common among junior devs and it had a name, MassiveViewController.

Apple encourages developers to use MVC in iOS apps, by its deep integration in UIKit. Following Apple’s MVC, junior developers wrongly put everything in the UIViewController like view layout, view updates, model manipulation, data access, networking, etc.

Looking for a solution, I became familiar with popular architectural patterns in the presentation layer that vary from MVC such as MVP and MVVM. As many junior devs in the community, I thought that the MV* variations mentioned before would fix my problem. And as other junior developers I made the mistake of trying to fit every object in the project in one of the categories that compose the acronym of the pattern of choice, i.e using MVVM, objects either were a Model, a ViewModel or a View.

Other mistake I made was to give objects too many responsibilities, for instance ViewModels ended up being in charge of networking, data access, model manipulation, etc, just like using a UIViewController with MVC. Later I learnt that not all objects would fit it in one of those categories and that responsibilities should be limited.

So after learning from my mistakes and started using the patterns the right way I realized that even though the patterns helped my code to be more readable and objects in the project had fewer responsibilities than before, they didn't seemed to really fix the core issue, maintainability was still difficult and adding new features a nightmare. This is because the problem wasn’t in how the UI and Presentation were talking to each other, but in how the whole software was structured, in other words the problem I was facing was with the software architecture.

Some Research

So What is MVC?

Model View Controller (MVC) was introduced back in 1979 by Trygve Reenskaug in the Model View Controller Report. Reenskaug defines MVC in this article as follows.

MVC was conceived as a general solution to the problem of users controlling a large and complex data set.

The essential purpose of MVC is to bridge the gap between the human user’s mental model and the digital model that exists in the computer. The ideal MVC solution supports the user illusion of seeing and manipulating the domain information directly. The structure is useful if the user needs to see the same model element simultaneously in different contexts and/or from different viewpoints. The figure below illustrates the idea.

Fig.1 Trygve Reenskaug’s MVC — Diagram by Trygve Reenskaug

If we analyze the diagram above MVC refers to the interaction between the user, an input tool, a process and an output. MVC seems like a very generic concept, it’s so generic that it can apply to almost any architecture that isn’t total spaghetti code. But nowadays applications are not only composed of a view like a text-editor, a model like a text, and a controller to manage the interaction. Applications now hold complex behaviors and processes, need remote data access, analytics and what not.

Essentially MVC refers to what today we know as the presentation part of an application. Therefore MVC, MVP and MVVM only solve the way we structure the presentation of an application, each in a different way.

More Research

I knew that there had to be a way to design software that wasn’t so hard to maintain. So I dived into lots of software architecture literature, Clean-Architecture by Uncle Bob, Hexagonal Architecture, Layered Architecture and Microservices. Throughout my readings I can conclude that they are essentially all the same. Bear with me here, each architecture is different from each other and serve different purposes. This is in the surface though, deep down they intend the same goal, which is to separate responsibilities and establish a standard way of communication between its components, improve maintainability and scalability and reduce development time.

So, how do I go from MassiveViewController to a scalable easy to maintain project 🚀?

After all my research, I had some knowledge to build my own implementation of a software architecture and solve my problem. I found that a clean-layered- modular oriented architecture was the way to go, so I did that.

The architecture I came up with focuses on building software by composing multiple independent reusable modules, a practice that many developers use.

Benefits

This approach lets developers built software that is easy to test, maintain and scale. On the business side, this architecture helps developers deliver applications in small chunks which increases speed of development since multiple developers can build different parts of the software simultaneously, it even allows for software to be developed with no need to wait for the UI to be designed. Ultimately this means better productivity which translates to our clients using higher quality products, better costumer retention and maybe even increased profitability.

Before diving in

This is not a silver bullet. Every software behaves differently and is designed to solve a specific need, hence each software must have its architecture designed to fit its purpose. Nonetheless this simple architecture which is based on widely popular architectural patterns, should fit most applications.

Structure

Most applications are composed of multiple features that use a database, some sort of storage and third party services.

Features

A Feature refers to a specific set of tasks and use cases of an application, i.e Messages in a social networking app. Every Feature must live in its own module (think of a sandbox) so it can be replaced by other implementations that perform the same required tasks, for instance replace the Messages Feature with a new version with new UI and fancy animations without breaking the whole app.

Breaking down a Feature

A Feature like Messages is composed of small pieces that do a specific task, these pieces are what users actually interact with.

Taking the previous Messages example into consideration, let's analyze a real world app like iMessage.

Fig.2 — iMessage in iOS7 Images by Apple Inc.

If we break it down we will find that the messaging feature of an app is probably composed by some of the next parts:

  • ChatList
  • NewChat
  • Chat

Through this exercise we realize that the Messages Feature is only a concept that encapsulates a lot of parts that execute specific tasks.

Fig.3 Messages Feature

Some Features may also contain Sub-Features. Sub-Features are parts of a Feature that may render too complex to be implemented in a single module.

Since Sub-Features are Features, they also live in their own module. Take the Chat component in iMessage as an example, entering in a Chat we might find the following.

Fig. 4 — iMessage chat in iOS7 Images by Apple Inc.

As we can see it isn’t just a list of messages, it contains tasks to send voice messages, video, images and what not. The Chat component seems large and we may consider it as a Sub-Feature, thus it may also contain components like MessagesList and SearchMessage and other Sub-Features such as Camera which might be used to take pictures and then send them. So the Chat Sub-Feature might render as follows.

Fig.5 Chat Sub-Feature

The architecture

Software architecture is all about communication & responsibilities.

Now that we understand how a feature is composed we may start thinking of a more generic diagram to represent the architecture. In this section we will see each module in detail with an example.

Domain: Business Logic

Business logic such as Use Cases and Models are defined all together in a separate very low level single module named Domain.

Use Cases should contain the business logic actual implementation, here you should manipulate your models, access data or storage all through interfaces defined in the Domain module. The Domain module must only depend on the chosen programming language’s base framework and some interfaces needed to perform its tasks. For example in case we were using Swift, the Domain module should only depend on Foundation, though there’s one exception.

The exception

A Use Case works like a black box, it receives an input and then manipulates models and other objects to perform a specific task. When finished, it returns its outputs to the interested parties. When asynchronously returning outputs callbacks and closures may be easier to use but be careful if you end up using nested closures as they could cause retain-cycles (if you use classes for your Use Cases), retain-cycles affect your app’s performance and may even cause it to crash. Which is why we prefer to use functional reactive programming and the Observer design pattern. Therefore we recommend that ‘execute’ methods should publish results to observers via a subscription. You could use KVO from Cocoa or frameworks such as RxSwift, Combine or whatever framework for FRP you like. So in this case Domain can also depend on Combine or the framework you choose.

Fig.6 Domain Module

Interfaces

Interfaces, or protocols in Swift, serve as blueprints or contracts that specify a behavior for objects to implement. This contracts allow objects to communicate with other objects that expect this interfaces as dependencies without creating tight coupling, this also helps to avoid inheritance when using classes.

In the Domain module we use interfaces to communicate UseCases with objects outside the module (to avoid module dependency). Objects we may want to communicate with include objects responsable for data access and maybe some other objets for different tasks such as analytics and what not.

Example

Following the iMessage example we may have a SendTextMessageUseCase which only job would be to send a message with text. We may write it as follows:

This is a very simple Use Case example that adds the provided Message to a database through an object that conforms to MessageRepository protocol. In this example we use a Model, a Use Case and a Repository. The idea behind the Use Case is that you should write your business logic inside the execute method and return whatever output it produces to the caller once it finishes. This way we isolate business logic from UIViewControllers or other classes which lets us reuse code in other modules or even other applications.

Feature

An app will have multiple Feature modules. As mentioned before a Feature is composed of small pieces in charge of performing a specific set of tasks. The Feature will grab its logic from the Domain module, so all it should contain are UI & Presentation related objects.

In UIKit projects:

  • UIViewControllers
  • UIViews
  • UINavigationControllers
  • Xibs
  • Delegates
  • Routers **
  • ViewModels / Presenters *

In SwiftUI projects:

  • Views
  • ViewModels
  • Routers **

*This should be present on your Feature module whether you use MVVM or MVP. Though if you prefer to use MVC there’s no need to add this files, you can use ViewControllers to stitch your logic (Domain), networking & database operations (Data) with your UI&Presentation.

** Depends on how you manage your routing logic.

Fig.7 Feature Module

This is a very simple module since it only holds UI&Presentation related files. In the example shown we used MVVM, the Model part comes from the Domain which the ViewModel takes from, the View is represented by the ViewController (which receives inputs from its underlaying view and manages its lifecycle, UIKit only) and at last the ViewModel which manipulates Use Cases from Domain on ViewController’s disposal.

Routing and navigation is up to you, you may choose to use a Router or not, since it belongs to UI & Presentation how we implement it doesn’t really affect the architecture, more on this later.

Keep in mind that the green box is only a representation, it may contain a lot of Single Component boxes since each of those represent a ‘part’ of the Feature like ChatsList, MessagesList or Camera.

Data & Third Parties

Data module contains DataModels, DataModels exist in a 1:1 relationship with Models in Domain, Mappers help DataModels be mapped into Models. Repositories must only return Models. Under the Bridge Pattern, Repositories are defined as objects that encapsulate data access.

Interfaces (protocols) describe a contract between objects to communicate. In simpler words protocols specify all the properties and methods a type must implement to be able to communicate with its dependees in any module. In this case as data interfaces will also be used in the Domain module by UseCases, they must be declared in the Domain module to ensure independency between modules.

For example if Type A implements Interface S and Type B also implements S, this means that Type A and B should behave the same in eyes of the Domain module and this also means that both implementations are interchangeable.

Fig.8 Data module interacting with other modules

Interfaces in this example:

  • <Repository>: Defines a set of methods a type must implement to execute data related tasks required by a set of Use Cases. We should use one repositories for each model as a rule, having a single repository using generics gets complicated and it’s not recommend since each model may need different operations.
  • <ThirdPartyService>: Defines a set of methods a type must implement to use a third party service. Setting an interface for this means that we can swap the framework or library we use and don’t affect the app at all.

Keep in mind that every module is plug and play. This allows modules to be shared between other modules or even across applications.

Module Provider

The Module Provider switches between a Feature module’s root navigation controller, on UIKit projects and rootView on SwiftUI projects. The switching is done through the activeModule attribute of type Module of the ModuleProvider class. The Module is just an enum with all the available module names. The switch could be done through a singleton or via NotificationCenter, this means that any module can tell the ModuleProvider that it’s time to switch to a new module or to dismiss itself to the root module.

It’s up to you to choose how to provide AppDelegate or SceneDelegate with the appropriate rootViewController or rootView respectively through the ModuleProvider, to avoid toying with Combine, DI and Singletons (more on that in a following post) the following implementation does the job.

Fig.9 Module Provider example

With that implementation you can send the notification to change a module from any ViewModel, Presenter or ViewController you need.

The whole picture

Fig.10 Dependency diagram of a the whole architecture

--

--

Rolando Rodríguez
The Startup

I’m a software engineer, l code and design some stuff 👨🏾‍💻