Skip to content

Instantly share code, notes, and snippets.

@sunfmin
Created January 12, 2026 13:35
Show Gist options
  • Select an option

  • Save sunfmin/e39c23a9aebc343e0e32cb6f88886372 to your computer and use it in GitHub Desktop.

Select an option

Save sunfmin/e39c23a9aebc343e0e32cb6f88886372 to your computer and use it in GitHub Desktop.

Apple Platform Clean Architecture Constitution

Core Principles

I. Dependency Rule (Inward Dependencies)

Source code dependencies MUST only point inward. Inner layers MUST NOT know anything about outer layers.

  • Entities MUST NOT import Domain, Data, or Presentation code
  • Domain (Use Cases) MUST NOT import Data or Presentation code
  • Data MUST NOT import Presentation code
  • Presentation MAY import all inner layers through protocols

Rationale: This rule ensures the core business logic remains independent of frameworks, UI, and external services, enabling the system to be testable, maintainable, and adaptable to change.

II. Layer Separation

The application MUST be organized into four distinct layers:

  • Entities: Enterprise-wide business objects and rules. Pure Swift structs/classes with no framework dependencies.
  • Domain (Use Cases): Application-specific business rules. Orchestrates data flow between entities and implements use case logic.
  • Data: Repository implementations, network clients, database access. Converts external data formats to/from entities.
  • Presentation: SwiftUI views, ViewModels, Coordinators. Handles UI logic and user interaction.

Rationale: Clear layer boundaries enable independent development, testing, and replacement of components without affecting other parts of the system.

III. Protocol-Driven Design

All layer boundaries MUST be defined by Swift protocols (interfaces).

  • Use Cases MUST define repository protocols that Data layer implements
  • Presentation MUST interact with Domain through use case protocols
  • Concrete implementations MUST be injected via dependency injection
  • No concrete class from an outer layer MAY be referenced by an inner layer

Rationale: Protocol boundaries enable mocking for tests, allow swapping implementations (e.g., switching from CoreData to Realm), and enforce the dependency rule at compile time.

IV. Testability First

All business logic MUST be independently testable without UI, network, or database.

  • Entities MUST be testable with pure unit tests (no mocks needed)
  • Use Cases MUST be testable by injecting mock repositories
  • ViewModels MUST be testable by injecting mock use cases
  • Integration tests MUST verify repository implementations against real services
  • UI tests MAY be used for critical user flows

Rationale: Clean Architecture's primary benefit is testability. If code cannot be tested in isolation, the architecture is compromised.

V. SOLID Principles

All code MUST adhere to SOLID principles:

  • Single Responsibility: Each class/struct has one reason to change. Use Cases handle one use case. ViewModels handle one screen.
  • Open-Closed: Extend behavior through new conformances, not modification. Add new Use Cases rather than modifying existing ones.
  • Liskov Substitution: Any protocol conformance MUST be substitutable. Mock implementations MUST behave consistently with real ones.
  • Interface Segregation: Protocols MUST be small and focused. Split large protocols into role-specific ones.
  • Dependency Inversion: High-level modules MUST NOT depend on low-level modules. Both MUST depend on abstractions (protocols).

Rationale: SOLID principles are the foundation of Clean Architecture and ensure long-term maintainability.

VI. Test-Driven Development (TDD)

All features MUST be developed using Test-Driven Development. Tests MUST be written FIRST and MUST PASS before any human review.

TDD Cycle (Red-Green-Refactor)

  1. RED: Write a failing test that defines the expected behavior
  2. GREEN: Write the minimum code to make the test pass
  3. REFACTOR: Improve the code while keeping tests green

AI-Assisted Development Requirements

When AI assists with development, it MUST:

  1. Write thorough tests FIRST before implementing any feature code
  2. Run all tests and ensure they compile (RED phase - tests should fail initially)
  3. Implement the feature to make tests pass
  4. Run all tests again and verify ALL tests pass (GREEN phase)
  5. Only stop for human review when all tests are passing

