You are not paid to code; you are paid to create solutions.
The topic of iOS app architecture has evolved a long way from MVC. Unfortunately, the conversation becomes a frameworks and patterns war. The reality is: Rx is a framework; MVVM is a presentation pattern; and so on. Frameworks and patterns always come and go, but architectures are timeless. In this post, we will examine the Clean Architecture for building scalable apps in iOS.
What About Bob?
The Clean Architecture was coined by Uncle Bob almost 20 years ago. It is independent of platforms, frameworks, databases, or UI. It is testable. It’s a way of life (ok, software life). Sound good?
If you have an hour to spare for learning, I highly suggest watching this lecture called The Principles of Clean Architecture by Uncle Bob Martin (skip to 10:00 if you don’t want the biology lesson, although still interesting):
A Thing About Screaming Architecture
Regarding project structure, let’s first get the feature vs. type debate out the way. The application should scream what it does without any IDE or explanation at all. Anyone should be able to open up the file system and know exactly what the application does. Take a look at the two apps below:

In the first app, all it tells me is that it’s an MVC project and has something to do with products and users. Why do I have to know MVC to figure out what it does? I have to compile and run it in my mind to figure out what it does. What’s worse, I have to expand all the folders to start investigating. In a complex app, imagine hundreds of controllers or views polluting your eyes. Maintaining and debugging are nightmare too, jumping back and forth between folders is like a game of ping pong.
Now take a look at the second app. Right off the bat, I can tell it lists products, displays it, and shows a user’s profile. I don’t have to expand the folders to know what it is. I don’t have to know what framework or pattern it’s using. I don’t even have to know what language it’s written in!
Another way to look at it is would you rather organize drawers by just throwing everything into three drawers, or is it better to have several small well-defined and well-labeled drawers?
Ok let’s move on…
The Anatomy of the Clean Architecture
There are actually many variations and flavours of the Clean Architecture. You’ve probably heard of VIPER or Clean Swift. When you jump into other platforms like Android or .NET, there even exists more flavours. The one I will show you in this post was heavily inspired by Clean Swift. I put my own spin to it after battle-testing it and maintaining it across several teams, platforms, and environments.
To get started, we have to get some terminology out the way. Below are a list of components in the architecture:
- View: The interface where user interactions occur, such as the
Storyboard
orXIB
. - Controller: The layer which binds the view to code, this being the
UIViewController
in our case. - Interactor: The business logic layer where the controller must send requests through.
- Presenter: The layer that formats the response from the Interactor and sends it back to the controller.
- Router: The layer that transports the user to another use case scene, usually an event that occurs in the controller.
The core pieces of the architecture is the Controller, Interactor, and Presenter. The important thing to notice is that it has a uni-directional flow! This tremendously reduces complexity and makes it easy to manage. Since a picture is worth a thousand words, I have summed up the entire architecture in a single diagram:

Follow the flow below to understand how the architecture works:
- User interacts with View
- Controller event fires, builds a request model, and calls the Interactor
- The Interactor calls the underlying core library with all your workers to access the data (could even subscribe to an observable here if needed)
- The Interactor receives data from the worker and wraps it in a response model to send to the Presenter
- The Presenter calls app-level helpers if needed to format or localize the response and builds a view model to send to the Controller
- The Controller receives the view model in a function and binds it to the View
- Repeat the cycle…
Also notice the architecture is cross-platform. It doesn’t care that the view is served by an iOS app, Android app, web browser, or even a REST API endpoint. Once it gets past the view, everything should be identical across platforms. In fact, an iOS and Android app can be eerily similar with just the Storyboard
vs Layout
being the difference. Even the iOS UIViewController
and Android Activity
code would almost be identical except for the binding logic. This makes collaboration and bug fixing tight between teams!
The Details

