Clean Architecture: iOS App
By employing clean architecture, you can design applications with very low coupling and independent of technical implementation details. That way, the application becomes easy to maintain and flexible to change.
Clean architecture allows us to create architectural boundaries between dependencies which allows components to be intrinsically testable.
What’s to follow is our attempt to build an iOS application using clean architecture with TDD. The app will be used to manage contacts.
We are going to structure our application in the following way:
Our application is partitioned into Presentation, Domain and Data Layers.
The Presentation layer is responsible for all consumer facing components like views and view models.
The Domain Layer holds all business Logic (use cases) and System Logic (repositories).
And the Data Layer keeps all infrastructure components, like data sources and services.
At specific points in our applications, we define dependency rules with interfaces. We unit-test every component by mocking its dependencies.
Note: We don’t unit-test our views as they should contain minimal logic to aid in rendering data graphically. It is therefore best to test them with the eye.
System Structure
The structure shows intent through filenames and folder structure
-- Contacts
│── Presentation
│ └── Contact
│ ├── Create
│ │ ├── ContactCreateViewModel.swift
│ │ └── ContactCreateView.swift
│ ├── Edit
│ │ ├── ContactEditViewModel.swift
│ │ └── ContactEditView.swift
│ └── List
│ ├── ContactListViewModel.swift
│ └── ContactListView.swift
├── Domain
│ ├── Protocols
│ │ ├── UseCases
│ │ │ └── Contact
│ │ │ ├── CreateContactUseCaseProtocol.swift
│ │ │ ├── UpdateContactUseCaseProtocol.swift
│ │ │ ├── DeleteContactUseCaseProtocol.swift
│ │ │ ├── GetContactUseCaseProtocol.swift
│ │ │ └── GetAllContactsUseCaseProtocol.swift
│ │ └── Repositories
│ │ └── ContactRepositoryProtocol.swift
│ ├── Models
│ │ └── Contact.swift
│ ├── UseCases
│ │ └── Contact
│ │ ├── CreateContact.swift
│ │ ├── UpdateContact.swift
│ │ ├── DeleteContact.swift
│ │ ├── GetAllContacts.swift
│ │ └── GetOneContact.swift
│ └── Repositories
│ └── ContactRepositoryImpl.swift
└── Data
├── Protocols
│ ├── Wrappers
│ │ └── CoreDataWrapperProtocol.swift
│ └── ContactDataSourceProtocol.swift
└── DataSources
└── CoreData
├── Entities
│ └── Contact.xcdatamodeld
├── Wrappers
│ └── CoreDataWrapper.swift
└── CoreDataContactDataSource.swift
-- ContactTests
│── Mocks
│ ├── Domain
│ │ ├── Repositories
│ │ │ └── MockContactRepository.swift
│ │ └── UseCases
│ │ └── Contact
│ │ ├── MockCreateContact.swift
│ │ ├── MockUpdateContact.swift
│ │ ├── MockDeleteContact.swift
│ │ ├── MockGetAllContacts.swift
│ │ └── MockGetOneContact.swift
│ └── Data
│ └── DataSources
│ ├── MockContactDataSource.swift
│ └── CoreData
│ └── Wrappers
│ └── MockCoreDataWrapper.swift
│── Presentation
│ └── Contact
│ ├── Create
│ │ └── ViewModelContactCreateTests.swift
│ ├── Edit
│ │ └── ViewModelContactEditTests.swift
│ └── List
│ └── ViewModelContactListTests.swift
├── Domain
│ ├── UseCases
│ │ └── Contact
│ │ ├── UseCaseContactCreateTests.swift
│ │ ├── UseCaseContactUpdateTests.swift
│ │ ├── UseCaseContactDeleteTests.swift
│ │ ├── UseCaseContactsGetAllTests.swift
│ │ └── UseCaseContactGetOneTest.swift
│ └── Repositories
│ └── ContactRepositoryTests.swift
└── Data
└── DataSources
└── CoreDataContactDataSourceTests.swift
We create our application using an App target (Contacts) and Test target(ContactTests). The Test target mirrors the structure of the application target.
The first thing we usually do is ask ourselves what will the application do. This we define using models and interfaces/protocols.
Contact Model
Because the app involves contacts, we define it in the contact.swift file. In this model file, we define a response and a request model for contact. For simple entities you might not need to do this, however, we think it helps streamline the shape of the data to the data source (request model) and the data from the data source (response model). In a function like “create” we’ll use the request model and in functions like “get” we’ll only need the response model. Think of these models as data pipes up and down through the layers.
Protocols
We use protocols or interfaces to illustrate intent and enforce the behaviour of a class. The domain layer for example has a “Protocols” folder which holds all interfaces for that layer. Let’s look at the domain protocols
Use Case Protocols
These use case protocols specify our use case rules
Contact Repository Protocol
The repository is typically called by one or more use cases and is predominantly used to manage data sources. These are the methods that are available for the use cases.
Contact Data Source Protocol
Data sources that implement this protocol would need to do the following operations.
View Model Tests
We’ll use XCode to create a Unit Test Bundle Target for our Tests. Let’s call it “ContactTests”. Before creating any production code, let’s first write tests for our contact list view model.
Now, of course, all tests within this test suite will fail, in fact, it won’t even build. The view model and the mocks have not been created.
In the setup function of the test suite, we create mock use cases to inject into the view model we are testing
Let’s first create our mock use cases. Before we start, just a quick note on mocking frameworks in swift:
Note: Swift does not allow read-write reflection. Read-write reflection basically allows for modifying programs at run time. Most mocking frameworks rely on this language feature. The mocking frameworks that are written for Swift, therefore, cannot use read-write reflection and has to use code generation at compile time to generate mocks. I’m not a fan of this and will therefore create my own simplistic mocks
Let’s create the 2 use case mocks that the view model needs
Finally, Let’s also create our view model code so that our tests can pass
View Model
This view model has 4 public facing members:
- The list of contacts,
- The error message,
- The “getContacts” function
- The “deleteContact” function
Moving down the vertical slice of functionality, we can write tests for the use cases
Here the use case depends on the contact repository. Let’s create the repo mock
and then our use case code to make the tests pass
You get the picture! To follow further, check out the GitHub which includes tests for all testable components.
Check out the GitHub repo here
Originally published at https://nanosoft.co.za/blog/post/clean-architecture-ios