Building Scalable iOS Apps with Clean Architecture
Clean Architecture, popularized by Robert C. Martin (Uncle Bob), is a software design philosophy that separates code into layers with clear responsibilities. In this post, I will share how I apply these principles in my iOS projects.
Why Clean Architecture for iOS?
As iOS developers, we have all experienced the "Massive View Controller" problem. Clean Architecture solves this by enforcing strict boundaries:
- Testability: Each layer can be unit tested independently
- Maintainability: Changes in the UI layer do not affect business logic
- Scalability: Adding new features becomes straightforward
- Reusability: Business logic can be shared across platforms
The Layer Structure
Domain Layer (Innermost)
This is the heart of your application. It contains:
- Entities: Core business objects (e.g.,
User,Product) - Use Cases / Interactors: Application-specific business rules
- Repository Protocols: Interfaces that define data access contracts
1// Domain Entity
2struct BlogPost {
3 let id: UUID
4 let title: String
5 let content: String
6 let publishedAt: Date?
7 let author: Author
8}
9
10// Use Case
11protocol GetFeaturedPostsUseCase {
12 func execute() async throws -> [BlogPost]
13}
14
15// Repository Protocol
16protocol BlogRepository {
17 func fetchFeatured() async throws -> [BlogPost]
18 func fetchBySlug(_ slug: String) async throws -> BlogPost?
19}Data Layer
Implements the repository protocols defined in the Domain layer:
1final class BlogRepositoryImpl: BlogRepository {
2 private let apiClient: APIClient
3 private let cache: CacheManager
4
5 func fetchFeatured() async throws -> [BlogPost] {
6 if let cached = cache.get("featured_posts") {
7 return cached
8 }
9 let posts = try await apiClient.get("/posts?featured=true")
10 cache.set("featured_posts", posts, ttl: 300)
11 return posts
12 }
13}Presentation Layer (Outermost)
Uses the MVVM pattern with SwiftUI:
1@MainActor
2final class BlogViewModel: ObservableObject {
3 @Published var posts: [BlogPost] = []
4 @Published var isLoading = false
5
6 private let getFeaturedPosts: GetFeaturedPostsUseCase
7
8 func loadPosts() async {
9 isLoading = true
10 defer { isLoading = false }
11 do {
12 posts = try await getFeaturedPosts.execute()
13 } catch {
14 // Handle error
15 }
16 }
17}Dependency Injection
I use a simple DI container to wire everything together:
1final class AppContainer {
2 lazy var apiClient = APIClient(baseURL: Config.apiURL)
3 lazy var blogRepository: BlogRepository = BlogRepositoryImpl(apiClient: apiClient)
4 lazy var getFeaturedPosts: GetFeaturedPostsUseCase = GetFeaturedPostsInteractor(repo: blogRepository)
5}Key Takeaways
- Start with the Domain — define your entities and use cases first
- Dependencies point inward — outer layers depend on inner layers, never the reverse
- Use protocols — they enable testing and flexibility
- Keep ViewModels thin — delegate business logic to use cases
Clean Architecture requires more upfront structure, but the long-term benefits in testability and maintainability are well worth the investment.