Claude-skill-registry clean-architecture-ios
Expert Clean Architecture decisions for iOS/tvOS: when Clean Architecture adds value vs overkill, layer boundary judgment calls, dependency rule violations to catch, and practical trade-offs between purity and pragmatism. Use when designing app architecture, debugging layer violations, or deciding what belongs where. Trigger keywords: Clean Architecture, layer, domain, data, presentation, use case, repository, dependency rule, entity, DTO, mapper
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/clean-architecture-ios" ~/.claude/skills/majiayu000-claude-skill-registry-clean-architecture-ios && rm -rf "$T"
skills/data/clean-architecture-ios/SKILL.mdClean Architecture iOS — Expert Decisions
Expert decision frameworks for Clean Architecture choices. Claude knows the layers — this skill provides judgment calls for boundary decisions and pragmatic trade-offs.
Decision Trees
When Clean Architecture Is Worth It
Is this a side project or prototype? ├─ YES → Skip Clean Architecture (YAGNI) │ └─ Simple MVVM with services is fine │ └─ NO → How many data sources? ├─ 1 (just API) → Lightweight Clean Architecture │ └─ Skip local data source, repository = API wrapper │ └─ Multiple (API + cache + local DB) └─ How long will codebase live? ├─ < 1 year → Consider simpler approach └─ > 1 year → Full Clean Architecture └─ Team size > 2? → Strongly recommended
Clean Architecture wins: Apps with complex business logic, multiple data sources, long maintenance lifetime, or teams > 3 developers.
Clean Architecture is overkill: Prototypes, simple apps with single API, short-lived projects, solo developers who know the whole codebase.
Where Does This Code Belong?
Does it know about UIKit/SwiftUI? ├─ YES → Presentation Layer │ └─ Views, ViewModels, Coordinators │ └─ NO → Does it know about network/database specifics? ├─ YES → Data Layer │ └─ Repositories (impl), DataSources, DTOs, Mappers │ └─ NO → Is it a business rule or core model? ├─ YES → Domain Layer │ └─ Entities, UseCases, Repository protocols │ └─ NO → Reconsider if it's needed
UseCase Granularity
Is this operation a single business action? ├─ YES → One UseCase per operation │ Example: CreateOrderUseCase, GetUserUseCase │ └─ NO → Does it combine multiple actions? ├─ YES → Can actions be reused independently? │ ├─ YES → Separate UseCases, compose in ViewModel │ └─ NO → Single UseCase with clear naming │ └─ NO → Is it just CRUD? ├─ YES → Consider skipping UseCase │ └─ ViewModel → Repository directly is OK for simple CRUD │ └─ NO → Review the operation's purpose
The trap: Creating UseCases for every operation. If it's just
repository.get(id:) pass-through, skip the UseCase.
NEVER Do
Dependency Rule Violations
NEVER import outer layers in inner layers:
// ❌ Domain importing Data layer // Domain/UseCases/GetUserUseCase.swift import Alamofire // Data layer framework! import CoreData // Data layer framework! // ❌ Domain importing Presentation layer import SwiftUI // Presentation framework! // ✅ Domain has NO framework imports (except Foundation) import Foundation
NEVER let Domain know about DTOs:
// ❌ Repository protocol returns DTO protocol UserRepositoryProtocol { func getUser(id: String) async throws -> UserDTO // Data layer type! } // ✅ Repository protocol returns Entity protocol UserRepositoryProtocol { func getUser(id: String) async throws -> User // Domain type }
NEVER put business logic in Repository:
// ❌ Business validation in Repository final class UserRepository: UserRepositoryProtocol { func updateUser(_ user: User) async throws -> User { // Business rule leaked into Data layer! guard user.email.contains("@") else { throw ValidationError.invalidEmail } return try await remoteDataSource.update(user) } } // ✅ Business logic in UseCase final class UpdateUserUseCase { func execute(user: User) async throws -> User { guard user.email.contains("@") else { throw DomainError.validation("Invalid email") } return try await repository.updateUser(user) } }
Entity Anti-Patterns
NEVER add framework dependencies to Entities:
// ❌ Entity with Codable for JSON struct User: Codable { // Codable couples to serialization format let id: String let createdAt: Date // Will have JSON parsing issues } // ✅ Pure Entity, DTOs handle serialization struct User: Identifiable, Equatable { let id: String let createdAt: Date } // Data layer handles Codable struct UserDTO: Codable { let id: String let created_at: String // API format }
NEVER put computed properties that need external data in Entities:
// ❌ Entity needs external service struct Order { let items: [OrderItem] var totalWithTax: Decimal { // Where does tax rate come from? External dependency! total * TaxService.currentRate } } // ✅ Calculation in UseCase final class CalculateOrderTotalUseCase { private let taxService: TaxServiceProtocol func execute(order: Order) -> Decimal { order.total * taxService.currentRate } }
Mapper Anti-Patterns
NEVER put Mappers in Domain layer:
// ❌ Domain knows about mapping // Domain/Mappers/UserMapper.swift — WRONG LOCATION! // ✅ Mappers live in Data layer // Data/Mappers/UserMapper.swift
NEVER map in Repository if domain logic is needed:
// ❌ Silent default in mapper enum ProductMapper { static func toDomain(_ dto: ProductDTO) -> Product { Product( currency: Product.Currency(rawValue: dto.currency) ?? .usd // Silent default! ) } } // ✅ Throw on invalid data, let UseCase handle enum ProductMapper { static func toDomain(_ dto: ProductDTO) throws -> Product { guard let currency = Product.Currency(rawValue: dto.currency) else { throw MappingError.invalidCurrency(dto.currency) } return Product(currency: currency) } }
Pragmatic Patterns
When to Skip the UseCase
// ✅ Simple CRUD — ViewModel → Repository is fine @MainActor final class UserListViewModel: ObservableObject { private let repository: UserRepositoryProtocol func loadUsers() async { // Direct repository call for simple fetch users = try? await repository.getUsers() } } // ✅ UseCase needed — business logic involved final class PlaceOrderUseCase { func execute(cart: Cart) async throws -> Order { // Validate stock // Calculate totals // Apply discounts // Create order // Notify inventory // Return order } }
Rule: No business logic? Skip UseCase. Any validation, transformation, or orchestration? Create UseCase.
Repository Caching Strategy
final class UserRepository: UserRepositoryProtocol { func getUser(id: String) async throws -> User { // Strategy 1: Cache-first (offline-capable) if let cached = try? await localDataSource.getUser(id: id) { // Return cached, refresh in background Task { try? await refreshUser(id: id) } return UserMapper.toDomain(cached) } // Strategy 2: Network-first (always fresh) let dto = try await remoteDataSource.fetchUser(id: id) try? await localDataSource.save(dto) // Cache for offline return UserMapper.toDomain(dto) } }
Minimal DI Container
// For small-medium apps, simple factory is enough @MainActor final class Container { static let shared = Container() // Lazy initialization — created on first use lazy var networkClient = NetworkClient() lazy var userRepository: UserRepositoryProtocol = UserRepository( remote: UserRemoteDataSource(client: networkClient), local: UserLocalDataSource() ) // Factory methods for UseCases func makeGetUserUseCase() -> GetUserUseCaseProtocol { GetUserUseCase(repository: userRepository) } // Factory methods for ViewModels func makeUserProfileViewModel() -> UserProfileViewModel { UserProfileViewModel(getUser: makeGetUserUseCase()) } }
Layer Reference
Dependency Direction
Presentation → Domain ← Data ✅ Presentation depends on Domain (imports UseCases, Entities) ✅ Data depends on Domain (implements Repository protocols) ❌ Domain depends on nothing (no imports from other layers)
What Goes Where
| Layer | Contains | Does NOT Contain |
|---|---|---|
| Domain | Entities, UseCases, Repository protocols, Domain errors | UIKit, SwiftUI, Codable DTOs, Network code |
| Data | Repository impl, DataSources, DTOs, Mappers, Network | UI code, Business rules, UseCases |
| Presentation | Views, ViewModels, Coordinators, UI components | Network code, Database code, DTOs |
Protocol Placement
| Protocol | Lives In | Implemented By |
|---|---|---|
| Domain | Data (UserRepository) |
| Data | Data (UserRemoteDataSource) |
| Domain | Domain (GetUserUseCase) |
Testing Strategy
What to Test Where
| Layer | Test Focus | Mock |
|---|---|---|
| Domain (UseCases) | Business logic, validation, orchestration | Repository protocols |
| Data (Repositories) | Coordination, caching, error mapping | DataSource protocols |
| Presentation (ViewModels) | State changes, user actions | UseCase protocols |
// UseCase test — mock Repository func test_createOrder_validatesStock() async throws { mockProductRepo.stubbedProduct = Product(inStock: false) await XCTAssertThrowsError( try await sut.execute(items: [item]) ) { error in XCTAssertEqual(error as? DomainError, .businessRule("Out of stock")) } } // ViewModel test — mock UseCase func test_loadUser_updatesState() async { mockGetUserUseCase.stubbedUser = User(name: "John") await sut.loadUser(id: "123") XCTAssertEqual(sut.user?.name, "John") XCTAssertFalse(sut.isLoading) }
Quick Reference
Clean Architecture Checklist
- Domain layer has zero framework imports (except Foundation)
- Entities are pure structs with no Codable
- Repository protocols live in Domain
- Repository implementations live in Data
- DTOs and Mappers live in Data
- UseCases contain business logic, not pass-through
- ViewModels depend on UseCase protocols, not concrete classes
- No circular dependencies between layers
Red Flags
| Smell | Problem | Fix |
|---|---|---|
in Domain | Layer violation | Move to Presentation |
UseCase just calls | Unnecessary abstraction | ViewModel → Repo directly |
| DTO in Domain | Layer violation | Keep DTOs in Data |
| Business logic in Repository | Wrong layer | Move to UseCase |
| ViewModel imports NetworkClient | Skipped layers | Use Repository |