It’s time to see how this plays out in code. We can do this by first examining the protocols that is the blueprint for a use case; this is important so the boundaries are defined and components are decoupled:
protocol ListProductsDisplayable: class { // View Controller
func displayFetchedProducts(with viewModel: ListProductsModels.ViewModel)
func display(error: AppModels.Error)
}
protocol ListProductsBusinessLogic { // Interactor
func fetchProducts(with request: ListProductsModels.FetchRequest)
}
protocol ListProductsPresentable { // Presenter
func presentFetchedProducts(for response: ListProductsModels.Response)
func presentFetchedProducts(error: DataError)
}
protocol ListProductsRoutable: AppRoutable { // Router
func showProduct(for id: Int)
}
Here’s what happens when the controller loads via viewDidLoad
: interactor.fetchProducts > presenter.presentFetchedProducts > controller.displayFetchedProducts. Do you see it yet? The cycle is there. Let’s take another look…
Controller:
class ListProductsViewController: UIViewController {
private lazy var interactor: ListProductsBusinessLogic = ListProductsInteractor(
presenter: ListProductsPresenter(viewController: self),
productsWorker: ProductsWorker(store: ProductsMemoryStore())
)
private lazy var router: ListProductsRoutable = ListProductsRouter(
viewController: self
)
private var viewModel: ListProductsModels.ViewModel?
override func viewDidLoad() {
super.viewDidLoad()
interactor.fetchProducts(
with: ListProductsModels.FetchRequest()
)
}
}
extension ListProductsViewController: ListProductsDisplayable {
func displayFetchedProducts(with viewModel: ListProductsModels.ViewModel) {
self.viewModel = viewModel
tableView.reloadData()
}
func display(error: AppModels.Error) {
let alertController = UIAlertController(
title: error.title,
message: error.message,
preferredStyle: .alert
)
alertController.addAction(
UIAlertAction(title: "OK", style: .default, handler: nil)
)
present(alertController, animated: true, completion: nil)
}
}
extension ListProductsViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let model = viewModel?.products[indexPath.row] else { return }
router.showProduct(for: model.id)
}
}
The controller creates the instances to the interactor, injects the presenter, and also creates the router. When loaded, it creates a request model and calls the interactor:
struct ListProductsInteractor {
private let presenter: ListProductsPresentable
private let productsWorker: ProductsWorkerType
init(presenter: ListProductsPresentable, productsWorker: ProductsWorkerType) {
self.presenter = presenter
self.productsWorker = productsWorker
}
}
extension ListProductsInteractor: ListProductsBusinessLogic {
func fetchProducts(with request: ListProductsModels.FetchRequest) {
productsWorker.fetch {
guard let value = $0.value, $0.isSuccess else {
return self.presenter.presentFetchedProducts(error: $0.error ?? .unknownReason(nil))
}
self.presenter.presentFetchedProducts(
for: ListProductsModels.Response(products: value)
)
}
}
}
The fetchProducts
function in the interactor calls the injected productsWorker
(DI was left out for clarity, see my other post Swifty Dependency Injection for that topic). Underneath, it will call the appropriate storage, whether it be Core Data, Realm, or even a file system… the architecture doesn’t care and could be swapped out without affecting the cycle! When the data returns from the data storage asynchronously, it will wrap it in a response model and send it to the presenter:
struct ListProductsPresenter: ListProductsPresentable {
private weak var viewController: ListProductsDisplayable?
private let currencyFormatter: NumberFormatter
init(viewController: ListProductsDisplayable?) {
self.viewController = viewController
self.currencyFormatter = NumberFormatter()
self.currencyFormatter.numberStyle = .currency
}
}
extension ListProductsPresenter {
func presentFetchedProducts(for response: ListProductsModels.Response) {
let viewModel = ListProductsModels.ViewModel(
products: response.products.map {
ListProductsModels.ProductViewModel(
id: $0.id,
name: $0.name,
content: $0.content,
price: currencyFormatter.string(from: NSNumber(value: Float($0.priceCents) / 100)) ?? "\($0.priceCents / 100)"
)
}
)
viewController?.displayFetchedProducts(with: viewModel)
}
func presentFetchedProducts(error: DataError) {
// Handle and parse error
let viewModel = AppModels.Error(
title: NSLocalizedString("products.error.title", "Title for product error"),
message: String(format: NSLocalizedString("products.error.message", "Message for product error"), error)
)
viewController?.display(error: viewModel)
}
}
The presenter will convert the response into a view model, applying any formatting and localization, and call the controller function to display it. The view model properties are almost always strings because the view will just display. If the interactor gave the presenter an error, it will even create a view model out of the error and let the controller display it.
The models are encapsulated in an enum and are only relevant to its own use case. You do not cross boundaries without being wrapped in one of the models:
enum ListProductsModels {
struct FetchRequest {
}
struct SearchRequest {
let text: String
}
struct Response {
let products: [ProductType]
}
struct ViewModel {
let products: [ProductViewModel]
}
struct ProductViewModel {
let id: Int
let name: String
let content: String
let price: String
}
}
Finally the router in case the controller has to send the user to another use case:
struct ListProductsRouter {
weak var viewController: UIViewController?
init(viewController: UIViewController?) {
self.viewController = viewController
}
}
extension ListProductsRouter: ListProductsRoutable {
func showProduct(for id: Int) {
let storyboard = UIStoryboard(name: "ShowProduct", bundle: nil)
guard let controller = storyboard.instantiateInitialViewController()) as? ShowProductViewController
else { return assertionFailure("Invalid controller for storyboard \(storyboard).") }
controller.productID = id
viewController?.present(controller, animated: true)
}
}
This way, the the controller is decoupled from the routing decisions and can be used from the controller as: router.showProduct(for: productID)
.
Conclusion
The Clean Architecture is flexible, scalable, and maintainable. Although it is more verbose than other architecture designs, it is necessary so components are not tightly coupled. That means everything must reference each other by its protocol, not its concrete types. And when crossing boundaries, data must be wrapped in request / response models, or again components will be coupled. In a future post, I will discuss how to unit test the whole stack.
See a working example of this Clean Architecture to try it out for yourself.
HAPPY CODING!!
Great article, great demo code.
Great article and great demo. My approach to separate out localizations was to write a library that separates it completely.
Great article. Thanks for sharing 🙂
I think this is not fulfilling SOLID principle. If I will quote your sentence: “The controller creates the instances to the interactor, injects the presenter, and also creates the router. When loaded, it creates a request model and calls the interactor:” The controller has so many responsibilities. (Counted 5)
Currently, I’m using Coordinators from Khanlou (your Router). Where Router would create instances of Controller, Interactor, and Presenter, ie: Will prepare objects for every use case from “above”.
In your usage, UIViewController knows about every other component and cannot be reused or replaced for example for A/B testing of view.
One more thing is that View is created in Router. But should be in Controller, Controller knows how to show the data, so it should also know about on which screen to show them. If you will decide not to use Storyboards, but create the view programmatically, will you also put all your code for the view into the Router? It makes much more sense to have it in UIViewController.
Hi Jakub, thanks for the excellent feedback and sharing your thoughts.
I agree the architectural components can be instantiated in a dependency container outside of the view controller, but I don’t think this counts as responsibilities or violates SOLID as it is. The logic still resides in their respective components, and since the view controller references all the components by their protocol instead of their concrete types, they can still be swapped out before the view controller life cycle begins. I still agree it would be more proper to inject the components from elsewhere, but consider the trade off of the use case not being as encapsulated or portable anymore (convenience?).
Regarding the router, it’s true I’ve taken an opinionated approach of instantiating view controllers through storyboards. I can see the benefits of the inverse where the controller instantiates and sets up its own view programmatically. I just felt I wanted to keep the view controller dumb as possible and simply be a layer for listening for interactions and binding view data. In my case, the storyboard/IB creates all the “logic” for how to display itself, the router just points to the storyboard and passes along any data needed. Interface Builder has come a long way and allows us to rid code for creating views; I feel this is what IB was designed for.
Great work. When will the course starts?.
Can’t wait.
Regards
Hey, interesting approach. I have two questions.
First, why is the interactor private (not testable)?
Second, why does the view controller know about a/the worker?
“`
private lazy var interactor: ListProductsBusinessLogic = ListProductsInteractor(
presenter: ListProductsPresenter(viewController: self),
productsWorker: ProductsWorker(store: ProductsMemoryStore())
)
“`
In my opinion you can give the interactor the data store as argument, not the worker.
Hi Miguel, thanks for the feedback.
I plan to write the unit test portion to illustrate this, but essentially I’d like to keep the architectural components private so the outside world can’t mess around with it. Instead, I’d rather create an extension for the unit tests:
extension ListProductsViewController {
func testSwap(for interactor: ListProductsBusinessLogic) {
self.interactor = interactor
}
}
I’m wondering if I can surround this with `#if unitTest` preprocessor macros so it’s only exposed to the unit test. That would be sweet.
You’re right about the view controller knowing about the workers in the example. I briefly mentioned I kept dependency injection out so I wouldn’t cloud the central idea of the post, but in a real app it would look more like this:
private lazy var interactor: ListProductsBusinessLogic = ListProductsInteractor(
presenter: ListProductsPresenter(viewController: self),
productsWorker: dependencies.resolveWorker()
)
That way, the view controller wouldn’t know anything at all about the worker and the dependencies can be swapped out behind the scenes. Here’s the post that talks about this: http://basememara.com/swift-protocol-oriented-dependency-injection/
Interesting approach. Could you elaborate a bit about the differences between yours and the one used on Clean Swift, (the one you said this one is based on)?, thanks in advance!
I’m familiar with the Clean Swift, (assuming is this one https://clean-swift.com/), so that’s the reason of my question.
Hi Diego, thx for the feedback. The central idea of a uni-directional flow is from Clean Swift, which is a game changer from other Clean Architecture implementations. The differences from there is I completely reimagined the router, dependency injection, and the use of value over reference types.