When you’re a self-taught software developer, you may find it uncomfortable to talk about software architecture. Maybe you’re confused with so many paradigms – MVC, MVVM, VIPER, reactive, such that you get the feeling that these are just buzzwords touted by charlatans to sell books.
Design patterns exists because they are solutions to common problems. The more they get used, imply that the problems are more common. Authors just distill the problem and solution pair such that they can be understood and re-applied in similar situations.
Software architectures are just like design patterns, but at a slightly larger scale. They exist due to reoccurring problems in structuring software that can be solved in a similar manner. Similar to design patterns, different architectures exist because they were intended to solve different kinds of problems – be it the domain of the software, the technical environment of the software, even non-functional aspects like the development schedule, team dynamics, or the expected lifetime of the software. Also like design patterns, software architectures are guidelines and not hard and fast rules.
In this article you would learn:
- How to compartmentalize code of an iOS app so that it makes sense.
- How to structure an app so that you can change the implementation of persistence or other details as the need arises.
- Know the best practices of architecting applications.
I’m going to use Clean Architecture and VIPER as a case study for this article. When done reading, you would be able to directly implement what you’ve learn here to some of your iOS apps. Furthermore you’d also be confident to identify what kinds of apps that the architecture won’t be appropriate.
Trust me, I have a computer science degree majoring in software engineering. Design patterns was my thesis’ topic. I also have two software architect certificates from two different companies. Look me up in LinkedIn to see my credentials.
Put simply, software architecture is the way code is organized so that:
- it makes sense thus helps its construction and probably maintenance.
- it accomplishes its intended function and solves the original problem.
With this definition in mind, Clean Architecture – which is a particular style of software architecture defined by Robert C. Martin (a.k.a. Uncle Bob) – intends to maximize the potential lifetime of a piece of software by separating its fragile details from its core policies. Because persistence libraries may change, APIs may get deprecated, user interfaces tend to follow fashion, and so on. However the core of the software should survive all of these changes relatively untouched.
Clean Architecture organizes code into a series of concentric rings. The inner parts of these rings are policy, or what the software is from the problem domain point of view. Whereas the outer rings are the details or how the software interacts with its environment. Furthermore code dependency always points inwards, from detail into policy. This way, when code in an outer layer changes, the inner layers won’t get impacted. In other words, the inner parts would be more stable whereas fragility gets moved to the outer layers.
Code at outer layers are abstractions to serve inner layers. The inner layers would only perform pure computations – no I/O nor interact with the user. When an inner layer code need to perform such functionality, it defines an interface to be implemented by code in an outer layer. Then the concrete implementation gets injected at runtime.
The basic idea for Clean Architecture isn’t new. Many operating systems have hardware abstraction layers which role is to mask out the differences between various processor models and computer architectures. Similarly device drivers handle the details of interacting with a particular device. Even database connectivity stacks like JDBC and ODBC has the concept of “drivers” to handle the wire protocol of connecting to databases. Clean Architecture is only distilling the many occurrences of these design patterns into something that can be applied in a wider context. Therefore you can be confident that Clean Architecture is genuinely useful and long-lasting because its foundations has been applied in many other situations and were a solution to those problems.
When an idea or technology has been around for a long time, they would be around for even longer. Because they have proven to be useful and not a mere fad. Like the writing pen – from feather quills, to wooden graphite pencil, to fountain pens, to ballpoint pens, and even Apple Pencil. The details have changed but the principle of use is still the same. More so, older models like graphite pencils and fountain pens are still manufactured and in use today because they are still useful. The idea has passed the test of time, or more formally, the Lindy Effect is at play.
Why Clean Architecture is Hard to Understand
Some iOS developers may have difficulties in understanding Clean Architecture because of terminology clashes. Uncle Bob describe it in a platform-neutral manner, but some terminologies clashes with those used in Apple’s technology stack. Have a look at the following schematic on Clean Architecture taken from his blog post.
If you look at the above diagram, you can see that some words have specific meanings in programming for Apple platforms:
- entities – are they Core Data entities or some other database objects?
- controllers – are view controllers part of these?
Also notice that the word “entities” are at the core whereas “DB” (probably database) is at the outermost ring. You might wonder, “So, is the data store at the center or is it at the edge?”
Another potential confusion is why controllers belong in an inner layer but UI is at an outer layer? Does this mean view controllers can’t depend on view classes? That’s absurd.
I’ll address these shortly – keep on reading.
Have a look at the following architecture diagram. Taken from Martin Fowler’s PresentationDomainDataLayering article, this is a rather “typical” three-layer architecture consisting of presentation logic, domain logic and data storage.
This architecture is useful because it’s rather intuitive and very simple. At the bottom layer there’s the data store which performs I/O to the disk (or cloud). Next up there’s business domain processing where the data is being operated upon. Finally the highest layer performs I/O to the user (and consists of the user interface).
A major problem with this architecture is that everything depends on the data store. Thus if more databases are added, data are split between databases, or a rather “trivial” change such as switching database software would probably impact the domain logic and presentation code.
There should be a better way, and there is.
Intuitive Clean Architecture
That intuitive three-layer architecture described previously can be made clean via a simple change. Just changing the direction of the dependency between the data and domain layer such that the former depends on the latter and not the other way around.
In this architecture, the data layer becomes an implementation detail layer. It talks to the database and/or cloud systems as before. However its source code is an implementation of the interfaces defined by the domain layer. In turn, source code in the domain layer has no dependency to any names in the data layer. At runtime, object instances of concrete classes in the data layer gets injected as collaborators to objects coming from the domain layer.
This is probably the simplest implementation of Clean Architecture thus far. That is, taking the standard three-layer architecture and free the domain layer from being dependent on the data layer.
What is VIPER
VIPER stands for View, Interactor, Presenter, Entity, Router. It is an architectural style which revolves around these groups of classes:
- Views – presents the user interface and accepts user input.
- Interactors – performs data processing and applies business rules.
- Presenters – formats data for presentation and parses input from the user.
- Entities – represents nouns of the business rules and things that gets persisted.
- Routers – handles navigation between views.
VIPER is among several architecture styles that are intended to solve the massive view controller problem which sometimes happen in iOS apps.
UIViewController subclasses tend to be a magnet for code and thus it may do too many things thus the files containing it becomes too large and hard to maintain and extend. In contrast, VIPER code would move most logic out of the view controller and make it only as a mediator between
UIView subclasses and a presenter.
Unlike some other “new architecture” ideas, VIPER doesn’t need 3rd party libraries to implement in an iOS app. Unlike, say, RxSwift or ReactiveCocoa. You don’t need to add yet another library to adopt VIPER.
Frankly I’m not a big fan of 3rd party libraries or frameworks that are merely “glue code” to support some architecture or paradigm. Adding dependencies often means taking in additional risk in areas such as potential vulnerabilities or compatibility issues with future SDK or OS versions. I feel that these risks are only worth taking if the particular library adds real functionalities.
VIPER is not (always) Clean Architecture
Contrary to many, I feel that VIPER is not necessarily Clean Architecture. Clean Architecture is about distilling policies from details and to ensure that source code dependency graph points inwards from details into policies. VIPER prescribes how to structure a GUI application into classes that are grouped in a certain way, in which those groups are elements of the VIPER acronym.
However you can create a project having VIPER classes that doesn’t comply with Clean Architecture. One example is that when an interactor class having knowledge of networking code — that may be VIPER but definitely not Clean Architecture since interactor classes are purely computational whereas networking code is a detail.
Likewise code complying to Clean Architecture may not always be VIPER. Applications without any user interface would not have a need for views nor presenters, but still can benefit from applying the principles of Clean Architecture.
Overheads? Not really
Some say applying VIPER or clean architecture means a lot of overheads. I beg to differ. Yes there are overheads, but it’s not that much in the grand scheme of things when you have a sizeable project.
The development overheads of adopting VIPER is just the discipline to organize code into their respective VIPER class archetypes. Taking logic out from the view controller and neatly place it in an interactor, presenter, or router.
Whereas the overheads for Clean Architecture are:
- Take care of the source code dependencies between different layers and ensure these are uni-directional and points inwards.
- Separate interface from implementations – program to a protocol whenever possible.
You don’t need code generators either to implement VIPER with Clean Architecture. In fact, if you need a code generator for this, you’re doing it wrong.
Implementing Clean Architecture VIPER in iOS Apps
You need to organize your app’s source code into four top-level folders representing each layer of the Clean Architecture onion.
Take note of the order of the above layers. Make sure code in one layer only depends from its own layer or the layer that come before it. For example, files in the
BusinessLogic layer must not mention any names defined by files in the
Optionally define a module for each layer. Enclosing them in modules would make dependencies more explicit, which should help in ensuring the directionality of these dependencies. Furthermore modules help preventing implementation details from leaking out by employing internal-level access.
Underneath these top-level folders you can subdivide them into further sub-folders if necessary. Similarly you can subdivide each layer into more than one module should your project is large enough to benefit from this.
DomainEntities layer mostly houses protocols defining the nouns of the application’s problem domain. It can also contain common data types for use in subsequent layers. However code in this layer must not perform I/O, hence won’t contain any code for handling files, making network requests, nor presenting a user interface. Ideally code in this layer only depends on the programming language and core libraries. For example, Swift code in this layer can only import
Foundation, won’t make any assumption whether it is run in iOS or Linux, and won’t have references to networking nor file classes.
In VIPER, the entity classes are usually declared as protocols in these layer. If you’re using Core Data or some other persistence library applying the active record pattern, the
DomainEntities layer is not the place to put your database objects. However if your entities are pure structures and not have any method for I/O, then they are welcome here.
BusinessLogic code contains your app’s special algorithms that sets it apart from other apps — it’s business logic. Like
DomainEntities that came before it, code in
BusinessLogic also doesn’t do I/O and should only depend on the programming language and core libraries. However code in
BusinessLogic functions by taking in some
DomainEntity objects and produce some other
Interactor code would make the bulk of the
BusinessLogic layer. The layer would also house protocol definitions needed by these interactors. Each interactor would typically define an interactor output protocol that would receive signals from the interactor. There would also be data manager protocols to load and save data which may be shared with more than one interactors.
When code in
BusinessLogic need to perform I/O, it would declare a protocol having the methods that reflects what needs to be done. In turn, code in outer layers would implement the protocol in which the two would be paired at runtime. This is the delegation pattern at work.
ApplicationLogic layer would have the bulk of the code related to the application. It houses:
- Routers, which are wireframe classes and storyboards
- View controller subclasses
- View subclases
When your application uses Core Data, the
NSManagedObject subclasses would probably be in this layer as well.
ExternalInterfaces layer contain the parts that are most fragile with respect to your app. Typically these are the ones that you would need change often or maybe replace in the near future. These are the functionalities that you should factor out to this layer or even additional outer layers:
- Network code – such as those making URL requests and JSON parsing.
- 3rd party API – Firebase and Parse comes to mind.
- Startup code – application delegate and scene delegate.
Having applied these, your source code dependency graph would probably look like the following package diagram.
Notice that dependency arrows go one way from an outer layer into an inner layer. The Domain Entities is at the core. Then comes Business Logic that processes data objects defined by Domain Entities hence the dependency. Application Logic depends on both Business Logic and Domain Entities. External Interfaces mostly depends on Application Logic, probably with a few references to data objects defined by Domain Entities. Note that adding dependency from External Interfaces into Business Logic would not violate Clean Architecture since it doesn’t change the dependencies’ direction.
Up to new we’ve only touched the compile time dependencies. In the next section you will see how these objects interact at runtime.
The following object diagram shows how VIPER objects connect with each other when an iOS application is running. You can see objects from the application’s classes as well as the ones coming from system frameworks.
A typical screenful would have at least a trio instances of
ViewController would own its instance of the top-level
View as well as its
Presenter. In turn, the classes for these are typically built together – a
Presenter class is typically built only for a particular
UIViewController subclass and won’t be much useful to be paired with other view controllers.
However since an
Interactor class maps to a use case, it would likely be reusable across several view controller classes. Moreover a coherent group of view controller objects may share a single interactor instance should it makes sense, such as the interactor for wizard-type user flows (also known as assistant UI).
In some cases, it may make sense for the interactor to maintain state. In wizard-type user flows, this state could be the object being built/populated by the wizard (following the builder pattern). In these cases, the interactor would support UI State Preservation / Restoration by applying the memento pattern.
Remember to always decouple interactor classes from the user interface. The same interactor class should work unmodified regardless whether the UI is on iOS, macOS, or even a web application. Likewise when you need to add scripting support to your application, code which talks to the scripting engine would make use of the same interactor classes that the view controllers use (albeit may not be the same instance). The same goes when adding voice command support through SiriKit. Scripting and voice are just different user interfaces that talks to the same set of business logic code.
Presenter configures the view (which includes the view controller) with data. This would include formatting data as appropriate as well as parsing it. Hence locale-specific information would be best held in the presenter. Usually view controllers call methods in its
Presenter object in response to action invocations by the views under its purview. These would have a higher-level semantic and be more meaningful for the business domain. Say for example, button press would call an
@IBAction method in the view controller. In turn the view controller would call a method in its presenter to begin processing of a certain activity. In turn the
Presenter may call methods in an
Interactor to perform business processing.
Wireframe class routes from one top-level view controller into another. Storyboards also perform a similar role as
Wireframe classes. Thus collectively these are called routers in
VIPER. Presenters call methods in the wireframe typically to present the details of a data item (a drill down navigation). For a storyboard based setup, presenters call
performSegue on the storyboard instead of the wireframe object.
Being a router, Wireframe classes would be responsible for creating subsequent view controllers, configuring its presenter object, and getting it on screen. This is a similar role of what Storyboards do. Likewise should the app’s UI be addressable by a URL-like scheme, then the wireframe would be the place to do it. The wireframe can have a method receiving a URL instance, parse it, and then present the appropriate view controller as a response.
Now we’ll look at how these objects interact. The following sequence diagram shows an example how a detail view gets presented in a master-detail iOS application. In this example, a standard
UITableView instance shows a list of items having its view controller as its delegate. In turn the view controller is a custom subclass of
UIViewController and part of a VIPER setup.
When the user taps on a cell in the
UITableView instance, the latter sends a
tableView(_: didSelectRowAt:) message to its delegate, which in this case is the owning
ViewController. This message contains the location of the selected cell, expressed in an index path. In turn this view controller translates this index path into a simple array index, since it knows there is only one section in the table view. It then sends
showDetailOfItemAt(..) message to its
Presenter. The presenter then looks up the data item at the appropriate index then creates a
detailPresenter object configured with that particular data item. Then the presenter calls the
Wireframe to navigate into a new view controller, passing the detail presenter along with the
ViewController object serving as the starting point of the navigation event. Finally the
Wireframe instantiates a new view controller appropriate for the navigation event, configure it with
detailPresenter and then calls
present(...) so that the originating view controller would show the detail view controller.
Source Code Dependencies
Let’s revisit how things look in code. The following class diagram shows how VIPER classes interlink with each other. You can see that the dependency graphs are still one-direction, from outer-layer classes into inner-layer classes. Any outer-layer functionality required by an inner-layer class manifests as an inner-layer interface that gets implemented by an outer-layer class.
Have a look at the color code in the classes’ borders. The bulk of the code would be in Application Logic – and for an iOS application this would be typical. A few classes are in External Interfaces and code here are the ones expected to be most fragile (subject to change frequently in response of its technical environment). However the Core Data entities, being a built-in iOS framework, could reside in the Application Logic layer since their fragility would be about the same as any other platform frameworks.
Notice that the
DataManager depends to data objects solely via the
Entity interface. In turn the protocol gets implemented by either Core Data objects or objects defined by network code, likely as a result of JSON parsing. The
Entity interface abstracts the location where the data came from – which is a detail – such that local-origin data items are handled the same way as remote-origin data items.
A presenter implementation (shown as
PresenterImp in the diagram), implements both its
Presenter interface and the
InteractorOutput interface of the interactor object that it uses. The
InteractorOutput interface is used by the
Interactor class when it needs to return an asynchronous result. That said, the interactor could also use completion handlers instead of a dedicated output protocol.
Similarly a presenter also defines a
PresenterOutput protocol to call back to an owning object. In turn this protocol is typically implemented by a view controller owning the presenter.
The following package diagram details the same one shown earlier. It has the same dependencies but with examples on which code goes into which package. Note that these packages are not necessarily Swift packages, but merely a coherent collection of source code.
To summarize, these would be the typical contents for each layer:
- Domain Entities – protocols for various Entity objects
- Business Logic – interactor protocols, their corresponding interactor output protocols, and the interactor implementation.
- Application Logic – view controllers, views, presenters, wireframes, and any “plug-in” protocols to be implemented by subsequent layers
- External Interfaces – network API client class (including JSON parsers and their respective data objects, if any), and potentially top-level entry points such as application delegate or scene delegate. However start-up code such as application delegate and scene delegate can be in an even more “outer” layer by themselves.
Here is a snippet of the UML notations used in this article to jog your memory.
- A class is drawn as a rectangle.
- A protocol is drawn like a class with
- An object is drawn like a class with an underline in its name.
- The object name (if present) is written to the left of the colon (
:) where the class name (type) is at its right side.
- Anonymous objects are written just using the colon and type name.
- Subclassing uses a triangular arrow with a solid line.
- Protocol implementation also uses a triangular arrow but with a dotted line.
- Dependencies are drawn with stick arrows and dotted line.
- Object composition means that the lifetime of the owning object matched to the one being owned.
- Drawn using a filled diamond in the “owner” side.
- In this example, the
- When the
Cargets de-initialized, so will the
- Object aggregation means that the lifetime of the container object may be shorter than the objects it refers to
- Drawn using a hollow diamond in the “container” side.
- In this example, when
Pondgets de-initialized, the
Ducks are free to go (maybe find another pond).
- Object association doesn’t specify any lifetime constraints.
- These are are drawn using stick arrows.
- I use this “association” arrow to show a weak reference relationship.
- The plus (
+) sign behind a name shows it’s accessible publicly.
- The tilde (
~) sign behind a name shows package-only access (i.e. module-level access in Swift).
VIPER would work well with data-driven applications. Notably “drill-down” type of applications with many kinds of top-level view controllers. Examples would include shopping apps, museum guides, or even personal finance applications.
In contrast, apps without navigation probably won’t benefit much from adopting VIPER architecture. Apps like the built-in iOS’ Reminders App should probably stick to plain MVC or maybe MVVM architectures. Without navigation, the “router” component of VIPER may not be necessary. Just like multi-page storyboards aren’t necessary in these kind of applications. Without a “router” component, VIPER devolves into something akin to MVVM architecture where the “presenter” of VIPER becomes analogous to the “view-model” of MVVM.
Very simple apps won’t benefit from VIPER. Specifically apps with only one or two use cases. Remember that an interactor maps to a use case. When the app only has one use-case for its entire foreseen lifetime, interactor methods would probably be too simple thus refactoring it out may add unnecessary complexity. Again, stick to plain MVC or MVVM with the “model” or “view-model” definitions be part of the core “domain” layer (and not have separate “business logic” layer to not add unnecessary complexity).
Likewise VIPER won’t be a good fit for technical demos or sample code. There’s a reason why many code samples in Apple’s documentation tend to shove code into the view controller subclass or application delegate – those projects are meant to demonstrate how to use an API, not how to create a maintainable application. The authors of such projects need to minimize code that are not relevant in demonstrating the API.
If unit testing is not a priority for the project, then likely VIPER won’t be beneficial. One major benefit of VIPER is to allow testing of presentation logic without the actual presentation. You don’t need to resort to UI testing for a large part of the UI code, since most of the UI logic lies in the presenter and wireframe classes. You can hook a presenter with a surrogate view controller subclass specifically designed for unit testing and then assert whether the presenter call the correct methods in response to stimuli of the unit test — à’la brain in a vat experiment.
Clean Architecture should be applicable to most apps that would be maintained. That is, software that are intended to last for a long time without going through a major re-write.
But single-shot “release, bugfix, abandon” type applications probably won’t benefit from Clean Architecture. In a “single-shot” project, overheads invested in a “longetivity-oriented” architecture won’t pay off. Examples include many agencies’ “fixed price” contracts — any overhead invested by the agency would only increase costs and won’t be recouped during maintenance since there isn’t any maintenance period. Similarly many games probably won’t benefit since a lot of studios only release a particular game once, maybe one or two bugfix follow-ups afterwards, and then move on to the next project. Hackathon projects also shouldn’t try to apply Clean Architecture 😉.
Now you’ve understood what is Clean Architecture, VIPER, and how to apply those techniques into an iOS app. You should apply the knowledge as soon as possible so that this knowledge sticks.
Have a look at your own on-going projects or products and see if you can start implementing Clean Architecture on it. Identify the domain entities and business logic of your app and try to isolate it. Refactor such that they no longer depend on the environment that the app runs on. Thus when you’re ready to support another user interface style — or another platform altogether — the code base would be ready to support you.
Don’t rush to a full VIPER in an on-going development project. First VIPER may not always be appropriate for your project and refactoring code to adopt a new architecture mid-way would probably do more harm than good. But on your next project, have a thought if VIPER is appropriate and organize the code accordingly.
If you have time and some spare change, do read Uncle Bob’s book: Clean Architecture: A Craftsman’s Guide to Software Structure and Design, ISBN 978-0-13-449416-6 (available in O’Reilly’s subscription library and probably some others). If you’re short of time, I recommend to read through Chapter 29 of the book, Clean Embedded Architecture. This chapter distills the spirit of the book using embedded systems as a case study. Then read the rest of the book when you have more time or need more clarifications.
Looking for sample project? Have a look at this next article.
Thank you – should you have any feedback do let me know in the comments.