Clean Architecture: Use-Case Centered Design in SwiftUI — MVVM

Roli Bernanda
5 min readAug 25, 2023
Photo by AltumCode on Unsplash


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

Clean Architecture — Dependency Rule

The project split into 4 layers, The Presentation Layer, The Domain Layer, The Data Layer and The Core Layer

  1. 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


struct Event: Identifiable {
var id: UUID = .init()
var title: String
var description: String
var date: Date


protocol EventRepository{
func getEvents() -> [Event]
func createEvent(event: Event) -> Event?


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


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{ item in
title: item.title ?? "",
description: item.desc ?? "",
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 =

do {

return event
} catch {
print("Failed creating new event")
print("Error: \(error.localizedDescription)")

return nil


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


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() { = eventUseCases.getEvents()

func createEvent() {
let newEvent = Event(title: title, description: description, date: date)
guard let event = eventUseCases.createEvent(event: newEvent) else {

withAnimation { events.append(event) }

func resetForm() {
self.title = ""
self.description = "" = .init()


struct EventList: View {
@StateObject var vm = EventListViewModel()

var body: some View {
NavigationStack {
List {
ForEach({ event in
VStack (alignment: .leading, spacing: 4) {
.navigationTitle("Todo List")
.toolbar {
Button("Add") {
.onAppear {

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.