Apps grow. What starts as a two-screen project accumulates business rules, integrations, state and edge cases. Without structure, the code turns into a web where changing one thing breaks another.
Clean Architecture, proposed by Robert C. Martin, splits code into layers with well-defined responsibilities. The central rule: dependencies only point inward. The domain layer doesn’t know SwiftUI exists, nor the database, nor the API.
The three layers
Domain
The heart of the app. It contains:
- Entities: the business data structures (
Transaction,Vehicle,Project) - Use Cases: the possible actions (
CreateTransaction,FetchVehicleById) - Repository interfaces: Swift protocols that define how data is obtained
No framework dependencies. Pure Swift. Testable without UIKit or SwiftUI mocks.
Data
Implements the domain’s protocols:
- Repository implementations: call APIs or local persistence
- Data sources:
RemoteDataSource(URLSession/REST) andLocalDataSource(SwiftData/CoreData) - Models: serializable structs using
Codable
Presentation
The actual SwiftUI layer:
- Views: screens and reusable components
- ViewModels:
@ObservableorObservableObjecttypes that expose state to the views - Coordinators: manage navigation without coupling views to each other
It depends only on use cases. It never makes network calls directly.
In practice: an example
In FinPessoal, the flow for recording a transaction is:
- The user fills in the form → the view calls
CreateTransactionUseCase - The use case validates the entity and calls
TransactionRepository.save() - The repository delegates to
LocalDataSource.insert(transactionModel) - Success or error travels back the same way via
async throws
None of these layers knows the internal implementation of the others — only the protocols.
The payoff in testing
With this separation, testing a use case is straightforward:
func testRejectsTransactionWithZeroAmount() async throws {
let useCase = CreateTransactionUseCase(
repository: MockTransactionRepository()
)
await #expect(throws: InvalidAmountError.self) {
try await useCase.execute(
Transaction(amount: 0, description: "Test")
)
}
}
No simulator. No real database. Just business logic.
The same goes for data sources: you test Codable serialization without depending on a real network. And views are tested without depending on business rules.
When to apply it
Clean Architecture doesn’t need to be applied rigidly in small apps. In projects with fewer than ten screens and no complex business logic, a simpler split (plain MVVM with @Observable) can be enough.
The layered architecture pays off when:
- The project will grow (new features are planned)
- There are multiple developers on the team
- Serious automated testing is needed
- The business logic is complex or will change
Conclusion
Clean Architecture isn’t overhead for small apps — it’s what keeps small apps small as they grow. Separating layers is what lets us add a new feature without fear of breaking what already works.
It’s the pattern we use in every werkStadtG project, and the one we recommend for any iOS app that’s meant to last.