Photo by Michael Burrows from Pexels

Clean Architecture: iOS App

Paul Allies

--

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:

  1. The list of contacts,
  2. The error message,
  3. The “getContacts” function
  4. 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

--

--