Clean Architecture: Use-Case Centered Design in SwiftUI — MVVM
Introduction
To make it clear, what Uncle Bob calls ‘Clean Architecture’ is more like a high-level guideline.
It’s not saying, “You have to use a Model layer, a View layer, and a ViewModel layer, following MVVM pattern” Instead, it gives you general tips for setting up layered architectures. It’s not super strict like “You must have these specific layers, and they must be done in MVVM pattern or any specific pattern”
The goal is to separate different aspects of the software. This means breaking down the software into layers, where each layer handles either business rules or interfaces
Why Use-Case Centered Design
Imagine you’re checking out an architect’s blueprint for a house. Now think about a plan for a regular family home. You’d expect to see the basics like a front entrance, dining room, living room, kitchen and so on. The goal is to make sure the house usable, without stressing over whether it’s made of bricks, stone, or cedar.
And that’s the point — the reason that good architecture are centered around use-cases is so that architects can safely describe the structures that support those use-cases without committing to frameworks, tools, and environment.
Our architecture should inform readers about the system itself, not about the frameworks or the databases we use.
Implementing Clean Architecture in SwiftUI
Project Structure
├── Domain
├── Data
├── Application
└── Utilites
Visual Representation
The project split into 4 layers, The Presentation Layer, The Domain Layer, The Data Layer and The Core Layer
- Presentation Layer
- ViewModel layer is used by the Pages layer. The purpose is to provide data to the Pages layer.
- View Layer should be as dumb as possible: no business logic, only UI logic
2. The Domain layer is split into three layers: the Entities layer, the Repositories layer, and the UseCases layer.
- The Entities layer is used by the Components and the ViewModel layer. i.e. struct Event {}
- The UseCases layer is used by the ViewModel layer.
- The Repositories layer is used by the UseCases layer and the DataSource layer
3. The Data layer is split into two layers: the Models layer and the DataSource layer.
- The Models layer is used by the Repositories layer and the DataSource layer. The purpose is to define what is expected result from the DataSource layer. i.e. a API response.
- The DataSource layer is used by the Repositories layer. The purpose is to get data from somewhere. i.e. from local storage or from an API.
4. The Core layer is used by the UseCases layer
… Enough talk, lets write the code.
Creating Use-Case: start with domain
Domain/Event/Entity.swift
struct Event: Identifiable {
var id: UUID = .init()
var title: String
var description: String
var date: Date
}
Domain/Repository/EventRepository.swift
protocol EventRepository{
func getEvents() -> [Event]
func createEvent(event: Event) -> Event?
}
Domain/UseCase/EventUseCases.swift
struct EventUseCases: EventRepository {
var repo: EventRepository
func getEvents() -> [Event] {
return repo.getEvents()
}
func createEvent(event: Event) -> Event? {
return repo.createEvent(event: event)
}
}
We will use the dependency injection pattern to utilize the EventUseCases.
Creating Data Layer
Let’s implement the EventRepository protocol that we wrote previously; we will name it EventCoreDataImpl since the data comes from a local source
Data/DataSource/CoreData/EventCoreDataImpl.swift
import Foundation
import CoreData
struct EventCoreDataImpl: EventRepository {
private let coreDataContext = PersistenceController.shared.context
func getEvents() -> [Event] {
let request: NSFetchRequest<EventMO> = EventMO.fetchRequest()
do {
let EventsMO = try coreDataContext.fetch(request)
return EventsMO.map({ item in
Event(
title: item.title ?? "",
description: item.desc ?? "",
date: item.date ?? Date()
)
})
} catch {
print("Error: \(error.localizedDescription)")
return []
}
}
func createEvent(event: Event) -> Event? {
let newEventMO = EventMO(context: coreDataContext)
newEventMO.title = event.title
newEventMO.desc = event.description
newEventMO.date = event.date
do {
try coreDataContext.save()
return event
} catch {
print("Failed creating new event")
print("Error: \(error.localizedDescription)")
return nil
}
}
}
Data/Repository/EventRepositoryImpl.swift
struct EventRepositoryImpl: EventRepository{
var dataSource: EventRepository
func getEvents() -> [Event] {
return dataSource.getEvents()
}
func createEvent(event: Event) -> Event? {
return dataSource.createEvent(event: event)
}
}
and that covers pretty much everything for the Domain and Data layers. Now we can proceed to write our presentation’s view model and view
Application/ViewModels/EventListViewModel.swift
class EventListViewModel: ObservableObject {
var eventUseCases = EventUseCases(repo: EventRepositoryImpl(dataSource: EventCoreDataImpl()))
@Published var events: [Event] = .init()
@Published var title = "Pet Grooming"
@Published var description = "A day dedicated to pet grooming"
@Published var date: Date = .init()
func getEvents() {
self.events = eventUseCases.getEvents()
}
func createEvent() {
let newEvent = Event(title: title, description: description, date: date)
guard let event = eventUseCases.createEvent(event: newEvent) else {
return
}
withAnimation { events.append(event) }
resetForm()
}
func resetForm() {
self.title = ""
self.description = ""
self.date = .init()
}
}
Application/Views/EventList.swift
struct EventList: View {
@StateObject var vm = EventListViewModel()
var body: some View {
NavigationStack {
List {
ForEach(vm.events){ event in
VStack (alignment: .leading, spacing: 4) {
Text(event.title)
Text(event.date.formatted())
}
}
}
.navigationTitle("Todo List")
.toolbar {
Button("Add") {
vm.createEvent()
}
}
.onAppear {
vm.getEvents()
}
}
}
}
Key Takeaway
The center of your application is the use case of your application ~ Uncle Bob
The idea is straightforward, Our design should not rely frameworks. Frameworks are tools to be used, they should not dictate how we build our system. If our architecture is based on frameworks, then it cannot be based on our use cases..
And we split the software into layers, following The Dependency Rule. This makes the system easy to test, which comes with its own set of benefits. So, if any of the external parts of the system, like the database or framework become outdated, we can switch them out without much trouble.
Reference and Additional Resources
- https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html
- https://www.codingblocks.net/podcast/clean-architecture-make-your-architecture-scream/
- https://medium.com/@tiagoflores_23976/how-choose-the-appropriate-ios-architecture-mvc-mvp-mvvm-viper-or-clean-architecture-2d1e9b87d48
- https://nalexn.github.io/clean-architecture-swiftui/
- https://paulallies.medium.com/clean-architecture-in-the-flavour-of-swiftui-5-5-8430786a83
- https://www.youtube.com/watch?v=Nsjsiz2A9mg&t=1579s