Test Coverage Requirements

Layer Minimum Coverage Test Types Required
Entities 100% Unit tests for all computed properties, initializers, edge cases
Use Cases 100% Unit tests with mock repositories for all execution paths
ViewModels 90% Unit tests with mock use cases for all public methods
Repositories 80% Integration tests for CRUD operations

Test Quality Standards

  • Each test MUST test ONE behavior (Single Assertion Principle)
  • Test names MUST describe the scenario: test[Method]_[Scenario]_[ExpectedResult]
  • Tests MUST be independent and not rely on execution order
  • Tests MUST use Arrange-Act-Assert (AAA) or Given-When-Then pattern
  • Mock objects MUST be created for all external dependencies
  • Edge cases MUST be tested: empty inputs, nil values, error conditions, boundary values
// ✅ Correct test naming and structure
func testExecute_WithValidItem_ReturnsCreatedItem() async throws {
    // Arrange (Given)
    let mockRepository = MockItemRepository()
    let useCase = CreateItemUseCase(repository: mockRepository)
    
    // Act (When)
    let result = try await useCase.execute(name: "Test Item")
    
    // Assert (Then)
    XCTAssertEqual(result.name, "Test Item")
    XCTAssertTrue(mockRepository.createCalled)
}

func testExecute_WithEmptyName_ThrowsValidationError() async {
    // Arrange
    let mockRepository = MockItemRepository()
    let useCase = CreateItemUseCase(repository: mockRepository)
    
    // Act & Assert
    do {
        _ = try await useCase.execute(name: "")
        XCTFail("Expected validation error")
    } catch {
        XCTAssertTrue(error is ValidationError)
    }
}

Pre-Review Checklist

Before stopping for human review, AI MUST verify:

  • All tests compile without errors
  • All tests pass (xcodebuild test exits with code 0)
  • Test coverage meets minimum requirements per layer
  • No skipped or disabled tests (no XCTSkip, no commented tests)
  • Edge cases are covered (empty, nil, error, boundary)
  • Mock objects verify method calls where appropriate

Rationale: TDD ensures code correctness from the start, provides living documentation, enables safe refactoring, and catches regressions immediately. Requiring passing tests before human review maximizes review efficiency.

Architecture Layers

Layer Structure

┌─────────────────────────────────────────────────────────────┐
│                    Presentation Layer                        │
│  (SwiftUI Views, ViewModels, Coordinators, UI Components)   │
├─────────────────────────────────────────────────────────────┤
│                      Data Layer                              │
│  (Repositories, Network, Database, DTOs, Mappers)           │
├─────────────────────────────────────────────────────────────┤
│                     Domain Layer                             │
│  (Use Cases, Repository Protocols, Domain Services)         │
├─────────────────────────────────────────────────────────────┤
│                    Entities Layer                            │
│  (Business Objects, Value Objects, Enterprise Rules)        │
└─────────────────────────────────────────────────────────────┘
         ↑ Dependencies point INWARD (toward Entities)

Module Organization

[AppName]/
├── Entities/           # Pure business objects, no dependencies
│   └── Models/
├── Domain/             # Use cases and repository protocols
│   ├── UseCases/
│   └── Interfaces/     # Repository protocols
├── Data/               # Concrete implementations
│   ├── Repositories/
│   ├── Network/
│   ├── Persistence/
│   └── DTOs/
├── Presentation/       # UI layer
│   ├── Views/
│   ├── ViewModels/
│   └── Coordinators/
└── DI/                 # Dependency injection container

Note: Replace [AppName] with your actual app name (e.g., NotesApp, TaskManager, PhotoEditor).

Data Flow

  1. User Action → View calls ViewModel method
  2. ViewModel → Invokes Use Case with request
  3. Use Case → Calls Repository protocol method
  4. Repository → Fetches/persists data, maps to Entity
  5. Response → Flows back: Repository → Use Case → ViewModel → View update

Tooling & Project Management

