SwiftUI MVVM clean architecture

Michał Ziobro
Mac O’Clock
Published in
5 min readDec 11, 2021

In this tutorial I will try to present you how to use clean, testable and maintainable architecture when using SwiftUI.

We start with diagram depicting this architecture and then we dive deep into each part with code samples.

  1. View

View is standard SwiftUI view or UIKit view/view controller wrapped into UIViewRepresentable/UIViewControllerRepresentable.

It has injected via constructor ViewModel. This injection should be done using protocol in order to not relay on concrete implementation but rather on abstraction. Protocol of view model usually inherit ObservableObject that has associatedtype. Therefore to inject such ViewModel relaying on its protocol we need to use generic type inheriting view model protocol. Moreover to enable View refreshing on ViewModel @Published properties changes it should be stored as @StateObject or @ObservedObject.

2. ViewModel

ViewModel is component that receives actions from events such as button tap or view lifecycle onAppear and processes them. To process this events it needs to have injected other objects usually some repositories to which it forward this processing. It also prepares state for views to render in next rendering. It is important that all public methods and properties used by View to be exposed via view model protocol. In addition state properties whose change should be reflected in the next view rendering should use @Published property wrapper (it calls objectWillChange.send() underneath).

ViewModel dependencies can be injected via constructor or using some injection framework like Resolver. You could also use some custom injection mechanism. In example below we abstracted it using @Inject property wrapper.

Additional remarks:
Starting from Swift 5.5 usage of Combine request can be replaced by async/await. Such task can be started using .task { } view modifier instead of .onAppear { } in view.
Combine can also be replaced with RxSwift. In case of views written in UIKit instead of bindings to @Published properties you could use other available mechanisms for view model output properties i.e. Combine: CurrentValueSubject, PassthroughSubject, RxSwift: BehaviourSubject, PublishSubject, closures, or delegation mechanisms (here view controller implements such delegate, and view model keeps weak reference to it)

Sometimes people split single view model protocol into to separate protocols for input and output like below:

3. Repository
Repository is sometimes named also Interactor in Swift community. It is intermediary between view model (presentation layer) and services or data access objects (DAO) (data layer). In some more advanced architectures there can be also separate domain layer with business logic implemented via objects named UseCases. In such cases Repository is usually assigned as part of data layer and has limited responsibility to just managing data accesses in local and remote storages. In our simplified case we assume that this business logic is implemented in such repository.
The main purpose of repository is managing access to local and remote data storages. To achieve this goal it uses two types DAO (for local persistent storage access) and Services (for remote data fetching)
Repository also has logic that manages data caching, i.e. view receives last local data state (using @FetchRequest or DAO fetch call) while in the meantime there is api request done using Services. Then data fetched from REST or GraphQL api are saved to local data storage using DAO.

4. Service

Service is a component of application that enables communication with web services (API) to make request to get or post some data. It can be used for both REST apis and GraphQL apis. Services should encapsulate communication with api for particular type of data like WordsetService, UserProfileService, etc.

Such service should usually use some networking module that creates simple to use networking infrastructure wrapping URLSession or Alamofire networking apis. If you are interested in such simple Networking library you can try my github repo. There you find implementation of BaseService type.

It is also wise to encapsulate all api endpoints in multiple Routers.

Such routers configures each api endpoints i.e. its Http Method, parameters, headers, and are then consumed by such BaseService to make actual request and return response with some data.

Returned data are usually in JSON or XML format and should be appropriately parsed to Swift types. In the case of JSON it can be done using Decodable. Such models used to pass parsed data back from Service to Repository are usually named DTO i.e. data transfer objects. This way they are independent from models used in app and stored in local data storage.

5. Data Access Object (DAO)

Data Access Object as it names implies allow to access local persistent storages to execute CRUD operations on it i.e. create, read, update, delete data in this local storage. For instance if you are using Core Data for storing entities such data maps your DTO objects and saves them as NSManagedObjects. Many basic CRUD operations can be abstracted away to BaseDao similarly as it was done with BaseService. Relaying on protocols rather then concrete implementation of Dao enables as to simple replace it to use Realm instead of CoreData, or yet another data storage.

When we use BaseDao with core CRUD implementations such concrete Dao type can look as simple as below code

6. Entities

The last but not least are entities i.e. models stored in local data storage. In the case of Core Data underlying persistent storage they can be just NSManagedObject.

If you use CoreData and SwiftUI views then it is possible to fetch entities directly from CoreData storage using @FetchRequest. This property wrapper enables to detect changes in underlying database and reflect them in SwiftUI view in the next rendering. So your views are observing changes in CoreData that are made by synchronization methods. This way views always display recent loaded data from data storage and refresh them on view appears from web apis.

In cases when you doesn’t want to relay on @FetchRequest you can adjust above methods from syncing to fetching data. So instead of just updating local storage with new data in Repository they also return observable current and refreshed data.

7. Simplified architectures

Of course if it isn’t need to cache data in local data storage then branch with Dao can be skipped.

In the most simplified scenario you can also consider omitting Repository and remain with just.

In both above scenarios dotted arrows means that data are passed back using bindings @Published properties or by subscribing Combine publishers.

You can imagine such scenario in view model.

--

--