A minimal, production‑grade example demonstrating how UseCase, Repository, and ViewModel interact inside a Clean Architecture iOS/macOS project.
This project exists for one purpose:
to show how a cleanly isolated domain can be tested without touching UI, storage, or infrastructure.
The domain is intentionally simple — a small library system.
It is “grown‑up” enough to represent real business logic, but minimal enough that nothing distracts from the architecture.
Modern iOS apps rely on business logic that must stay independent from the interface, storage engine, or networking layer.
This project demonstrates how to structure such logic using:
- Domain UseCases — the autonomous core of the application
- Repositories — abstract data access boundaries
- ViewModels — thin mediators used by SwiftUI
- Mocks — isolated test doubles that allow deterministic testing
The idea is straightforward:
If your UseCase works correctly with a mock repository,
it will work correctly with a real one — and without fragile, UI‑driven tests.
This repository is designed to be examined reading top‑down, from the conceptual architecture to the concrete implementation.
LibraryBooks/
│
├─ Presentation/
│ └─ BookList/
│ ├─ BookListView.swift
│ └─ BookListViewModel.swift
│
├─ Domain/
│ ├─ Entities/
│ │ └─ Book.swift
│ ├─ UseCases/
│ │ ├─ BookUseCase.swift
│ │ └─ BookUseCaseImpl.swift
│ └─ Errors/
│ └─ BookError.swift
│
├─ Data/
│ └─ Repositories/
│ ├─ BookRepository.swift
│ └─ BookRepositoryInMemory.swift
│
└─ Tests/
├─ Support/
│ ├─ BookRepositoryMock.swift
│ ├─ BookUseCaseMock.swift
│ └─ BookMocks.swift
├─ BookRepositoryTests.swift
├─ BookUseCaseTests.swift
└─ BookListViewModelTests.swift
- Contains
BookListViewModel - Fetches available books
- Handles borrow operations
- Reacts to UseCase outputs (success or error)
- Holds no business rules
- Contains:
- The
Bookentity - UseCase protocol (
BookUseCase) - UseCase implementation (
BookUseCaseImpl) - Domain errors (
BookError)
- The
This layer models what the application does, not how it is persisted or rendered.
- Contains repository protocol + an in‑memory implementation.
- Responsible only for reading/updating storage.
- Storage Type → replaceable at any moment:
- SwiftData
- CoreData
- SQLite
- JSON
- Network
- hybrid or mocked storage
Thanks to the repository boundary, switching the underlying engine does not touch the domain layer.
struct Book {
let id: UUID
let title: String
var isBorrowed: Bool
}
A minimal, strictly defined entity without behavior.
It contains no validation, no methods, and no implicit logic.
All decisions stay inside the UseCase.
BookUseCase describes the business rules:
-
getAvailableBooks()
Returns only books not currently borrowed. -
borrowBook(id:)
Marks a book as borrowed.
UseCase is intentionally synchronous to keep tests simple and focused on business logic, not concurrency.
Key property:
UseCase does not know where books come from or how they are saved.
This is the cornerstone of its testability.
BookRepository exposes the minimal required API:
fetchAll()— returns all bookssave(_:)— saves or updates a book
This boundary forces the Data layer to stay simple and predictable.
The default implementation is an in‑memory store.
It is intentionally small — the goal is clarity, not infrastructure.
Methods are synchronous to keep the focus on business logic testing, not async complexity.
BookListViewModel is a UI‑agnostic reactive wrapper around the UseCase.
Responsibilities:
- load available books
- borrow a book
- expose state (
books,error) - hold no side effects other than communicating with the UseCase
Because it contains no domain rules, it becomes trivial to test.
Note: For the purposes of this article, a separate "Borrowed Books" list is intentionally omitted. The focus is on demonstrating how to test business logic (filtering available books) rather than building a complete UI. Adding a borrowed books list would add complexity without contributing to the core testing concepts.
The project demonstrates a deterministic testing approach:
Test the business logic in isolation:
- availability filtering
- borrow operations
- correct error propagation
Test the data layer boundary:
- fetchAll returns all books
- save adds new books
- save updates existing books
Ensure correct presentation behavior:
- loads available books from use case
- handles errors gracefully
- updates state after borrow operations
Because business logic is isolated, 90% of correctness is proven via unit tests.
The testing approach in this project demonstrates a fundamental principle of Clean Architecture:
If your UseCase works correctly with a mock repository, it will work correctly with any real data source.
By testing business logic in isolation:
- No UI dependencies: Tests don't need SwiftUI views or user interactions
- No infrastructure dependencies: Tests don't need databases, networks, or file systems
- Fast execution: Tests run in milliseconds, not seconds
- Deterministic results: Same inputs always produce same outputs
The BookRepositoryMock implements the same protocol as BookRepositoryInMemory. This means:
- UseCase doesn't know it's talking to a mock
- Business logic is tested independently of storage implementation
- You can swap implementations without changing tests
When BookUseCaseTests pass, they prove:
- Business rules are correct (filtering, validation)
- Error handling works (bookNotFound, bookAlreadyBorrowed)
- State mutations are correct (isBorrowed flag updates)
These tests give confidence that the business logic will work correctly whether the repository uses:
- In-memory storage (current implementation)
- SwiftData (future implementation)
- Core Data (future implementation)
- Network API (future implementation)
This is the power of Clean Architecture: test once, deploy anywhere.
- Clean Architecture allows business logic to evolve without rewriting UI or persistence.
- UseCase sits at the center — it is the single source of truth for decisions.
- Repository abstraction keeps the domain independent from storage details.
- ViewModel is intentionally thin — a reactive bridge, not a logic container.
- Tests become small, meaningful, and deterministic.
This project is intentionally minimal.
It serves as a reference for anyone who wants to build scalable, testable Swift applications using a layered architecture.
You can easily modify or expand it:
- Swap the repository with a real database
- Add networking
- Add a “returnBook” feature
- Add reservations or due dates
- Add caching or offline mode
- Integrate into a larger app
The architecture remains stable, and each new feature grows naturally inside the existing boundaries.
Apache 2.0 License.