XcodeGen (Project Generation)

The Xcode project MUST be generated from a project.yml specification file using XcodeGen.

  • The .xcodeproj file MUST NOT be committed to version control
  • All project configuration MUST be defined in project.yml
  • Targets MUST be organized to reflect the layer structure (Entities, Domain, Data, Presentation)
  • Schemes MUST be defined for each testable target
  • Run xcodegen generate after any project.yml changes

Rationale: XcodeGen eliminates merge conflicts in .xcodeproj files, makes project structure declarative and reviewable, and ensures reproducible builds.

Carthage (Dependency Management)

External dependencies MUST be managed using Carthage.

  • All dependencies MUST be declared in Cartfile
  • Pinned versions MUST be tracked in Cartfile.resolved
  • Built frameworks MUST be stored in Carthage/Build/ (gitignored)
  • Dependencies MUST be linked as XCFrameworks for Apple Silicon compatibility

Platform-specific commands:

Platform Bootstrap Update
iOS carthage bootstrap --platform iOS --use-xcframeworks carthage update --platform iOS --use-xcframeworks
macOS carthage bootstrap --platform macOS --use-xcframeworks carthage update --platform macOS --use-xcframeworks
Multi-platform carthage bootstrap --use-xcframeworks carthage update --use-xcframeworks

Rationale: Carthage provides decentralized dependency management with pre-built binaries, reducing build times and avoiding CocoaPods' workspace modifications.

Project File Structure

project.yml              # XcodeGen project specification
Cartfile                 # Carthage dependencies
Cartfile.resolved        # Locked dependency versions
.gitignore               # Must include: *.xcodeproj, Carthage/Build/
[AppName]/
├── Entities/
├── Domain/
├── Data/
├── Presentation/
└── DI/
[AppName]Tests/
[AppName]UITests/

Build & Setup Commands

# Initial project setup (iOS)
carthage bootstrap --platform iOS --use-xcframeworks
xcodegen generate
open [AppName].xcodeproj

# Initial project setup (macOS)
carthage bootstrap --platform macOS --use-xcframeworks
xcodegen generate
open [AppName].xcodeproj

# After pulling changes
carthage bootstrap --platform [iOS|macOS] --use-xcframeworks
xcodegen generate

# Update dependencies
carthage update --platform [iOS|macOS] --use-xcframeworks

Note: Replace [AppName] with your app name and [iOS|macOS] with your target platform.

Platform Requirements

Deployment Targets

Minimum deployment targets for modern SwiftUI features:

Platform Minimum Version Rationale
iOS 17.0 navigationDestination(item:), @Observable
macOS 14.0 (Sonoma) navigationDestination(item:), @Observable
watchOS 10.0 Modern SwiftUI navigation
tvOS 17.0 Modern SwiftUI navigation
visionOS 1.0 All modern APIs available

Required for:

  • NavigationStack (iOS 16+ / macOS 13+)
  • navigationDestination(item:) (iOS 17+ / macOS 14+)
  • Modern SwiftUI navigation patterns
  • @Observable macro (iOS 17+ / macOS 14+)

Rationale: Modern SwiftUI navigation APIs provide type-safe, declarative navigation that aligns with Clean Architecture principles. Older APIs (NavigationView, NavigationLink with isActive) are deprecated and harder to test.

SwiftUI API Availability Checklist

Before using SwiftUI APIs, verify minimum platform version:

API iOS macOS Notes
NavigationStack 16.0 13.0 Replaces NavigationView
navigationDestination(for:) 16.0 13.0 Type-based navigation
navigationDestination(item:) 17.0 14.0 Optional binding navigation
@Observable 17.0 14.0 Replaces @ObservableObject
TextEditor 14.0 11.0 Basic text editing
.searchable 15.0 12.0 Search bar modifier
Inspector 17.0 14.0 Side panel inspector
ContentUnavailableView 17.0 14.0 Empty state views

Swift Concurrency Guidelines

