clean-architecture-ios

Clean Architecture iOS — Expert Decisions

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "clean-architecture-ios" with this command: npx skills add kaakati/rails-enterprise-dev/kaakati-rails-enterprise-dev-clean-architecture-ios

Clean 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

UserRepositoryProtocol

Domain Data (UserRepository)

UserRemoteDataSourceProtocol

Data Data (UserRemoteDataSource)

GetUserUseCaseProtocol

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

import UIKit in Domain Layer violation Move to Presentation

UseCase just calls repo.get()

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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

flutter conventions & best practices

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

getx state management patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

ruby oop patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

rails localization (i18n) - english & arabic

No summary provided by upstream source.

Repository SourceNeeds Review