When it comes to iOS software architectures, there is no shortage of acronyms:
and plenty of others I didn’t list out. Although each has their own unique applications and benefits, they all seem to follow many of the same principles described by Robert C. Martin’s book, Clean Architecture.
Clean Architecture was introduced in 2012 as a way to develop software so that the business layer would be separate from any external agencies, allowing it to be more maintainable, testable, and independent.
This is why I believe that studying Clean Architecture, and taking the time to understand the fundamental principles of it, will help us gain a better understanding of the architectures above, and many others.
Most importantly, it will provide us with the knowledge and tools needed to define our own architectures. So with that in mind, lets first understand why us iOS developers should care.
You can find the Swift Package used for this post's code examples at the following GitHub repo: https://github.com/obvios/employee-directory-core
Why should iOS developers care about Clean Architecture?
iOS developers should care about Clean Architecture for several reasons, as it offers significant benefits that align well with the complexities and demands of modern iOS app development.
Here are some of the key reasons why Clean Architecture is relevant and beneficial for iOS developers:
1. Maintainability: iOS applications can become quite complex over time, especially as they grow in features and user base. Clean Architecture helps in organizing the codebase in a way that it remains maintainable, readable, and manageable, even as the project scales.
2. Testability: Writing testable code is crucial for ensuring the reliability and stability of iOS applications. Clean Architecture, with its emphasis on separating business logic from UI and external dependencies, makes it easier to write unit tests for the core functionality of the app without relying on external factors like network calls or database access.
3. Flexibility and Adaptability: iOS development often involves rapid changes, whether due to new iOS versions, changing user interfaces, or evolving business requirements. Clean Architecture allows developers to adapt to these changes with minimal impact on the core business logic of the app. For example, transitioning from one data storage solution to another, or modifying the UI, can be done with less risk of breaking the business logic.
4. Improved Collaboration and Code Reviews: A well-structured codebase following Clean Architecture principles is easier for teams to understand and collaborate on. It also simplifies code reviews, as the separation of concerns makes it clearer what each part of the code is responsible for.
5. Support for Multiple Platforms: As Apple's ecosystem includes not just iOS, but also macOS, watchOS, and tvOS, having a clean, separated architecture makes it easier to share code across different platforms, reducing development time and effort. For example, the core business logic could be maintained as a separate Swift Package (more on that later).
6. Alignment with Swift: Clean Architecture aligns well with Swift’s emphasis on modular, reusable, and testable code. It leverages the strengths of Swift’s strong typing and protocol-oriented programming.
In summary, Clean Architecture offers a structured and pragmatic approach to building iOS applications. It helps in tackling complexity, maintaining code quality, and ensuring that the app can evolve and adapt over time with less friction, which is essential in the fast-paced world of iOS app development.
Clean Architecture in Swift
At the core of Clean Architecture implementations are the Use Cases. They represent the business logic, also known as the application-specific rules. They define what the application needs to do, independent of how it’s done (UI, databases, etc.).
According to Robert Martin, the entities are the enterprise wide rules, and they can be simple data structures or an object with methods. The use cases are the application specific rules. They interact with the entities, directing them to apply their enterprise wide rules if applicable.
To help us better understand this, let’s take a look at an example. Suppose we are developing an employee directory app, let’s call the app "EmployeeDirectory".
The requirements for our app are as follows:
It can list out all of the employees in a company, with the option to search by name (either first or last)
Users can view employee details
Users can update employee details (only email is validated, for simplicity)
Notice that these requirements are essentially defining what the application does. Further, they include some business rules, such as allowing users to search by either first or last name, and validating emails when editing employee data.
Naturally, these requirements are what we will use to build out our use cases. Take for example the first one, listing out employees in a company.
To display a list of employees, we will need to create a FetchEmployeeListUseCase. We don’t yet know where we are retrieving this information from, either a local database or the network. However, it doesn’t matter, all we know is that there is a data source where we can get the data from.
We will create a protocol for this data source and call it EmployeesRepository. Notice how we don’t even know where the data will be coming from, yet this doesn’t slow us down.
Our EmployeesRepository protocol is defined as follows:
By adding the interface layer between our use cases (the business layer) and the rest of the app, we can develop our business logic irrespective of the rest of the application and delay having to make decisions, like whether to fetch data locally or from the network, until absolutely necessary. This is one way Clean Architecture can help speed up development.
In this example, Employee is our entity. It is the model representing an employee. We only have an iOS app, however if our business had a web app or android app as well, they would share the same `Employee` entity.
With the above protocol defined, here is our FetchEmployeeListUseCase:
As we can see, we were able to quickly implement one of our application's use cases without needing to worry about details such as where the employee data comes from. Another important point to call out is that we don't have any external dependencies.
Our use case only depends on a protocol, namely EmployeesRepository. This is an important characteristic of Clean Architecture: dependencies run inward towards the business logic.
This just means that code belonging in one layer, has dependencies on code in the inner layer beneath it, and not the other way around. For example, our use cases do not depend on the code in the outer layers, like the presenters, database, or network API, since they only depend on a protocol. This is an example of an important SOlLID principle called Dependency Inversion.
However, the outer layers do depend on the Use Cases in the inner layer. If the use cases change or need a new capability from the EmployeesRepository protocol, the outer layers will be affected. They will need to be updated to be compatible with the changes.
This is how we protect our business logic from reacting to changes in external frameworks and details. We could use UIKit or SwiftUI, we could switch from using CoreData to Realm for persistence, it doesn't matter to our business logic. As a result, our app's core functionality is made more maintainable and independent.
Testing our Use Cases
Testing our use cases is simpler than ever now that we've architected our business logic to only depend on protocols. We can mock our EmployeeRepository to return what ever we need it to for our specific test scenarios.
For example, in order to test different scenarios for the FetchEmployeeListUseCase, I defined a MockEmployeesRepository which could be configured to return what ever we need it to for our test scenarios. We could configure both the list of employees that gets returned as well as the error, if any.
Here is the MockEmployeesRepository I defined in the FetchEmployeesUseCaseTests file:
With the above mock employees repository in place, it becomes trivial to test different scenarios for our use case. For instance, we could test the search functionality like below:
First, we create our list of employees and configure our mockRepository to return it. We then inject the mockRepository into our FetchEmployeeListUseCase. Lastly we execute our use case with the desired search term and run our assertions.
By having our use cases depend on protocols, we are able to configure it's dependencies to fit our testing needs. This provides us with the flexibility needed for extensively testing our business logic and rules.
Independence of our use cases
To emphasize how Clean Architecture allows our business layer code to remain independent, I developed the use cases for EmployeeDirectory as a Swift Package named EmployeeDirectoryCore, you can view the repository here: https://github.com/obvios/employee-directory-core.
Notice that the essence of what the app does is contained within this package. It contains the business entities and rules (use cases). Our package is not concerned with the frameworks we use to develop our app either.
It also has its own unit test suite which is maintained along side with it, allowing us to test it without needing to run it on a device. This provides greater guarantees about the robustness of our application's business logic and rules.
In summary, our application's core business logic can be developed, maintained, and tested separately from any other external agency which depends on it. It does not matter if we have a code base for an iOS app, a macOS application, or an apple watch app - there is only one place for our core business logic.
Today we gained a basic understanding of Clean Architecture and it's principles, then we looked at a practical example of how they can be applied to create an independent, maintainable, core business layer. In our next blog post we will explore how to build a fully featured iOS app using our EmployeeDirectoryCore package. So stay tuned and subscribe to our newsletter to be among the first to know once the next post is available.