@MainActor Isolation

All UI-related classes MUST be marked with @MainActor:

  • ViewModels: MUST be @MainActor (they update @Published properties bound to UI)
  • DependencyContainer: MUST be @MainActor (creates ViewModels)
  • Use Cases: SHOULD NOT be @MainActor (business logic is UI-independent)
  • Repositories: SHOULD NOT be @MainActor (data access is background work)
// ✅ Correct: ViewModel is MainActor-isolated
@MainActor
final class ItemListViewModel: ObservableObject {
    @Published var items: [Item] = []
}

// ✅ Correct: DependencyContainer matches ViewModel isolation
@MainActor
final class DependencyContainer: ObservableObject {
    func makeItemListViewModel() -> ItemListViewModel { ... }
}

// ✅ Correct: Use Case is not MainActor (can run on any actor)
final class GetItemsUseCase {
    func execute() async throws -> [Item] { ... }
}

Rationale: Swift's strict concurrency checking requires consistent actor isolation. If a ViewModel is @MainActor, any factory that creates it must also be @MainActor or use await.

Async/Await Patterns

  • Repository methods MUST be async throws
  • Use Case methods MUST be async throws
  • ViewModel methods that call Use Cases MUST use Task { } blocks
  • Never block the main thread with synchronous data access
// ✅ Correct: Async call in Task block
func loadItems() {
    Task {
        items = try await getItemsUseCase.execute()
    }
}

Development Workflow

Feature Implementation Order (TDD)

  1. Define Entities - Create pure business objects
  2. Write Entity Tests - Test computed properties, initializers, edge cases
  3. Define Repository Protocol - Specify data access interface
  4. Write Mock Repository - Create mock for testing
  5. Define Use Case - Specify the interface
  6. Write Use Case Tests - Test all execution paths with mock repository
  7. Implement Use Case - Make tests pass
  8. Write ViewModel Tests - Test all public methods with mock use cases
  9. Implement ViewModel - Make tests pass
  10. Implement Repository - Concrete data access
  11. Write Repository Integration Tests - Test against real storage
  12. Implement View - SwiftUI view bound to ViewModel
  13. Wire Dependencies - Register in DI container
  14. Run All Tests - Verify everything passes before review

Testing Strategy

Layer Test Type Dependencies Coverage Target
Entities Unit None 100%
Use Cases Unit Mock Repositories 100%
Repositories Integration Real Network/DB 80%
ViewModels Unit Mock Use Cases 90%
Views UI/Snapshot Mock ViewModels Critical paths

Test Execution Commands

# Run all tests
xcodebuild test -project [AppName].xcodeproj -scheme [AppName] -destination 'platform=iOS Simulator,name=iPhone 17'

# Run tests with coverage
xcodebuild test -project [AppName].xcodeproj -scheme [AppName] -destination 'platform=iOS Simulator,name=iPhone 17' -enableCodeCoverage YES

# Run specific test class
xcodebuild test -project [AppName].xcodeproj -scheme [AppName] -destination 'platform=iOS Simulator,name=iPhone 17' -only-testing:[AppName]Tests/CreateItemUseCaseTests

Code Review Checklist

  • Dependencies point inward only
  • No framework imports in Entities or Domain
  • All boundaries defined by protocols
  • Use Cases are single-purpose
  • ViewModels do not contain business logic
  • Repository implementations are in Data layer only
  • All tests pass (verified by CI or local run)
  • Test coverage meets targets (100% Use Cases, 90% ViewModels)
  • Edge cases tested (empty, nil, error, boundary)
  • No skipped or disabled tests

Governance

This constitution supersedes all other architectural practices for this project.

  • All pull requests MUST verify compliance with these principles
  • Violations MUST be documented and justified in the PR description
  • Amendments require: documentation of change, team review, migration plan for existing code
  • Use this constitution as the reference for architectural decisions

Version: 2.1.0 | Ratified: 2026-01-12 | Last Amended: 2026-01-12

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment