A typical poorly-architected app would feature classes with too many responsibilities and properties. Working in parallel with other developers on these spaghetti sources would be difficult — being magnets for merge conflicts. Likewise, properly unit testing these mega-blobs could be a project in itself.
On the other hand, developing a fully-featured application having an elegant and simple architecture is challenging. As what the late Steve Jobs once said, “Simple can be harder than complex.” It is tempting to cobble most logic into view controller subclasses and not worry on separating concerns into their respective components.
Luckily many software design problems have been solved before. These gets documented under software architecture or design patterns. You only need to find the software architecture and design patterns fitting your current challenge and apply it to your situation. Having the right model architecture, you can focus on properly compartmentalizing the different functionality in your apps.
In a previous article, I’ve provided a guideline on how to apply the VIPER Clean Architecture into an iOS application. This article builds further by showing you an example iOS application built with using VIPER and Clean Architecture principles in mind. You can find the full source code to the project linked at the end of this article.
When you’re done reading this article and having practice what you’ll learn on the sample project, you’ll be able to architect applications to solve the problem at hand and make it flexible enough to cater for tomorrow’s challenges. You’ll be able to determine if the code you’ve written in the past followed best practices and have proper separation of concerns so that code you’ll write in the future would be more maintainable and anti-fragile.
The Case Study App
The case study is a UIKit application for discovering new popular movies. The app was written using Xcode 11.4.1 and Swift 5. It runs on iPhone, iPad, as well as macOS (Catalina) thanks to Mac Catalyst. The app’s source code is structured with Clean Architecture and VIPER in mind.
Have a look at the app’s use case diagram below. There’s one user role and two use cases:
- List Movies
- View Movie Detail.
The app shows titles of popular movies released within the pass year. Selecting a movie title would show its detail. When you scroll down and reach the end of the list, the app responds by fetching more movies to watch – giving the impression of an infinite list. This list of movies comes from The Movie Database, which provides a web API to retrieve its data.
I concur that VIPER might be overkill for this app, having only two screens where an MVVM architecture would suffice. However even with only two screens, a router component would be useful for navigating between them. Nevertheless, Clean Architecture should help improve reuse when the app gets brought over to other platforms that supports Swift.
Clean Architecture Implementation
The app consists of modules conforming to Uncle Bob’s Clean Architecture. Each layer gets mapped as a static library which defines their respective module. Having layers as static libraries means there is no run-time overhead coming from dynamic linking normally associated with Swift frameworks. Moreover, defining modules enforces explicit
import function when using code between modules and enables internal (module-level) access control.
These are the modules of the app:
libDomainEntitieslibrary hosts the Domain Entity layer consisting mostly interfaces and data structures.
libBusinessLogiclibrary contains the Business Logic which are interactor classes their respective public protocols.
libApplicationLogicis the Application Logic layer which is the bulk of the project, containing views, view controllers, supporting code, as well as the wireframe class.
libExternalInterfacescontains the concrete implementation of The Movie Database client class and supporting code which parses JSON coming from the web API.
ViperMovieDemois the main application binary. Startup-level code are linked directly here, which includes the app delegate and the scene delegate. If this is an Objective-C project, the
mainfunction would be linked directly to the app as well.
The source code is also organized that top-level folders maps to their respective layers and modules. In turn, the application logic layer is further sub-divided into class archetypes like view controllers, presenters, or wireframes. Most layers would have a source file named
Interfaces.swift containing protocols and supporting types that are publicly accessible outside of it.
Objects at Runtime
The following object diagram shows what objects are instantiated and how they inter-relate in the application when it is showing a movie detail. That is, when the user started the app and then select a movie to show its detail.
Being a Split View application, it shows a master list on the left whereas the right shows details of the selected item in the master list. This concurrent display of master data and its detail is only shown in the regular width size class, which are usually iPad and Mac. But in the compact width size class (such as the iPhone and iPod touch), the app only shows the master list at first and only discloses the detail view when an item gets selected in the master list.
SceneDelegate owns the
wireframe objects. In turn the window owns the split view as its root view controller and subsequent view controllers coming after that. View controllers own their presenter objects. Subsequently presenter objects maintains strong references to the
wireframe object and their respective interactor objects.
Since presenters strongly holds their
wireframe object, the latter shouldn’t maintain strong references to view controllers. Instead, should the wireframe needs a view controller to perform a functionality, it would take a view controller as a parameter.
Each presenter holds on to their own respective interactor instances. But interactor instances shares the same
MovieDataStore instance which in turn accesses The Movie Database’s (remote) data store. The data store is intended to be stateless with respect to its callers.
On navigation events, presenters would typically pass the wireframe with two objects:
- what to present.
- where is it coming from.
In this example app, “what to present” is either a presenter object or an error object. Whereas the “where is it coming from” is the current
The following sequence diagram shows the order of events happening when the user selects an item to show its detail. You can see how views collaborate with its controllers and how presenters and the wireframe comes into play.
Classes at Compile Time
This section shows how classes in the application relates to each other. These class diagrams show how things are at compile time. In other words, these associations can be deduced from the source code without running it.
Since this is a movie browser app, the domain entities layer are mostly about movies.
MovieIdentifieris an abstract type for identifying movies. In turn, this can be implemented as a UUID, URL, or other data as its underlying value.
Movieis the base for all data related to movies.
MovieSummaryis “compact” movie data meant to display in overview lists.
MovieDetailrepresents everything known about a movie.
- When listing movies, sometimes it would be useful to how to filter and sort the list – so that not everything in the data store gets returned. This is what enumerations
There are two interactor classes supporting the two use cases of the app:
MovieListInteractor returns paginated lists of
MovieSummary objects. Being part of policy code, the interactor defines the sort and filter parameters for querying the list. However the page size (i.e. number of movie summaries shown per batch) is configured into the interactor, since this is dependent on how the user interface is structured. The class is also responsible for creating
MovieDetailInteractor instances for interacting with a particular movie record.
MovieDetail objects containing details of a movie. Instances of this interactor are configured with a
MovieIdentifier object upon creation, hence it would only return details for that particular movie record.
The two interactor objects typically connect to the same
MovieDataStore instance. Again, this data store is expressed in the business logic code as a protocol to be implemented by an outer layer.
Those interactors are exported through protocols with the implementation classes hidden via package access. This way the business logic component won’t need to expose its implementation classes.
MovieFetchRequest defines how interactors would retrieve movie lists from the data store. This is a simple data transfer object implemented as a Swift
struct without any logic.
An implementation of
MovieBrowserWireframe sets up the initial set of view controllers then gives it to the
SceneDelegate. In turn, the wireframe is also responsible for setting up subsequent view controllers in response to navigation requests by presenters. It pretty much fulfills the role of a storyboard, albeit in code. In a sense, the
wireframe is a factory for view controllers and supporting classes.
MovieBrowserWireframeImp is the concrete implementation for the wireframe, hidden from outside of the module. Similarly, the view controllers and presenters concrete classes are also internal-access only.
Besides setting up the view controllers, the wireframe also sets up the initial interactor instance (implementing
MovieListInteractor) and hook it up with a data store. More on the data store stack later in this article.
The movie list is shown by class
MovieListViewControllerImp. It is a
UITableViewController subclass which shows the movie titles in a table view. It owns the table view, configures cells, and owns the presenter object. It also translates user actions into higher-level events handled by the presenter. Notably table view cell selection and scroll to the end of the list event. These gets translated to the presenter as show detail and load more data events respectively.
MovieListPresenterOutput in order to receive notifications from its
MovieListPresenter instance. This delegation setup enables the presenter to signal the availabilities of new data independent of user events – making cases such as silent push easier to implement without any change to the user interface.
Cells in the table view owned by
MovieListViewControllerImp implements the
MovieSummaryCell protocol. The presenter only knows this protocol and thus switching from a table view to collection view implementation of this display won’t affect the presenter at all.
In turn the presenter talks to an instance of
MovieListInteractor, which houses the business logic of fetching the movie list. The presenter configures the interactor with the current date and the device’s preferred language. In turn the interactor uses these as input for filter criteria in which to fetch the list of movie from the data store.
The primary owner of a movie detail screen is class
MovieDetailViewControllerImp. This is also a
UITableView subclass but instead of presenting individual data items as cells, it presents attributes as rows of the table view. Its corresponding presenter is
MovieDetailPresenter hence the class implements
MovieDetailPresenterOutput to respond to movie refresh event.
MovieDetailPresenter uses a
MovieDetailInteractor instance behind the scenes. The view controller has no knowledge of the interactor. Likewise the presenter has no knowledge of the view controller.
Having the presenter isolated from the view structure or even the view controller’s concrete type allows the movie detail screen to switch from a
UITableViewController implementation without any impact to the presenter nor the business logic. For example, instead of displaying movie detail attributes as a table view cell, the view controller shows them it as a collection of text fields in a stack view.
This app’s data store is mainly a network client since its persistent store is located in The Movie DB’s servers. However the location of data at rest is well isolated from the rest of the application. Therefore if the data store changes into a local/remote hybrid or even a fully local store (like how an offline encyclopaedia app is structured), most parts of the application won’t get impacted.
MovieDatabaseClient makes calls to The Movie DB’s network API via
URLSession. The class also work with collaborators which parses JSON data returned from the API.
MovieDatabaseMovieDetail are concrete implementations of the corresponding entities defined in domain entities:
MovieDetail, respectively. These structures are also responsible to parse JSON data coming from The Movie DB and storing the parsed result.
The network client and its helpers are placed in the outermost, most fragile layer. Should The Movie DB decides to change their API, these classes would be most affected. Hence these are part of the
libExternalInterfaces library, which is our implementation of the outermost layer in Clean Architecture.
The Movie DB has a “page-based” API in which data rows are fetched in multiples of 20 items. However the app needs variable-length fetch batch size, depending on how many items that the master view can display. At the moment this depends on the height of the screen or window of the master list table view.
That’s where class
MovieDataAdapter comes in. It implements
MovieDataStore protocol defined by the Business Logic layer and exposes variable-length fetching. In turn, it uses an instance of
MovieDataSource which uses page-based fetching. The adapter translates between variable-length batches coming in from interactor objects into one or more calls of fixed-length pages to
MovieDataSource – making multiple calls to the data source if the batch size is larger than the page size.
However since the
MovieDataAdapter class is part of the Application Logic layer, it doesn’t make any network I/O – no references to
URLSession or other networking classes. The role of networking is reserved for the “external interfaces” layer in this project.
Let’s revisit the layer dependencies of our implementation of Clean Architecture. The diagram shown below is essentially the same package diagram shown earlier in the article, but expanded with examples of each package’s prominent members.
You can see that Clean Architecture is really an architecture for a hierarchy of plugins. Here, outer layers “plugs in” to inner layers by implementing an interface defined by the latter. In turn the pieces gets “assembled” at run-time through dependency injection.
Get the Source
The source code is available on my Github repo, licensed as Apache 2.0 open source. Please link to this article when you expand the project or create derived works from it.
Now that you’ve see a full implementation of VIPER and Clean Architecture, it’s time that you apply the knowledge. One way to start is to “maintain” this sample application and expand its scope. That is, pretend that you’re just hired to improve this
ViperMovieDemo application and need to continue developing it.
Here are some ideas to start applying your knowledge and improve the application.
- Support State Preservation and Restoration – you’ll probably need to apply the memento pattern on the interactors and presenters and have the view controller persist their mementos as part of the preserved user interface state.
- Show movie images — such as thumbnails in the list, splash, photos of actors.
- Show movie trailer — some movie records have links to YouTube videos of their trailers.
- Have a local data store. Maybe users would be able to favorite movies or add notes about them.
- Add tvOS Target. Update it for the big screen with slightly different view controller implementations while keeping presenters and routers the same,
- Add watchOS Target. The router and view controllers would change (or added specifically for the watchOS target), however the presenters should be mostly intact apart from references to
- Add Web Application Target. Most of the Application Logic layer would probably need to be rewritten or added to support a web-based user interface. However the data store setup would likely be the same. See if you can keep the interactors unchanged by keeping a live connection via web sockets or by using a combination of asynchronous I/O (i.e. futures) and keeping interactor instances as session-scope objects.
Remember to keep in mind of the Clean Architecture Principles when adding features and extending the app’s supported platforms:
- Dependencies always point one way, inwards.
- Outer layers are plug-ins for inner layers.
- The Domain Entities and Business Logic layers are reserved for policies and would be free of I/O code and independent of external details.
- The more fragile a functionality is (i.e. more prone to change due to external factors), the further it should be removed from the core.
That’s all for now. Take care.
0 thoughts on “Clean Architecture with VIPER Sample Project for UIKit”
You must log in to post a comment.