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:
libDomainEntities
library hosts the Domain Entity layer consisting mostly interfaces and data structures.libBusinessLogic
library contains the Business Logic which are interactor classes their respective public protocols.libApplicationLogic
is the Application Logic layer which is the bulk of the project, containing views, view controllers, supporting code, as well as the wireframe class.libExternalInterfaces
contains the concrete implementation of The Movie Database client class and supporting code which parses JSON coming from the web API.- Target
ViperMovieDemo
is 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, themain
function 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.
The SceneDelegate
owns the window
and 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 UIViewController
.
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.
Domain Entities
Since this is a movie browser app, the domain entities layer are mostly about movies.
- Protocol
MovieIdentifier
is an abstract type for identifying movies. In turn, this can be implemented as a UUID, URL, or other data as its underlying value. - Protocol
Movie
is the base for all data related to movies. - Protocol
MovieSummary
is “compact” movie data meant to display in overview lists. - Protocol
MovieDetail
represents 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
MovieFilterAttribute
andMovieSortAttribute
are for.
Business Logic
There are two interactor classes supporting the two use cases of the app:
MovieListInteractor
MovieDetailInteractor
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.
In turn, MovieDetailInteractor
produces 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.
Lastly 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.
Wireframe
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.
1
2
3
4
5
|
public protocol MovieBrowserWireframe : class { var rootViewController: UIViewController { get } func present(movieDetail: MovieDetailPresenter , from: UIViewController ) func present(error: Error , from sourceVC: UIViewController , retryHandler: (() -> Void )? ) } |
Class 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.
Movie List
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.
Class MovieListViewControllerImp
implements 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.
Movie Detail
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.
In turn 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.
Data Store
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.
Class 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.
Swift struct
s MovieDatabaseMovieSummary
and MovieDatabaseMovieDetail
are concrete implementations of the corresponding entities defined in domain entities: MovieSummary
and 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.
In Summary
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.
Next Steps
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
UIViewController
. - 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.