Clean Architecture Setup for iOS Apps – Turnkey
Imagine an app with 50 screens, where every ViewController pulls in the networking layer, database, and navigation logic. A new feature takes a week to add, but each change breaks old tests. The codebase grows, and development speed drops. We've seen this in 30+ projects – from banking to streaming. The solution is the Clean Architecture approach, a layered architecture that rigidly separates responsibilities and makes code predictable.
Clean Architecture, popularized by Robert Martin, proposes three concentric layers: Domain (core business logic), Data (repository implementations and data sources), and Presentation (UI and ViewModel). Dependencies point inward: Domain knows nothing about Data and Presentation. This allows testing business logic without launching the simulator and changing UI without touching the core. Implementing this approach speeds up adding new features by 3x and reduces regression testing time by 80% – figures from our practice. Typical investment for a full setup ranges from $3,000 to $15,000, depending on project complexity.
We have implemented this pattern for 30+ commercial projects: banking apps, streaming services, marketplaces. Clients get an architecture that accelerates development and reduces maintenance costs. Below we break down how it works in practice.
How This Architecture Solves iOS Development Problems
In Bob Martin's classic interpretation, there are three rings: Entities → Use Cases → Interface Adapters. In iOS this maps as follows.
Domain layer – the core. Here are Entity models: pure Swift structures without importing Foundation, only business data. Alongside are UseCase protocols and their implementations. For example, FetchUserProfileUseCase takes a UserRepository via dependency injection and returns AnyPublisher<UserProfile, DomainError>. No URLSession, no CoreData. This layer compiles and tests in isolation.
protocol UserRepository { func fetchProfile(id: String) -> AnyPublisher<UserProfile, DomainError> } final class FetchUserProfileUseCase { private let repository: UserRepository init(repository: UserRepository) { self.repository = repository } func execute(id: String) -> AnyPublisher<UserProfile, DomainError> { repository.fetchProfile(id: id) } } Data layer – repository implementations. UserRepositoryImpl works with URLSession or Alamofire, maps DTO to domain model, handles network errors. CoreDataUserCache implements the same protocol for local caching. The choice of data source is in UserRepositoryImpl via strategy or in the DI container.
Presentation layer – here live ViewModel/Presenter. In combination with SwiftUI, an ObservableObject-ViewModel is convenient: it calls the UseCase, transforms the result into @Published state and publishes it. ViewController or SwiftUI View handles rendering exclusively.
Navigation and DI
| Layer | Responsibility | Dependencies |
|---|---|---|
| Domain | Business logic, Entity, UseCase | None |
| Data | Repository implementations, mapping | Domain |
| Presentation | ViewModel, View, Coordinator | Domain |
A typical problem – ViewController creates the next ViewController and pushes it. Solution – Coordinator. ViewModel holds a weak reference to ProfileCoordinator. The concrete ProfileCoordinatorImpl knows about UINavigationController and subsequent screens. ViewModel does not.
protocol ProfileCoordinator: AnyObject { func showEditProfile(user: UserProfile) func showOrders(userId: String) } We use initializer injection in the chain: SceneDelegate creates AppCoordinator, which creates specific repositories and UseCases, passes them to ViewModel via init. Swinject or Needle can be plugged in, but for most projects manual assembly in CompositionRoot is sufficient. Service Locator is an anti-pattern: it hides dependencies and breaks tests.
Why Clean Architecture Is Better Than Standard MVC
In MVC, ViewController often becomes a "god object" responsible for everything. The layered architecture gives a 3x speedup in adding new features due to clear boundaries: changes in UI do not affect business logic. Build time for domain layer tests is reduced by 80% – seconds instead of minutes. Here's a test example:
final class FetchUserProfileUseCaseTests: XCTestCase { func test_execute_returnsProfile() { let mock = MockUserRepository(result: .success(.stub())) let sut = FetchUserProfileUseCase(repository: mock) var received: UserProfile? _ = sut.execute(id: "123").sink( receiveCompletion: { _ in }, receiveValue: { received = $0 } ) XCTAssertEqual(received?.id, "123") } } How to Implement Clean Architecture in an iOS Project: Step-by-Step Guide
- Audit the current architecture and identify cross-layer points.
- Create Swift Packages for each layer: Domain, Data, Presentation.
- Define repository protocols in Domain, implement them in Data.
- Set up CompositionRoot – a central class for manual dependency initialization.
- Write unit tests for key UseCases.
Common Mistakes When Implementing Clean Architecture
- Too thin UseCases.
GetUsernameUseCasethat doesreturn user.nameis useless. - Domain models with Codable. DTO and mapping must be in the Data layer.
- ViewModel knows about a concrete repository. Only protocol + initializer injection.
Avoiding these mistakes, you get an architecture that is easy to maintain and extend. We guarantee quality based on over 5 years of experience and more than 200 successful deployments. Contact us for a consultation – we will evaluate your project and offer an optimal solution. Order Clean Architecture setup and accelerate development multiple times.
Turnkey Clean Architecture Setup: Timelines and Pricing
Setup from scratch on a new project: 3–5 days. Refactoring an existing project with migration of 10–15 modules: 2–4 weeks. Cost is calculated after code analysis, typically between $3,000 and $15,000. Get a consultation – we will evaluate your project and provide a guarantee on the architecture. Contact us to discuss details.
What's Included in the Setup
| Stage | Actions | Result |
|---|---|---|
| Audit | Analyze current architecture, identify dependencies | Migration plan |
| Modules | Create Swift Package modules for Domain, Data, Presentation | Clean project structure |
| DI | Implement CompositionRoot or DI container | Dependency management |
| Features | Set up 2–3 feature modules as templates | Example for the team |
| Tests | Write basic unit tests for domain layer | Test coverage |







