Skip to content

Instantly share code, notes, and snippets.

@kiarashvosough1999
Last active January 11, 2026 10:38
Show Gist options
  • Select an option

  • Save kiarashvosough1999/419496a5542e722f7fc7c2f946ae4b75 to your computer and use it in GitHub Desktop.

Select an option

Save kiarashvosough1999/419496a5542e722f7fc7c2f946ae4b75 to your computer and use it in GitHub Desktop.

Clean Architecture Documentation

📌 For AI/Cursor Context:

  • Project Type: Swift iOS app using Clean Architecture
  • Package Manager: Swift Package Manager (SPM) - MANDATORY for all features
  • Required External Dependencies: AugmentedSUICoordinator and Resolver - MUST be added as SPM dependencies
  • Critical Rule: Every component (Screen, Repository, UseCase) MUST be an SPM package with Package.swift
  • Dependency Rule: Dependencies point inward only (Presentation → UseCase → Repository → Data)
  • Quick Start: See 🚀 Quick Start section below
  • SPM Templates: See 📦 Package Templates section

Table of Contents

Quick Reference

Core Concepts

Implementation Guides

Advanced Topics


🎯 Key Concepts (Quick Reference)

For AI/Cursor implementing features:

  1. SPM Packages are MANDATORY - Every component (Screen, Repository, UseCase, Coordinator) must be an SPM package with Package.swift
  2. API/Impl Pattern - Each component has two packages: {Component}API (protocols) and {Component}Impl (implementation)
  3. Dependency Direction - Always points inward: Presentation → UseCase → Repository → Data
  4. Use Cases are the Center - They define repository interfaces; repositories implement them
  5. Implementation Order - Bottom-up: UseCaseAPI → UseCaseImpl → RepositoryAPI → RepositoryImpl → ScreenAPI → ScreenImpl
  6. Protocol-Based - All inter-layer communication uses protocols, never concrete types
  7. ViewModels are Thin - They delegate to UseCases and format for UI only
  8. Coordinator Pattern - Navigation is handled by coordinators using SUICoordinator library
  9. Resolver for DI - All Impl packages use Resolver for dependency injection (not API packages)
  10. SUICoordinator for Navigation - All Coordinator packages and ScreenAPI packages use SUICoordinator

Quick Links:


Overview

This project follows Clean Architecture as defined by Robert C. Martin (Uncle Bob). Clean Architecture is not about "layers" or "MVVM best practices" — it's about dependency rules and boundaries.

Core Principle: The Dependency Rule

Source code dependencies must point inward. Nothing in an inner circle can know anything at all about something in an outer circle.

Clean Architecture Layers (Outer to Inner)

  1. Frameworks & Drivers (Outermost)

    • UI frameworks (SwiftUI, UIKit)
    • Web frameworks (HTTP clients, networking)
    • Database frameworks (CoreData, SQLite)
    • Device services (Keychain, UserDefaults)
    • Image caching services
  2. Interface Adapters

    • ViewModels (convert UseCase outputs to UI format)
    • Presenters (format data for display)
    • Controllers (handle user input)
    • Gateways (convert data formats)
  3. Use Cases (Interactors) (Application Business Rules)

    • This is the center of the architecture
    • Encapsulate all business rules
    • Define repository interfaces (input boundaries)
    • Define output boundaries (presenters/result models)
    • Must not know about UI, frameworks, or data sources
  4. Entities (Enterprise Business Rules)

    • Pure business objects
    • No framework dependencies
    • No persistence concerns
    • No UI concerns

What This Means

  • Use Cases are the center — everything else exists to serve them
  • Frameworks are details — the architecture doesn't know about SwiftUI, Combine, or URLSession
  • Dependencies point inward — inner layers define interfaces, outer layers implement them
  • Business rules live in Use Cases — nowhere else

📦 Required External Dependencies

This project requires two external Swift Package Manager (SPM) dependencies that must be fetched and added to the project before implementing any features:

1. AugmentedSUICoordinator

Repository: https://github.com/kiarashvosough1999/AugmentedSUICoordinator

Purpose: Navigation coordinator library for SwiftUI. Provides the coordinator pattern implementation used throughout the project for managing navigation flows.

Required For:

  • All Coordinator packages (both API and Impl)
  • ScreenAPI packages that define coordinator protocols

How to Add:

  1. In Xcode: FileAdd Package Dependencies...
  2. Enter the repository URL: https://github.com/kiarashvosough1999/AugmentedSUICoordinator.git
  3. Select the version/branch as needed
  4. Add to your target

Usage in Package.swift:

dependencies: [
    .package(url: "https://github.com/kiarashvosough1999/AugmentedSUICoordinator.git", from: "1.0.0")
]

See Section 4.6.2: Adding SUICoordinator to SPM Packages for detailed integration instructions.

2. Resolver

Repository: https://github.com/hmlongco/Resolver

Purpose: Dependency injection framework. Used for managing and resolving dependencies throughout the application.

Required For:

  • All Impl packages (UseCaseImpl, RepositoryImpl, ScreenImpl, CoordinatorImpl)
  • NOT required for API packages

How to Add:

  1. In Xcode: FileAdd Package Dependencies...
  2. Enter the repository URL: https://github.com/hmlongco/Resolver.git
  3. Select version 1.5.1 or later
  4. Add to your target

Usage in Package.swift:

dependencies: [
    .package(url: "https://github.com/hmlongco/Resolver.git", from: "1.5.1")
]

See Section 5.1: Adding Resolver to SPM Packages for detailed integration instructions.

Important Notes

  • ⚠️ These dependencies are mandatory and must be added to the project before implementing features
  • ⚠️ Each SPM package in the project will reference these dependencies in their Package.swift files
  • ⚠️ The main Xcode project must have these packages added as dependencies first
  • ✅ Once added to the main project, individual SPM packages can reference them in their Package.swift files

🚀 Quick Start: Implementing a Feature

When implementing ANY feature, follow these steps in order:

Step 1: Create SPM Package Structure

Every component MUST be an SPM package with Package.swift. See CRITICAL REQUIREMENT section for detailed steps.

Minimum structure for a feature:

Features/{Feature}/
├── Presentation/{Screen}/{Screen}API/ (with Package.swift)
├── Presentation/{Screen}/{Screen}Impl/ (with Package.swift)
├── Repositories/{Repository}/{Repository}API/ (with Package.swift)
├── Repositories/{Repository}/{Repository}Impl/ (with Package.swift)
├── UseCases/{UseCase}/{UseCase}API/ (with Package.swift)
└── UseCases/{UseCase}/{UseCase}Impl/ (with Package.swift)

Step 2: Implementation Order (Bottom-Up)

  1. UseCaseAPI - Define protocol and repository protocol
  2. UseCaseImpl - Implement use case
  3. RepositoryAPI - Define data protocols
  4. RepositoryImpl - Implement repository
  5. ScreenAPI - Define screen protocol
  6. ScreenImpl - Implement view and view model

Step 3: Key Rules

  • ✅ Use Cases define Repository interfaces (not vice versa)
  • ✅ Dependencies point inward only
  • ✅ Every component is an SPM package
  • ✅ API packages contain only protocols, no implementation
  • ❌ Never put implementation in API packages
  • ❌ Never make Impl packages depend on other Impl packages

Step 4: Add Resolver to Impl Packages

Add Resolver dependency to all Impl packages (UseCaseImpl, RepositoryImpl, ScreenImpl) in their Package.swift files. Do NOT add Resolver to API packages.

See Section 5.1: Adding Resolver to SPM Packages for detailed instructions.

Step 5: Add SUICoordinator to Coordinator and ScreenAPI Packages

Add SUICoordinator dependency to:

  • All Coordinator packages (both API and Impl)
  • ScreenAPI packages that define coordinator protocols

See Section 4.6.2: Adding SUICoordinator to SPM Packages for detailed instructions.

Step 6: Create Coordinators (if needed)

If your feature requires navigation, create coordinator packages following the coordinator pattern. Each coordinator consists of:

  • {Feature}CoordinatorAPI - Defines coordinator protocol
  • {Feature}CoordinatorImpl - Implements coordinator using SUICoordinator

See Section 4.6: Coordinator Pattern for detailed implementation guide.

Step 7: Register Dependencies

Add all implementations to the dependency injection container in PayRed/DI/DIRegisterar.swift using Resolver's registration API.

See Section 5.2: Registration Pattern for detailed examples.


Project Structure: SPM Feature Modules

This project uses Swift Package Manager (SPM) to organize code into feature modules. Each feature is a self-contained module with clear boundaries, enabling:

  • Modularity: Features can be developed, tested, and maintained independently
  • Dependency Management: Clear compile-time dependencies between modules
  • Encapsulation: Internal implementation details are hidden behind API packages
  • Reusability: Features can be reused across different projects
  • Testability: Each module can be tested in isolation

Why SPM for Feature Modules?

SPM is essential for this architecture because:

  1. Feature Isolation: Each feature is a separate SPM package, preventing accidental dependencies between features
  2. API/Impl Separation: SPM packages enforce the separation between public APIs (protocols) and private implementations
  3. Dependency Graph: SPM's dependency resolution ensures the dependency rule is enforced at compile time
  4. Incremental Compilation: Only changed modules need to be recompiled
  5. Module Boundaries: SPM packages create explicit boundaries that prevent architectural violations

⚠️ CRITICAL REQUIREMENT: SPM Packages Are Mandatory

When implementing ANY feature in this project, you MUST create SPM packages. This is NOT optional.

Every Component Must Be an SPM Package

Every single component in a feature (Screen, Repository, UseCase) MUST be organized as separate SPM packages with:

  • ✅ A Package.swift file in each API and Impl directory
  • ✅ Proper Sources/ directory structure
  • ✅ Correct package dependencies configured

DO NOT:

  • ❌ Create Swift files without a Package.swift file
  • ❌ Put multiple components in a single package
  • ❌ Skip creating API/Impl package separation
  • ❌ Create folders without SPM package structure

Step-by-Step: Creating a New Feature with SPM Packages

When implementing a new feature (e.g., PaymentFeature), follow these steps:

1. Create Feature Directory Structure

Features/PaymentFeature/
├── Presentation/
│   └── PaymentScreen/
│       ├── PaymentScreenAPI/      # Create this directory
│       └── PaymentScreenImpl/     # Create this directory
├── Repositories/
│   └── PaymentRepository/
│       ├── PaymentRepositoryAPI/  # Create this directory
│       └── PaymentRepositoryImpl/ # Create this directory
└── UseCases/
    └── PaymentUseCase/
        ├── PaymentUseCaseAPI/     # Create this directory
        └── PaymentUseCaseImpl/    # Create this directory

2. Create SPM Package for Each API Component

For each API package (e.g., PaymentUseCaseAPI):

  1. Create the directory structure:

    mkdir -p Features/PaymentFeature/UseCases/PaymentUseCase/PaymentUseCaseAPI/Sources/PaymentUseCaseAPI
  2. Create Package.swift file in PaymentUseCaseAPI/:

    // swift-tools-version: 5.9
    import PackageDescription
    
    let package = Package(
        name: "PaymentUseCaseAPI",
        platforms: [
            .iOS(.v16)
        ],
        products: [
            .library(
                name: "PaymentUseCaseAPI",
                targets: ["PaymentUseCaseAPI"]
            ),
        ],
        dependencies: [
            // Only API packages, no Impl packages
        ],
        targets: [
            .target(
                name: "PaymentUseCaseAPI",
                dependencies: []
            ),
        ]
    )
  3. Create protocol files in Sources/PaymentUseCaseAPI/:

    • PaymentUseCaseProtocol.swift
    • PaymentUseCaseRepositoryProtocol.swift
    • PaymentUseCaseNameSpace.swift

3. Create SPM Package for Each Impl Component

For each Impl package (e.g., PaymentUseCaseImpl):

  1. Create the directory structure:

    mkdir -p Features/PaymentFeature/UseCases/PaymentUseCase/PaymentUseCaseImpl/Sources/PaymentUseCaseImpl
  2. Create Package.swift file in PaymentUseCaseImpl/:

    // swift-tools-version: 5.9
    import PackageDescription
    
    let package = Package(
        name: "PaymentUseCaseImpl",
        platforms: [
            .iOS(.v16)
        ],
        products: [
            .library(
                name: "PaymentUseCaseImpl",
                targets: ["PaymentUseCaseImpl"]
            ),
        ],
        dependencies: [
            .package(path: "../PaymentUseCaseAPI"),  // Depends on API package
        ],
        targets: [
            .target(
                name: "PaymentUseCaseImpl",
                dependencies: [
                    .product(name: "PaymentUseCaseAPI", package: "PaymentUseCaseAPI")
                ]
            ),
        ]
    )
  3. Create implementation files in Sources/PaymentUseCaseImpl/:

    • PaymentUseCaseImpl.swift

4. Repeat for All Components

You must create SPM packages for:

  • ✅ Every Screen (ScreenAPI + ScreenImpl)
  • ✅ Every Repository (RepositoryAPI + RepositoryImpl)
  • ✅ Every UseCase (UseCaseAPI + UseCaseImpl)

Total packages per feature: 6 SPM packages minimum (3 API + 3 Impl)

5. Add Packages to Xcode Project

After creating all SPM packages:

  1. Open Xcode project
  2. File → Add Package Dependencies...
  3. Add Local packages for each created package
  4. Or add them via project settings → Package Dependencies

Quick Reference: Package.swift Templates

API Package Template:

// swift-tools-version: 5.9
import PackageDescription

let package = Package(
    name: "{Component}API",
    platforms: [.iOS(.v16)],
    products: [
        .library(name: "{Component}API", targets: ["{Component}API"])
    ],
    dependencies: [
        // Only other API packages
    ],
    targets: [
        .target(name: "{Component}API", dependencies: [])
    ]
)

Impl Package Template:

// swift-tools-version: 6.2
import PackageDescription

let package = Package(
    name: "{Component}Impl",
    platforms: [.iOS(.v17)],
    products: [
        .library(name: "{Component}Impl", targets: ["{Component}Impl"])
    ],
    dependencies: [
        .package(path: "../{Component}API"),
        .package(
            url: "https://github.com/hmlongco/Resolver.git",
            from: "1.5.1"
        ),
    ],
    targets: [
        .target(
            name: "{Component}Impl",
            dependencies: [
                .product(name: "{Component}API", package: "{Component}API"),
                .product(name: "Resolver", package: "Resolver")
            ]
        )
    ]
)

⚠️ Important Notes:

  • Resolver is REQUIRED for all Impl packages
  • Resolver is NOT added to API packages
  • Use version 1.5.1 or later for Resolver
  • Always add Resolver to both dependencies and target dependencies arrays

Verification Checklist

Before considering a feature complete, verify:

  • Every API directory has a Package.swift file
  • Every Impl directory has a Package.swift file
  • All Package.swift files have correct dependencies
  • Resolver is NOT added to API packages
  • Resolver IS added to all Impl packages
  • SUICoordinator IS added to all Coordinator packages (API and Impl)
  • SUICoordinator IS added to ScreenAPI packages that define coordinator protocols
  • All packages are added to Xcode project
  • Project builds successfully with all SPM packages
  • No Swift files exist outside of SPM package structure
  • All implementations are registered in DIRegisterar.swift
  • Coordinators are registered in DIRegisterar.swift (if applicable)

Package Structure Pattern

⚠️ REMINDER: Every component MUST be an SPM package with a Package.swift file. See "CRITICAL REQUIREMENT: SPM Packages Are Mandatory" section above for implementation steps.

Each feature follows the API/Impl pattern with separate SPM packages:

Features/{Feature}/
├── Presentation/
│   └── {Screen}/
│       ├── {Screen}API/              # SPM Package: Public protocols & dependencies
│       │   ├── Package.swift
│       │   └── Sources/
│       │       └── {Screen}API/
│       │           ├── {Screen}Protocol.swift
│       │           └── {Screen}Dependencies.swift
│       └── {Screen}Impl/              # SPM Package: Implementation
│           ├── Package.swift
│           └── Sources/
│               └── {Screen}Impl/
│                   ├── {Screen}View.swift
│                   └── {Screen}ViewModel.swift
│
├── Repositories/
│   └── {Repository}/
│       ├── {Repository}API/           # SPM Package: Data protocols & interfaces
│       │   ├── Package.swift
│       │   └── Sources/
│       │       └── {Repository}API/
│       │           ├── {Repository}RemoteDataProtocol.swift
│       │           ├── {Repository}DataBaseDataProtocol.swift
│       │           └── {Repository}KeyChainDataProtocol.swift
│       └── {Repository}Impl/          # SPM Package: Repository implementation
│           ├── Package.swift
│           └── Sources/
│               └── {Repository}Impl/
│                   └── {Repository}Impl.swift
│
└── UseCases/
    └── {UseCase}/
        ├── {UseCase}API/              # SPM Package: UseCase protocol & repository protocol
        │   ├── Package.swift
        │   └── Sources/
        │       └── {UseCase}API/
        │           ├── {UseCase}Protocol.swift
        │           ├── {UseCase}RepositoryProtocol.swift
        │           └── {UseCase}NameSpace.swift
        └── {UseCase}Impl/             # SPM Package: UseCase implementation
            ├── Package.swift
            └── Sources/
                └── {UseCase}Impl/
                    └── {UseCase}Impl.swift

API vs Impl Packages

API Packages ({Feature}API):

  • Purpose: Define public interfaces (protocols) and data models
  • Visibility: Public - can be imported by other modules
  • Contains:
    • Protocol definitions
    • Domain models (namespaces)
    • Dependency interfaces
    • No implementation code
  • Dependencies: Only other API packages (if needed)

Impl Packages ({Feature}Impl):

  • Purpose: Implement the protocols defined in API packages
  • Visibility: Internal - implementation details are hidden
  • Contains:
    • Concrete implementations
    • Business logic
    • Framework-specific code
  • Dependencies:
    • Corresponding API package (to implement protocols)
    • Other API packages (to use their interfaces)
    • Never depends on other Impl packages directly

Example: Login Feature Package Structure

Features/Login/
├── Presentation/
│   └── LoginScreen/
│       ├── LoginScreenAPI/
│       │   ├── Package.swift
│       │   └── Sources/LoginScreenAPI/
│       │       ├── LoginScreenProtocol.swift
│       │       └── LoginScreenDependencies.swift
│       └── LoginScreenImpl/
│           ├── Package.swift
│           └── Sources/LoginScreenImpl/
│               ├── LoginScreenView.swift
│               └── LoginScreenViewModel.swift
│
├── Repositories/
│   └── LoginRepository/
│       ├── LoginRepositoryAPI/
│       │   ├── Package.swift
│       │   └── Sources/LoginRepositoryAPI/
│       │       ├── LoginRepositoryRemoteDataProtocol.swift
│       │       ├── LoginRepositoryDataBaseDataProtocol.swift
│       │       └── LoginRepositoryNameSpace.swift
│       └── LoginRepositoryImpl/
│           ├── Package.swift
│           └── Sources/LoginRepositoryImpl/
│               └── LoginRepositoryImpl.swift
│
└── UseCases/
    └── LoginUseCase/
        ├── LoginUseCaseAPI/
        │   ├── Package.swift
        │   └── Sources/LoginUseCaseAPI/
        │       ├── LoginUseCaseProtocol.swift
        │       ├── LoginUseCaseRepositoryProtocol.swift
        │       └── LoginUseCaseNameSpace.swift
        └── LoginUseCaseImpl/
            ├── Package.swift
            └── Sources/LoginUseCaseImpl/
                └── LoginUseCaseImpl.swift

Package.swift Example

LoginUseCaseAPI/Package.swift:

// swift-tools-version: 5.9
import PackageDescription

let package = Package(
    name: "LoginUseCaseAPI",
    platforms: [
        .iOS(.v16)
    ],
    products: [
        .library(
            name: "LoginUseCaseAPI",
            targets: ["LoginUseCaseAPI"]
        ),
    ],
    dependencies: [
        // Only API packages, no Impl packages
    ],
    targets: [
        .target(
            name: "LoginUseCaseAPI",
            dependencies: []
        ),
    ]
)

LoginUseCaseImpl/Package.swift:

// swift-tools-version: 5.9
import PackageDescription

let package = Package(
    name: "LoginUseCaseImpl",
    platforms: [
        .iOS(.v16)
    ],
    products: [
        .library(
            name: "LoginUseCaseImpl",
            targets: ["LoginUseCaseImpl"]
        ),
    ],
    dependencies: [
        .package(path: "../LoginUseCaseAPI"),  // Depends on API package
    ],
    targets: [
        .target(
            name: "LoginUseCaseImpl",
            dependencies: [
                .product(name: "LoginUseCaseAPI", package: "LoginUseCaseAPI")
            ]
        ),
    ]
)

Data Layer Packages

Data layer implementations are organized as SPM packages that implement protocols from RepositoryAPI packages:

Data/
├── MobileGatewayService/
│   └── MobileGatewayServiceImpl/
│       ├── Package.swift
│       └── Sources/
│           └── MobileGatewayServiceImpl/
│               ├── NetworkService.swift
│               ├── NetworkService+LoginRepository.swift
│               └── NetworkService+CardListRepository.swift
│
├── DataBase/
│   └── DataBase/
│       ├── Package.swift
│       └── Sources/
│           └── DataBase/
│               ├── Database.swift
│               ├── Database+LoginRepository.swift
│               └── Database+CardListRepository.swift
│
├── Keychain/
│   └── KeyChainDataImpl/
│       ├── Package.swift
│       └── Sources/
│           └── KeyChainDataImpl/
│               └── KeychainService.swift
│
└── ImageCache/
    └── ImageCacheImpl/
        ├── Package.swift
        └── Sources/
            └── ImageCacheImpl/
                └── ImageCacheImpl.swift

Dependency Rules in SPM

SPM enforces the dependency rule at compile time:

  1. API packages can only depend on other API packages
  2. Impl packages depend on their corresponding API package and other API packages
  3. Impl packages never depend on other Impl packages directly
  4. Data layer packages implement protocols from RepositoryAPI packages

Benefits of SPM Feature Modules

Compile-time Safety: SPM prevents circular dependencies and architectural violations
Clear Boundaries: Package boundaries make layer separation explicit
Incremental Builds: Only changed packages are recompiled
Module Isolation: Features can be developed independently
Dependency Graph: SPM shows the dependency structure clearly
Reusability: Features can be extracted and reused in other projects
Testing: Each package can have its own test target


Clean Architecture Dependency Rule

The Dependency Rule (Non-Negotiable): Source code dependencies can only point inward. Nothing in an inner circle can know anything about something in an outer circle.

Clean Architecture Layers

graph TB
    subgraph Frameworks["Frameworks & Drivers<br/>(Outermost Layer)"]
        SwiftUI["SwiftUI Views"]
        ImageCache["Image Cache"]
        URLSession["URLSession/HTTP"]
        CoreData["CoreData"]
        Keychain["Keychain"]
    end
    
    subgraph Adapters["Interface Adapters"]
        ViewModel["ViewModels<br/>(State Mapping)"]
        Presenter["Presenters<br/>(Data Formatting)"]
        Controller["Controllers<br/>(Input Handling)"]
        Gateway["Gateways<br/>(Data Conversion)"]
    end
    
    subgraph UseCases["Use Cases<br/>(Application Business Rules)<br/>THE CENTER"]
        UseCaseImpl["UseCase Implementation"]
        UseCaseProtocol["UseCase Protocol<br/>(Input Boundary)"]
        OutputBoundary["Output Boundary<br/>(Result Models)"]
        RepoInterface["Repository Interface<br/>(Defined by UseCase)"]
    end
    
    subgraph Entities["Entities<br/>(Enterprise Business Rules)"]
        DomainEntity["Domain Entities<br/>(Pure Business Objects)"]
    end
    
    Frameworks -->|depends on| Adapters
    Adapters -->|depends on| UseCases
    UseCases -->|depends on| Entities
    
    style Frameworks fill:#ffcdd2
    style Adapters fill:#fff9c4
    style UseCases fill:#c8e6c9
    style Entities fill:#bbdefb
Loading

Implementation-Specific Architecture Diagram

Note: The diagram below shows how Clean Architecture is implemented in this project. The diagram is also available as a Mermaid file: docs/diagrams/architecture-diagram.mmd

graph TB
    subgraph Presentation["Presentation Layer"]
        View["View<br/>(SwiftUI)"]
        ViewModel["ViewModel<br/>(Observable)"]
        ScreenAPI["ScreenAPI<br/>(Protocol)"]
        ImageCache["Image Cache<br/>(Caching)"]
        
        View --> ViewModel
        ViewModel --> ScreenAPI
        ViewModel -.-> ImageCache
    end
    
    subgraph UseCase["UseCase Layer"]
        UseCaseImpl["UseCaseImpl<br/>(Business Logic)"]
        UseCaseAPI["UseCaseAPI<br/>(Protocol)"]
        RepoProtocol["RepositoryProtocol<br/>(from UseCaseAPI)"]
        
        UseCaseImpl -.implements.-> UseCaseAPI
        UseCaseImpl -->|uses| RepoProtocol
    end
    
    subgraph Repository["Repository Layer"]
        RepoImpl["RepositoryImpl<br/>(Orchestrates Data Sources)"]
        DataProtocols["DataProtocols<br/>(from RepositoryAPI)"]
        
        RepoImpl -.implements.-> RepoProtocol
        RepoImpl -->|uses| DataProtocols
    end
    
    subgraph Data["Data Layer"]
        RemoteData["Remote Data<br/>(API Calls)"]
        Database["Database<br/>(CoreData)"]
        Keychain["Keychain<br/>(Secure)"]
        
        RemoteData -.implements.-> DataProtocols
        Database -.implements.-> DataProtocols
        Keychain -.implements.-> DataProtocols
    end
    
    Presentation -->|uses| UseCase
    UseCase -->|implemented by| Repository
    Repository -->|uses| Data
    
    style Presentation fill:#e1f5ff
    style UseCase fill:#fff4e1
    style Repository fill:#e8f5e9
    style Data fill:#fce4ec
Loading

Architectural Rules (Non-Negotiable)

These rules are hard constraints that must never be violated. They are the foundation of Clean Architecture.

Rule 1: Dependency Direction

Allowed: Outer layers depend on inner layers
Forbidden: Inner layers depend on outer layers

Examples of violations:

  • UseCase importing SwiftUI
  • UseCase importing URLSession
  • Domain entity conforming to Codable
  • UseCase knowing about ViewModels

Rule 2: Use Cases Are The Center

Allowed: Use Cases contain all business rules
Forbidden: Business rules in ViewModels, Repositories, or Data layers

What this means:

  • Validation logic → Use Case
  • Business decisions → Use Case
  • State machine logic → Use Case
  • ViewModels only map/format data

Rule 3: Framework Independence

Allowed: Frameworks in outermost layer only
Forbidden: Framework types crossing boundaries

Forbidden imports in Use Cases:

  • import SwiftUI
  • import Combine (unless for pure reactive interfaces)
  • import Foundation networking types
  • import CoreData

Rule 4: Repository Interfaces Belong to Use Cases

Allowed: Use Cases define repository interfaces
Forbidden: Data layer defining what Use Cases need

Correct pattern:

// UseCaseAPI defines the interface
protocol LoginUseCaseRepositoryProtocol {
    func login(email: String) async throws -> LoginResponse
}

// RepositoryImpl implements it
// Data implementations provide the data

Rule 5: Domain Models Are Pure

Allowed: Pure Swift types with business logic
Forbidden: Framework pollution in domain models

Forbidden in domain entities:

  • Codable conformance
  • Database IDs
  • API field names
  • Framework-specific types

Rule 6: ViewModels Are Interface Adapters Only

Allowed: State mapping, event forwarding, UI formatting
Forbidden: Business logic, validation, decision-making

ViewModel responsibilities:

  • Convert UseCase outputs to UI format
  • Forward user events to UseCases
  • Format data for display
  • Manage UI-specific state (loading indicators, etc.)

Not ViewModel responsibilities:

  • Validating business rules
  • Making business decisions
  • Fetching data directly
  • Containing business logic

Layer Descriptions (Clean Architecture Mapping)

Frameworks & Drivers Layer (Outermost)

Purpose: Framework details and device services

Contains:

  • SwiftUI Views (UI framework)
  • Image Cache (caching service)
  • Network clients (URLSession, Alamofire)
  • Database frameworks (CoreData)
  • Keychain services
  • UserDefaults

Rules:

  • Can depend on Interface Adapters
  • Must not be referenced by inner layers
  • Framework-specific implementations only

Interface Adapters Layer

Purpose: Convert data between Use Cases and Frameworks

Contains:

  • ViewModels (convert UseCase outputs to UI)
  • Presenters (format data for display)
  • Controllers (handle user input)
  • Gateways (convert between data formats)

Rules:

  • Depends on Use Cases (inner)
  • Depends on Frameworks (outer)
  • No business logic
  • Only data transformation

Use Cases Layer (THE CENTER)

Purpose: Application business rules

Contains:

  • UseCase implementations
  • UseCase protocols (input boundaries)
  • Repository interfaces (defined here)
  • Output boundaries (result models)
  • Business validation
  • State machines
  • Business decisions

Rules:

  • This is the center — everything serves Use Cases
  • Depends only on Entities (inner)
  • Defines interfaces for outer layers
  • Contains ALL business rules
  • Framework-independent

Entities Layer (Innermost)

Purpose: Enterprise business rules

Contains:

  • Pure domain models
  • Business value objects
  • Domain logic

Rules:

  • No dependencies on other layers
  • Pure Swift types
  • No framework types
  • No persistence concerns

1. Frameworks & Drivers Layer (Data Sources)

Clean Architecture Position: Outermost layer (Frameworks & Drivers)

Purpose: Framework-specific implementations for data persistence and retrieval. These are details that the architecture doesn't care about.

Important: This layer implements interfaces defined by Use Cases (through Repository interfaces). The Use Cases don't know these implementations exist.

1.1 Remote Data (API)

Location: Data/{ServiceName}/{ServiceName}Impl/

Purpose: Handles all network communication with the backend API.

Key Components:

  • Service Implementation: Main service class that performs HTTP requests (e.g., using Alamofire, URLSession)
  • Protocol Conformance: Implements *RepositoryRemoteDataProtocol interfaces from RepositoryAPI package
  • Features:
    • Request/response handling
    • Authentication interceptors
    • SSL certificate pinning configuration (optional)
    • Error handling and logging

Example Implementation:

// NetworkService+LoginRepository.swift
import LoginRepositoryAPI  // Contains LoginRepositoryRemoteDataProtocol

// Data implementation IMPLEMENTS DataProtocol from RepositoryAPI
extension NetworkService: LoginRepositoryRemoteDataProtocol {  // From LoginRepositoryAPI
    public func login(email: String) async throws -> LoginRepositoryNameSpace.LoginOrRegisterRemoteResult {
        let request = RequestBuilder(
            endpoint: "/login",
            method: .post,
            body: LoginRequest(email: email)
        )
        
        let result: ResponseModel<LoginResponse> = 
            try await performRequest(request: request)
        
        return mapToLoginResult(result.payload)
    }
}

Characteristics:

  • Each feature has its own extension file (e.g., NetworkService+LoginRepository.swift)
  • Implements DataProtocols from RepositoryAPI package
  • Uses request builders for constructing API calls
  • Handles request/response transformation
  • Returns domain-specific entities defined in Repository namespace

1.2 Database

Location: Data/Database/{DatabaseImpl}/

Purpose: Provides local persistence (e.g., CoreData, SQLite, Realm).

Key Components:

  • Database Manager: Main database stack manager
  • Entities: Database managed objects (e.g., LoginStateEntity, ProfileEntity)
  • Protocol Conformance: Implements *RepositoryDataBaseDataProtocol interfaces from RepositoryAPI package

Example Implementation (CoreData):

// Database+LoginRepository.swift
import LoginRepositoryAPI  // Contains LoginRepositoryDataBaseDataProtocol

// Data implementation IMPLEMENTS DataProtocol from RepositoryAPI
extension Database: LoginRepositoryDataBaseDataProtocol {  // From LoginRepositoryAPI
    public func createUserState(with email: String, temporaryToken: String?) async throws {
        try await context.perform(schedule: .enqueued) { [context] in
            let entity = LoginStateEntity(context: context)
            entity.email = email
            entity.state = .idle
            entity.createdAt = Date()
            entity.temporaryToken = temporaryToken
            try context.save()
        }
    }
    
    public func doesUserStateExist(email: String) async throws -> Bool {
        try await context.perform(schedule: .enqueued) { [context] in
            let request: NSFetchRequest<LoginStateEntity> = LoginStateEntity.fetchRequest()
            request.predicate = NSPredicate(format: "email ==[c] %@", email)
            request.fetchLimit = 1
            let count = try context.count(for: request)
            return count > 0
        }
    }
}

Characteristics:

  • All database operations are async/await
  • Uses thread-safe database access patterns
  • Each feature has its own extension file
  • Returns domain-specific entities

1.3 Keychain

Location: Data/Keychain/{KeychainImpl}/

Purpose: Stores sensitive data securely (tokens, PINs, biometric data).

Protocol Conformance: Implements *RepositoryKeyChainDataProtocol interfaces from RepositoryAPI package

Example Usage:

  • Storing app PIN
  • Storing authentication tokens
  • Storing biometric state

1.4 UserDefaults

Location: Data/UserDefaults/{UserDefaultsImpl}/

Purpose: Stores simple key-value preferences.

Protocol Conformance: Implements *RepositoryUserDefaultsDataProtocol interfaces from RepositoryAPI package


2. Repository Pattern (Interface Adapters)

Clean Architecture Position: Interface Adapters layer

Critical Understanding:

  • Repository interfaces are defined in Use Cases (inner layer)
  • Repository implementations live in Interface Adapters (outer layer)
  • Data sources are in Frameworks & Drivers (outermost layer)

Key Principle:

  • Use Cases define repository interfaces (input boundaries)
  • RepositoryImpl implements RepositoryProtocol from UseCaseAPI (defined by Use Case)
  • RepositoryImpl uses (depends on) DataProtocols from RepositoryAPI
  • Data implementations implement DataProtocols from RepositoryAPI

Important: Repositories do NOT "fetch data" — they abstract how data is obtained. The Use Case requests data, the Repository provides it through the abstraction.

2.1 Repository Structure (SPM Packages)

📋 IMPLEMENTATION INSTRUCTIONS FOR AI/CURSOR:

  1. Create TWO SPM packages: {Repository}API and {Repository}Impl
  2. Each MUST have a Package.swift file
  3. RepositoryAPI defines data protocols (RemoteDataProtocol, DataBaseDataProtocol, etc.)
  4. RepositoryImpl implements RepositoryProtocol from UseCaseAPI package
  5. RepositoryImpl depends on data protocols from RepositoryAPI package
  6. See CRITICAL REQUIREMENT section for Package.swift templates

Each repository is organized as separate SPM packages following this structure:

Features/{Feature}/Repositories/{Repository}/
├── {Repository}API/                          # SPM Package
│   ├── Package.swift
│   └── Sources/
│       └── {Repository}API/
│           ├── {Repository}RemoteDataProtocol.swift    # Data protocol (defined here, Data Layer implements)
│           ├── {Repository}DataBaseDataProtocol.swift  # Data protocol (defined here, Data Layer implements)
│           ├── {Repository}KeyChainDataProtocol.swift  # Data protocol (if needed)
│           └── {Repository}NameSpace.swift             # Domain models
└── {Repository}Impl/                        # SPM Package
    ├── Package.swift
    └── Sources/
        └── {Repository}Impl/
            └── {Repository}Impl.swift                   # Implementation

Features/{Feature}/UseCases/{UseCase}/
└── {UseCase}API/                            # SPM Package
    ├── Package.swift
    └── Sources/
        └── {UseCase}API/
            └── {UseCase}RepositoryProtocol.swift       # Repository protocol (defined here, RepositoryImpl implements)

Important:

  • RepositoryProtocol is defined in UseCaseAPI SPM package
  • DataProtocols are defined in RepositoryAPI SPM package
  • Each API and Impl is a separate SPM package with its own Package.swift

2.2 Repository Implementation Pattern

Example: LoginRepository

// LoginRepositoryImpl.swift
import LoginRepositoryAPI  // Contains DataProtocols (RemoteDataProtocol, DataBaseDataProtocol)
import LoginUseCaseAPI     // Contains RepositoryProtocol (LoginUseCaseRepositoryProtocol)

public final class LoginRepositoryImpl: @unchecked Sendable {
    
    // Dependencies: RepositoryImpl USES (depends on) Data Protocols from RepositoryAPI
    private let remoteData: LoginRepositoryRemoteDataProtocol  // From LoginRepositoryAPI
    private let datbaseData: LoginRepositoryDataBaseDataProtocol  // From LoginRepositoryAPI

    public init() {
        self.remoteData = DependencyContainer.resolve(LoginRepositoryRemoteDataProtocol.self)
        self.datbaseData = DependencyContainer.resolve(LoginRepositoryDataBaseDataProtocol.self)
    }
}

// MARK: - LoginUseCaseRepositoryProtocol
// RepositoryImpl IMPLEMENTS the protocol from UseCaseAPI (what UseCase depends on)
extension LoginRepositoryImpl: LoginUseCaseRepositoryProtocol {  // From LoginUseCaseAPI

    public func login(email: String) async throws -> LoginUseCaseNameSpace.LoginResponse {
        // 1. Call remote API (using Data Protocol)
        let remoteData = try await remoteData.login(email: email)

        // 2. Transform remote data to UseCase domain model
        return if let temporaryToken = remoteData.token, case .verifyEmail = remoteData.step {
            .userNotExist(temporaryToken: temporaryToken)
        } else {
            .userExist
        }
    }

    public func saveUserLoginState(email: String, temporaryToken: String?, userExist: Bool) async throws {
        // 1. Check if state exists (using Data Protocol)
        if try await datbaseData.doesUserStateExist(email: email) == false {
            try await datbaseData.createUserState(with: email, temporaryToken: temporaryToken)
        }
        
        // 2. Update state based on user existence
        if userExist {
            try await datbaseData.updateUserLoginStateToShouldEnterPin(
                email: email, 
                temporaryToken: temporaryToken, 
                userExist: userExist
            )
        } else {
            try await datbaseData.updateUserLoginStateToShouldVerify(
                email: email, 
                temporaryToken: temporaryToken, 
                userExist: userExist
            )
        }
    }
    
    public func doesUserExist(email: String) async throws -> Bool {
        try await datbaseData.doesUserStateExist(email: email)
    }
    
    // Example: Fetching profile with image URLs (NOT downloading images)
    public func fetchProfile() async throws -> ProfileEntity {
        let remoteProfile = try await remoteData.fetchProfile()
        
        // Return profile with image URLs - do NOT download images here
        return ProfileEntity(
            firstName: remoteProfile.firstName,
            lastName: remoteProfile.lastName,
            profileImageURL: remoteProfile.profileImageURL,  // Pass URL only
            favorites: remoteProfile.favorites.map { favorite in
                FavoriteEntity(
                    id: favorite.id,
                    name: favorite.name,
                    imageURL: favorite.imageURL  // Pass URL only
                )
            }
        )
    }
}

Important Note: Repositories should never download images. They should only pass image URLs. The Presentation Layer (ViewModels) will use Image Cache to download and cache images when needed.

2.3 Repository Responsibilities

  1. Data Orchestration: Combines data from multiple sources (Remote, Database, Keychain, etc.)
  2. Data Transformation: Converts Data Layer models to UseCase domain models
  3. Caching Strategy: Decides when to use cached data vs. fetching fresh data
  4. Error Handling: Transforms Data Layer errors to domain errors
  5. Image URLs Only: Repositories should pass image URLs, never download images. Image downloading is handled by Presentation Layer.

2.4 Repository Patterns

Pattern 1: Remote-First with Database Cache

public func fetchProfile() async throws -> ProfileEntity? {
    // Try database first for offline support
    if let databaseProfile = try await database.fetchProfile() {
        return databaseProfile
    }
    
    // Fetch from remote
    let remoteProfile = try await remote.fetchProfile()
    
    // Cache in database
    try await database.saveProfile(remoteProfile)
    
    return remoteProfile
}

Pattern 2: Remote-Only with Database Persistence

public func login(email: String) async throws -> LoginResponse {
    // Always fetch from remote
    let remoteData = try await remoteData.login(email: email)
    
    // Transform and return
    return mapToLoginResponse(remoteData)
}

Pattern 3: Multi-Source Aggregation

public func fetchProfile() async throws -> ProfileEntity {
    // Fetch from remote
    let remoteProfile = try await remote.fetchProfile()
    
    // Save to database
    try await database.saveProfile(remoteProfile)
    
    // Return data with image URLs (NOT downloading images)
    // Image downloading is handled by Presentation Layer using Image Cache
    return ProfileEntity(
        firstName: remoteProfile.firstName,
        lastName: remoteProfile.lastName,
        favorites: remoteProfile.favorites.map { favorite in
            FavoriteEntity(
                id: favorite.id,
                name: favorite.name,
                imageURL: favorite.imageURL  // Pass URL, not downloaded image
            )
        }
    )
}

Important: Repositories should never download images. They should only pass image URLs. Image downloading and caching is handled directly by the Presentation Layer using Image Cache.


3. Use Cases Layer (THE CENTER)

Clean Architecture Position: Application Business Rules (THE CENTER)

Uncle Bob: "Use cases encapsulate all of the business rules of the system."

This is the most important layer. Everything else exists to serve Use Cases.

Key Principles:

  • Use Cases contain ALL business rules
  • Use Cases define repository interfaces (input boundaries)
  • Use Cases define output boundaries (result models)
  • Use Cases are framework-independent except Combine from Apple(Ask before using).
  • Use Cases do NOT know about UI, ViewModels, or data sources

What belongs in Use Cases:

  • Business validation
  • Business decisions
  • State machines
  • Business workflows
  • Domain logic coordination

What does NOT belong in Use Cases:

  • UI concerns
  • Framework types (SwiftUI)
  • Data source details
  • Presentation formatting

3.1 UseCase Structure (SPM Packages)

📋 IMPLEMENTATION INSTRUCTIONS FOR AI/CURSOR:

  1. Create TWO SPM packages: {UseCase}API and {UseCase}Impl
  2. Each MUST have a Package.swift file
  3. UseCaseAPI defines: UseCase protocol AND Repository protocol
  4. UseCaseImpl implements UseCase protocol from UseCaseAPI
  5. UseCaseImpl depends on Repository protocol from UseCaseAPI (not RepositoryImpl!)
  6. Use Cases are the CENTER - they define repository interfaces
  7. See CRITICAL REQUIREMENT section for Package.swift templates

Each UseCase is organized as separate SPM packages following this structure:

Features/{Feature}/UseCases/{UseCase}/
├── {UseCase}API/                             # SPM Package
│   ├── Package.swift
│   └── Sources/
│       └── {UseCase}API/
│           ├── {UseCase}Protocol.swift        # Public interface (what ViewModel depends on)
│           ├── {UseCase}RepositoryProtocol.swift  # Repository protocol (defined here, RepositoryImpl implements)
│           └── {UseCase}NameSpace.swift       # Domain models
└── {UseCase}Impl/                            # SPM Package
    ├── Package.swift
    └── Sources/
        └── {UseCase}Impl/
            └── {UseCase}Impl.swift            # Implementation

Important:

  • RepositoryProtocol is defined in UseCaseAPI SPM package
  • RepositoryImpl implements this protocol from UseCaseAPI
  • Each API and Impl is a separate SPM package with its own Package.swift

3.2 UseCase Implementation Pattern

Example: LoginUseCase

// LoginUseCaseImpl.swift
import LoginUseCaseAPI  // Contains UseCaseProtocol and RepositoryProtocol

public final class LoginUseCaseImpl: @unchecked Sendable {

    // Dependency: UseCaseImpl USES (depends on) Repository Protocol from UseCaseAPI
    private let loginRepository: LoginUseCaseRepositoryProtocol  // From LoginUseCaseAPI
    private let stateSubject: CurrentValueSubject<LoginUseCaseNameSpace.States, Never>

    public init() {
        self.loginRepository = DependencyContainer.resolve(LoginUseCaseRepositoryProtocol.self)
        self.stateSubject = CurrentValueSubject<LoginUseCaseNameSpace.States, Never>(
            .idle(email: "", acceptedTerms: false)
        )
    }
}

// MARK: - LoginUseCaseProtocol
// UseCaseImpl IMPLEMENTS the protocol from UseCaseAPI (what ViewModel depends on)
extension LoginUseCaseImpl: LoginUseCaseProtocol {  // From LoginUseCaseAPI

    public var statePublisher: AnyPublisher<LoginUseCaseNameSpace.States, Never> {
        stateSubject.eraseToAnyPublisher()
    }

    public func login(email: String) async throws {
        // 1. Validate input
        guard isValidEmail(email) else {
            stateSubject.send(.error(
                email: email, 
                acceptedTerms: false, 
                error: LoginUseCaseNameSpace.Errors.invalidEmailFormat
            ))
            throw LoginUseCaseNameSpace.Errors.invalidEmailFormat
        }

        // 2. Check current state
        switch stateSubject.value {
        case .idle(_, let acceptedTerms):
            guard acceptedTerms else {
                throw LoginUseCaseNameSpace.Errors.accpetTermsAndConditions
            }

            // 3. Update state to loading
            stateSubject.send(.loggingIn(email: email, acceptedTerms: acceptedTerms))

            // 4. Call repository
            let loginResponse = try await loginRepository.login(email: email)

            // 5. Handle response
            switch loginResponse {
            case .userExist:
                try await loginRepository.saveUserLoginState(
                    email: email,
                    temporaryToken: nil,
                    userExist: true
                )
                stateSubject.send(.loggedIn(
                    email: email,
                    registertaion: .alreadyRegistered,
                    acceptedTerms: true
                ))
            case .userNotExist(let temporaryToken):
                try await loginRepository.saveUserLoginState(
                    email: email,
                    temporaryToken: temporaryToken,
                    userExist: false
                )
                stateSubject.send(.loggedIn(
                    email: email,
                    registertaion: .notRegisteredBefore,
                    acceptedTerms: true
                ))
            }

            // 6. Clear state after completion
            clear()
            
        case .loggingIn:
            throw LoginUseCaseNameSpace.Errors.unableToLoginWhileLogginIn
        case .loggedIn:
            throw LoginUseCaseNameSpace.Errors.unableToLoginWhileLoggedIn
        case .error:
            // Handle error state
            break
        }
    }
}

3.3 UseCase Responsibilities

  1. Business Logic: Implements feature-specific business rules
  2. State Management: Manages state using Combine publishers
  3. Input Validation: Validates user inputs before processing
  4. Error Handling: Transforms repository errors to domain errors
  5. State Transitions: Manages state machine transitions

3.4 State Management Pattern

UseCases use Combine for reactive state management:

// State definition
extension LoginUseCaseNameSpace {
    public enum States: Equatable {
        case idle(email: String, acceptedTerms: Bool)
        case loggingIn(email: String, acceptedTerms: Bool)
        case loggedIn(email: String, registertaion: RegistrationState, acceptedTerms: Bool)
        case error(email: String, acceptedTerms: Bool, error: Errors)
    }
}

// State publisher
private let stateSubject: CurrentValueSubject<LoginUseCaseNameSpace.States, Never>

public var statePublisher: AnyPublisher<LoginUseCaseNameSpace.States, Never> {
    stateSubject.eraseToAnyPublisher()
}

3.5 UseCase Patterns

Pattern 1: Simple State Machine

public func performAction() async {
    switch stateSubject.value {
    case .idle:
        stateSubject.send(.loading)
        do {
            let result = try await repository.fetchData()
            stateSubject.send(.loaded(result))
        } catch {
            stateSubject.send(.error(error))
        }
    case .loading:
        // Ignore duplicate calls
        break
    case .loaded, .error:
        // Reset and retry
        stateSubject.send(.loading)
        // ... retry logic
    }
}

Pattern 2: Form Validation

public func updateForm(_ form: FormEntity) {
    self.form = form
    validateForm()
}

private func validateForm() {
    let isValid = isValidEmail(form.email) && 
                  isValidAmount(form.amount) && 
                  form.description.isEmpty == false
    
    isFormValidSubject.send(isValid)
}

Pattern 3: Combined Publishers

public var statePublisher: AnyPublisher<States, Never> {
    stateSubject
        .combineLatest(repository.cachedImageStatePublisher)
        .map { [weak self] state, cacheState in
            guard let self = self else { return state }
            return self.mergeStateWithCache(state: state, cacheState: cacheState)
        }
        .eraseToAnyPublisher()
}

4. Interface Adapters Layer (Presentation)

Clean Architecture Position: Interface Adapters

Purpose: Convert data between Use Cases (inner) and Frameworks (outer). This layer adapts business data to UI format and UI events to business operations.

Critical Understanding:

  • ViewModels are interface adapters, not business logic containers
  • Views are framework details (SwiftUI)
  • Image Cache is a framework service
  • This layer depends on Use Cases (inner) and depends on Frameworks (outer)

Key Principle: ViewModels only map/format/forward. They do NOT contain business rules.

4.1 Presentation Structure (SPM Packages)

📋 IMPLEMENTATION INSTRUCTIONS FOR AI/CURSOR:

  1. Create TWO SPM packages: {Screen}API and {Screen}Impl
  2. Each MUST have a Package.swift file
  3. ScreenAPI defines screen protocol and dependencies
  4. ScreenImpl contains View and ViewModel
  5. ViewModel depends on UseCase protocol from UseCaseAPI (not UseCaseImpl!)
  6. ViewModel is thin - delegates to UseCase, formats for UI
  7. View uses Image Cache directly for image downloading (not through Repository/UseCase)
  8. See CRITICAL REQUIREMENT section for Package.swift templates

Each screen is organized as separate SPM packages following this structure:

Features/{Feature}/Presentation/{Screen}/
├── {Screen}API/                              # SPM Package
│   ├── Package.swift
│   └── Sources/
│       └── {Screen}API/
│           ├── {Screen}Protocol.swift
│           └── {Screen}Dependencies.swift
└── {Screen}Impl/                            # SPM Package
    ├── Package.swift
    └── Sources/
        └── {Screen}Impl/
            ├── {Screen}View.swift
            └── {Screen}ViewModel.swift

Important:

  • Each API and Impl is a separate SPM package with its own Package.swift
  • ScreenAPI package defines the protocol that coordinators depend on
  • ScreenImpl package implements the protocol and contains SwiftUI views

4.1.1 Image Cache

Location: Presentation/ImageCache/{ImageCacheImpl}/ or Utils/ImageCache/{ImageCacheImpl}/

Purpose: Manages image caching and downloading for UI display. This is directly used by the Presentation Layer, not through the Repository/UseCase layers.

Usage in ViewModels:

final class ProfileViewModel: ObservableObject {
    @LazyInjected private var imageCache: ImageCacheProtocol
    @Published var profileImage: UIImage?
    @Published var favoriteImages: [Int: UIImage] = [:]
    
    // Load profile image from URL (received from UseCase/Repository)
    func loadProfileImage(url: URL) async {
        if let cachedImage = await imageCache.getCachedImage(for: url) {
            self.profileImage = cachedImage
        } else {
            let image = await imageCache.downloadAndCacheImage(from: url)
            self.profileImage = image
        }
    }
    
    // Load favorite images from URLs (received from UseCase/Repository)
    func loadFavoriteImages(favorites: [FavoriteEntity]) async {
        await withTaskGroup(of: (Int, UIImage?).self) { group in
            for favorite in favorites {
                guard let url = favorite.imageURL else { continue }
                group.addTask { [weak self] in
                    guard let self = self else { return (favorite.id, nil) }
                    if let cached = await self.imageCache.getCachedImage(for: url) {
                        return (favorite.id, cached)
                    } else {
                        let image = await self.imageCache.downloadAndCacheImage(from: url)
                        return (favorite.id, image)
                    }
                }
            }
            
            for await (id, image) in group {
                if let image = image {
                    self.favoriteImages[id] = image
                }
            }
        }
    }
}

Characteristics:

  • Directly injected into ViewModels
  • Used for UI-specific image caching needs
  • Not part of the business logic flow - Repositories/UseCases only provide URLs
  • Provides synchronous/async image loading APIs
  • ViewModels receive URLs from UseCases and handle image downloading/caching

4.2 ViewModel Implementation Pattern

Example: LoginViewModel

// LoginViewModel.swift
final class LoginViewModel: ObservableObject, @unchecked Sendable {

    // Published properties for SwiftUI binding
    @Published var state: LoginUseCaseNameSpace.States = .idle(email: "", acceptedTerms: false)
    @Published var emailText: String = ""
    @Published var acceptTermsCheckbox: Bool = false
    @Published var hasError: Bool = false
    
    // Error handling
    @Published var isErrorAlertPresented: Bool = false
    @Published var errorTitle: String = "Error"
    @Published var errorMessage: String = ""
    @Published var errorRetryAction: (() -> Void)?

    private var cancellables: Set<AnyCancellable> = []
    private let dependencies: LoginScreenDependencies
    private let loginUseCase: LoginUseCaseProtocol

    // Dependency: ViewModel USES (depends on) UseCase Protocol
    init(dependencies: LoginScreenDependencies, loginUseCase: LoginUseCaseProtocol) {
        self.dependencies = dependencies
        self.loginUseCase = loginUseCase
        subscribeToPublishers()
    }

    private func subscribeToPublishers() {
        // Subscribe to UseCase state changes
        loginUseCase
            .statePublisher
            .map { state in
                switch state {
                case .idle(let email, _),
                     .loggingIn(let email, _),
                     .loggedIn(let email, _, _),
                     .error(let email, _, _):
                    email
                }
            }
            .receive(on: DispatchQueue.main)
            .assign(to: &$emailText)

        loginUseCase
            .statePublisher
            .receive(on: DispatchQueue.main)
            .assign(to: &$state)

        // Handle state transitions
        loginUseCase
            .statePublisher
            .removeDuplicates()
            .receive(on: DispatchQueue.main)
            .sink { [weak self] state in
                guard let self else { return }
                switch state {
                case .loggedIn(let email, let registertaion, _):
                    self.acceptTermsCheckbox = false
                    self.emailText = ""
                    Task { [dependencies] in
                        switch registertaion {
                        case .alreadyRegistered:
                            await dependencies.coordinator.pushLockScreen(email: email)
                        case .notRegisteredBefore:
                            await dependencies.coordinator.pushVerificationScreen(email: email)
                        }
                    }
                case .error(_, _, let error):
                    self.showError(
                        title: "Login Failed",
                        message: error.localizedDescription,
                        retryAction: { [weak self] in
                            Task { @Sendable [weak self] in
                                guard let self else { return }
                                try await self.loginUseCase.login(email: self.emailText)
                            }
                        }
                    )
                case .idle, .loggingIn:
                    break
                }
            }
            .store(in: &cancellables)
    }

    func onTapContinue() {
        Task.detached(priority: .userInitiated) { @Sendable [weak loginUseCase, emailText] in
            guard let loginUseCase else { return }
            do {
                try await loginUseCase.login(email: emailText)
            } catch {
                print(error)
            }
        }
    }
}

4.3 ViewModel Responsibilities (Strictly Limited)

✅ Allowed Responsibilities:

  1. State Mapping: Converts UseCase outputs to UI format
  2. Event Forwarding: Forwards user events to UseCases
  3. UI Formatting: Formats data for display (dates, currency, etc.)
  4. UI State: Manages UI-specific state (loading indicators, alerts, etc.)
  5. Error Presentation: Formats domain errors for UI display

❌ Forbidden Responsibilities:

  1. Business Validation: Validation belongs in Use Cases
  2. Business Decisions: Decision-making belongs in Use Cases
  3. Data Fetching: Data access belongs in Use Cases via Repositories
  4. Business Logic: All business rules belong in Use Cases
  5. State Machine Logic: State transitions belong in Use Cases

4.4 View Implementation Pattern

Example: LoginView

// LoginView.swift
struct LoginView: View {

    @StateObject private var viewModel: LoginViewModel
    
    init(dependencies: LoginScreenDependencies, loginUseCase: LoginUseCaseProtocol) {
        self._viewModel = StateObject(
            wrappedValue: LoginViewModel(
                dependencies: dependencies,
                loginUseCase: loginUseCase
            )
        )
    }
    
    var body: some View {
        VStack(alignment: .center, spacing: .zero) {
            Spacer()

            topTitle
            Spacer()
                .frame(height: .point(.medium))

            emailTextField
            
            if viewModel.hasError {
                errorTextView
            }
            
            termsAndConditionToggle
            
            Spacer()
            actionButton
        }
        .alert(
            viewModel.errorTitle,
            isPresented: $viewModel.isErrorAlertPresented
        ) {
            if viewModel.errorRetryAction != nil {
                Button("Retry") {
                    viewModel.onTryAgainTapped()
                }
            }
            Button("OK") {
                viewModel.dismissError()
            }
        } message: {
            Text(viewModel.errorMessage)
        }
        .padding(.horizontal, .standard)
    }
    
    private var emailTextField: some View {
        TextField(
            "Email Address",
            text: $viewModel.emailText,
            prompt: nil
        )
        .keyboardType(.emailAddress)
        .primaryTextFieldStyle()
    }
    
    private var actionButton: some View {
        Button {
            viewModel.onTapContinue()
        } label: {
            Text("Continue")
        }
        .buttonStyle(.appPrimary(isEnabled: viewModel.isActionButtonEnabled))
        .disabled(viewModel.isActionButtonEnabled == false)
    }
}

4.5 View Responsibilities

  1. UI Rendering: Defines the visual layout and components
  2. Data Binding: Binds to ViewModel @Published properties
  3. User Interaction: Calls ViewModel methods on user actions
  4. Styling: Applies design system styles and themes

4.6 Coordinator Pattern (Navigation)

This project uses the Coordinator Pattern for navigation management, implemented using the AugmentedSUICoordinator library. Coordinators handle all navigation logic, separating it from views and view models, which makes the codebase more maintainable and testable.

4.6.1 What is the Coordinator Pattern?

The Coordinator Pattern is a design pattern that:

  • Separates navigation logic from views and view models
  • Manages navigation flow and screen transitions
  • Handles deep linking and navigation state
  • Enables testability by abstracting navigation operations
  • Supports hierarchical navigation with parent-child coordinator relationships

Key Benefits:

  • Views and ViewModels don't know about navigation implementation details
  • Navigation logic is centralized and reusable
  • Easy to test navigation flows
  • Supports complex navigation scenarios (tabs, modals, deep links)

4.6.2 Adding SUICoordinator to SPM Packages

⚠️ CRITICAL RULE: SUICoordinator must be added to ALL Coordinator packages (both API and Impl), and to ScreenAPI packages that define coordinator protocols.

Step-by-Step: Adding SUICoordinator to a Coordinator Package

  1. Add SUICoordinator to Package.swift dependencies:

    dependencies: [
        .package(
            url: "https://github.com/kiarashvosough1999/AugmentedSUICoordinator.git",
            branch: "main"
        ),
        // ... other dependencies
    ],
  2. Add SUICoordinator to target dependencies:

    targets: [
        .target(
            name: "{Coordinator}API",  // or {Coordinator}Impl
            dependencies: [
                .product(name: "SUICoordinator", package: "AugmentedSUICoordinator"),
                // ... other dependencies
            ]
        )
    ]
  3. Import SUICoordinator in your files:

    import SUICoordinator

Complete Example: Package.swift for CoordinatorAPI

// swift-tools-version: 6.2
import PackageDescription

let package = Package(
    name: "LoginScreenCoordinatorAPI",
    platforms: [.iOS(.v17)],
    products: [
        .library(
            name: "LoginScreenCoordinatorAPI",
            targets: ["LoginScreenCoordinatorAPI"]
        ),
    ],
    dependencies: [
        .package(
            url: "https://github.com/kiarashvosough1999/AugmentedSUICoordinator.git",
            branch: "main"
        ),
    ],
    targets: [
        .target(
            name: "LoginScreenCoordinatorAPI",
            dependencies: [
                .product(
                    name: "SUICoordinator",
                    package: "AugmentedSUICoordinator"
                )
            ]
        )
    ]
)

Complete Example: Package.swift for CoordinatorImpl

// swift-tools-version: 6.2
import PackageDescription

let package = Package(
    name: "FirstAppCoordinatorImpl",
    platforms: [.iOS(.v17)],
    products: [
        .library(
            name: "FirstAppCoordinatorImpl",
            targets: ["FirstAppCoordinatorImpl"]
        ),
    ],
    dependencies: [
        .package(
            url: "https://github.com/kiarashvosough1999/AugmentedSUICoordinator.git",
            branch: "main"
        ),
        .package(
            url: "https://github.com/hmlongco/Resolver.git",
            from: "1.5.1"
        ),
        .package(path: "../FirstAppCoordinatorAPI"),
        // ... other dependencies
    ],
    targets: [
        .target(
            name: "FirstAppCoordinatorImpl",
            dependencies: [
                .product(
                    name: "SUICoordinator",
                    package: "AugmentedSUICoordinator"
                ),
                .product(
                    name: "Resolver",
                    package: "Resolver"
                ),
                .product(
                    name: "FirstAppCoordinatorAPI",
                    package: "FirstAppCoordinatorAPI"
                ),
                // ... other dependencies
            ]
        )
    ]
)

4.6.3 Coordinator Structure

Each coordinator consists of:

  1. CoordinatorAPI Package: Defines the coordinator protocol and routes
  2. CoordinatorImpl Package: Implements the coordinator using SUICoordinator
Coordinator/{Feature}Coordinator/
├── {Feature}CoordinatorAPI/              # SPM Package: Coordinator protocol & routes
│   ├── Package.swift                     # Includes SUICoordinator
│   └── Sources/
│       └── {Feature}CoordinatorAPI/
│           ├── {Feature}CoordinatorProtocol.swift
│           └── {Feature}Routes.swift     # Optional: if routes are in API
│
└── {Feature}CoordinatorImpl/             # SPM Package: Coordinator implementation
    ├── Package.swift                     # Includes SUICoordinator + Resolver
    └── Sources/
        └── {Feature}CoordinatorImpl/
            ├── {Feature}CoordinatorImpl.swift
            └── {Feature}Routes.swift      # Routes enum (if not in API)

4.6.4 Creating a Coordinator

Step 1: Define Coordinator Protocol (CoordinatorAPI)

// Coordinator/LoginCoordinator/LoginCoordinatorAPI/Sources/LoginCoordinatorAPI/LoginCoordinatorProtocol.swift
import SUICoordinator
import SwiftUI

@MainActor
public protocol LoginCoordinatorProtocol: CoordinatorType {
    func pushVerificationScreen(email: String) async
    func pushLockScreen(email: String) async
    func openTermsAndConditions(link: String) async
}

Step 2: Define Routes (CoordinatorImpl or CoordinatorAPI)

Routes define all possible navigation destinations for a coordinator:

// Coordinator/FirstAppCoordinator/FirstAppCoordinatorImpl/Sources/FirstAppCoordinatorImpl/FirstAppRoutes.swift
import SUICoordinator
import Resolver
import SwiftUI
import LoginScreenAPI
import VerificationCodeScreenAPI
import LockScreenAPI

public enum FirstAppRoutes: RouteType {
    
    case pushLoginScreen(
        coordinator: any LoginScreenCoordinatorProtocol,
        loginScreen: LoginScreenProtocol
    )
    case pushVerificationCodeScreen(
        coordinator: any VerificationCodeScreenCoordinatorProtocol,
        verificationCodeScreen: VerificationCodeScreenProtocol,
        email: String
    )
    case pushLockScreen(
        coordinator: any LockScreenCoordinatorProtocol,
        screen: LockScreenProtocol,
        email: String,
        firstLaunch: Bool
    )
    
    // Define presentation style for each route
    public var presentationStyle: TransitionPresentationStyle {
        switch self {
        case .pushLoginScreen,
             .pushVerificationCodeScreen,
             .pushLockScreen:
            .push
        }
    }
    
    // Define the view body for each route
    public var body: some View {
        switch self {
        case .pushLoginScreen(let coordinator, let screen):
            screen.screen(
                dependencies: LoginScreenDependencies(
                    coordinator: coordinator
                )
            )
            
        case .pushVerificationCodeScreen(let coordinator, let screen, let email):
            screen.screen(
                dependencies: VerificationCodeScreenDependencies(
                    coordinator: coordinator,
                    email: email
                )
            )
            
        case .pushLockScreen(let coordinator, let screen, let email, let firstLaunch):
            screen.screen(
                dependencies: LockScreenDependencies(
                    coordinator: coordinator,
                    email: email,
                    firstLaunch: firstLaunch
                )
            )
        }
    }
}

Step 3: Implement Coordinator (CoordinatorImpl)

// Coordinator/FirstAppCoordinator/FirstAppCoordinatorImpl/Sources/FirstAppCoordinatorImpl/FirstAppCoordinatorImpl.swift
import SUICoordinator
import Resolver
import SwiftUI
import FirstAppCoordinatorAPI
import LoginScreenAPI
import AuthenticationFlowUseCaseAPI
import Combine

public final class FirstAppCoordinatorImpl: Coordinator<FirstAppRoutes> {
    
    private var cancellables: Set<AnyCancellable> = []
    
    @LazyInjected private var authFlowUseCase: AuthenticationFlowUseCaseProtocol
    
    public override init() {
        super.init()
    }
    
    public override func start() async {
        // Initial navigation logic
        @LazyInjected var loginScreen: LoginScreenProtocol
        await startFlow(
            route: .pushLoginScreen(
                coordinator: self,
                loginScreen: loginScreen
            )
        )
    }
}

// MARK: - FirstAppCoordinatorProtocol

extension FirstAppCoordinatorImpl: FirstAppCoordinatorProtocol {
    
    public func coordinatorRootView() -> AnyView {
        getView()
            .earseToAnyView()
    }
}

// MARK: - LoginScreenCoordinatorProtocol

extension FirstAppCoordinatorImpl: LoginScreenCoordinatorProtocol {
    
    public func pushVerificationScreen(email: String) async {
        @Injected var verificationCodeScreen: VerificationCodeScreenProtocol
        await router.navigate(
            toRoute: .pushVerificationCodeScreen(
                coordinator: self,
                verificationCodeScreen: verificationCodeScreen,
                email: email
            )
        )
    }
    
    public func pushLockScreen(email: String) async {
        @LazyInjected var lockScreen: LockScreenProtocol
        await router.navigate(
            toRoute: .pushLockScreen(
                coordinator: self,
                screen: lockScreen,
                email: email,
                firstLaunch: false
            )
        )
    }
    
    public func openTermsAndConditions(link: String) async {
        // Handle terms and conditions navigation
    }
}

4.6.5 Coordinator Navigation Methods

SUICoordinator provides several navigation methods:

1. startFlow(route:) - Start Initial Flow

await startFlow(
    route: .pushLoginScreen(
        coordinator: self,
        loginScreen: loginScreen
    )
)

2. router.navigate(toRoute:) - Navigate to Route

await router.navigate(
    toRoute: .pushVerificationCodeScreen(
        coordinator: self,
        verificationCodeScreen: verificationCodeScreen,
        email: email
    )
)

3. navigate(to:presentationStyle:animated:) - Navigate to Another Coordinator

@Injected var mainTabBarCoordinator: any MainTabBarCoordinatorProtocol
await navigate(
    to: mainTabBarCoordinator,
    presentationStyle: .push,
    animated: true
)

4. router.dismiss() - Dismiss Current Screen

await router.dismiss()

5. router.pop() - Pop from Navigation Stack

await router.pop()

6. restart() - Restart Coordinator Flow

await restart()

7. finishFlow() - Finish Current Flow

await finishFlow()

4.6.6 Presentation Styles

SUICoordinator supports various presentation styles:

public var presentationStyle: TransitionPresentationStyle {
    switch self {
    case .pushLoginScreen:
        .push                          // Push onto navigation stack
    case .showModal:
        .sheet                         // Present as sheet
    case .showFullScreen:
        .fullScreenCover               // Present as full screen cover
    case .showNavigationSheet:
        .navigationSheet([.medium])    // Present as navigation sheet with detents
    case .showNavigationFullScreen:
        .navigationFullScreenCover     // Present as navigation full screen cover
    case .customDetents:
        .detents([.fraction(0.5)])    // Custom detents
    }
}

4.6.7 Screen Coordinator Protocols

Each screen defines a coordinator protocol that specifies navigation actions:

// Features/Login/Presentation/LoginScreen/LoginScreenAPI/Sources/LoginScreenAPI/LoginScreenCoordinatorProtocol.swift
public protocol LoginScreenCoordinatorProtocol: AnyObject, Sendable {
    func pushVerificationScreen(email: String) async
    func pushLockScreen(email: String) async
    func openTermsAndConditions(link: String) async
}

The coordinator protocol is included in screen dependencies:

// Features/Login/Presentation/LoginScreen/LoginScreenAPI/Sources/LoginScreenAPI/LoginScreenDependencies.swift
public struct LoginScreenDependencies: Sendable {
    public let coordinator: any LoginScreenCoordinatorProtocol
    
    public init(coordinator: any LoginScreenCoordinatorProtocol) {
        self.coordinator = coordinator
    }
}

4.6.8 Using Coordinators in ViewModels

ViewModels receive the coordinator through dependencies and use it for navigation:

// Features/Login/Presentation/LoginScreen/LoginScreenImpl/Sources/LoginScreenImpl/LoginViewModel.swift
final class LoginViewModel: ObservableObject {
    private let dependencies: LoginScreenDependencies
    private let loginUseCase: LoginUseCaseProtocol
    
    init(
        dependencies: LoginScreenDependencies,
        loginUseCase: LoginUseCaseProtocol
    ) {
        self.dependencies = dependencies
        self.loginUseCase = loginUseCase
    }
    
    func handleLoginSuccess(email: String) async {
        // Navigate using coordinator
        await dependencies.coordinator.pushVerificationScreen(email: email)
    }
    
    func handleTermsAndConditions() async {
        await dependencies.coordinator.openTermsAndConditions(
            link: "https://payred.com.tr/terms/"
        )
    }
}

4.6.9 Coordinator Hierarchy

Coordinators can have parent-child relationships:

// MainAppCoordinator manages FirstAppCoordinator and MainTabBarCoordinator
public final class MainAppCoordinatorImpl: Coordinator<MainAppCoordinatorRoutes> {
    
    public override func start() async {
        // Navigate to child coordinator
        @Injected var firstAppCoordinator: any FirstAppCoordinatorProtocol
        await navigate(
            to: firstAppCoordinator,
            presentationStyle: .push,
            animated: true
        )
    }
}

4.6.10 Registering Coordinators with Resolver

Coordinators are registered in DIRegisterar.swift:

// PayRed/DI/DIRegisterar.swift
import Resolver
import MainAppCoordinatorAPI
import MainAppCoordinatorImpl

extension Resolver: @retroactive ResolverRegistering {
    public static func registerAllServices() {
        // Register MainAppCoordinator
        Resolver.register(MainAppCoordinatorProtocol.self) { @MainActor in
            MainAppCoordinatorImpl()
        }
        .scope(.application)
        
        // Register FirstAppCoordinator (implements multiple protocols)
        Resolver.register(FirstAppCoordinatorImpl.self) { @MainActor in
            FirstAppCoordinatorImpl()
        }
        .implements(FirstAppCoordinatorProtocol.self)
        .implements(SplashScreenCoordinatorProtocol.self)
        .implements(LoginScreenCoordinatorProtocol.self)
        .implements(VerificationCodeScreenCoordinatorProtocol.self)
        .implements(LockScreenCoordinatorProtocol.self)
        .scope(.shared)
    }
}

4.6.11 Coordinator Best Practices

1. One Coordinator Per Feature Flow

Each major feature flow should have its own coordinator:

  • FirstAppCoordinator - Handles authentication flow
  • MainTabBarCoordinator - Handles main app navigation
  • HomeTabCoordinator - Handles home tab navigation
  • RegisterCoordinator - Handles registration flow

2. Coordinator Protocols for Screens

Each screen should define a coordinator protocol that specifies its navigation needs:

// ScreenAPI defines coordinator protocol
public protocol LoginScreenCoordinatorProtocol: AnyObject, Sendable {
    func pushVerificationScreen(email: String) async
    func pushLockScreen(email: String) async
}

// CoordinatorImpl implements the protocol
extension FirstAppCoordinatorImpl: LoginScreenCoordinatorProtocol {
    public func pushVerificationScreen(email: String) async {
        // Implementation
    }
}

3. Use @MainActor for Coordinators

All coordinators should be marked with @MainActor:

@MainActor
public final class FirstAppCoordinatorImpl: Coordinator<FirstAppRoutes> {
    // ...
}

4. Register Coordinators with Appropriate Scopes

  • .application: Root coordinators (e.g., MainAppCoordinator)
  • .shared: Feature coordinators (e.g., FirstAppCoordinator, MainTabBarCoordinator)

5. Use Dependency Injection for Child Coordinators

// ✅ Good: Inject child coordinator
@Injected var firstAppCoordinator: any FirstAppCoordinatorProtocol
await navigate(to: firstAppCoordinator, presentationStyle: .push)

// ❌ Bad: Create coordinator directly
let coordinator = FirstAppCoordinatorImpl()
await navigate(to: coordinator, presentationStyle: .push)

6. Handle Coordinator Lifecycle

public override func start() async {
    // Initial setup
    await setupInitialFlow()
    
    // Subscribe to state changes
    subscribeToStateChanges()
}

deinit {
    // Cleanup
    cancellables.removeAll()
}

4.6.12 Common Coordinator Patterns

Pattern 1: Authentication Flow Coordinator

public final class FirstAppCoordinatorImpl: Coordinator<FirstAppRoutes> {
    
    @LazyInjected private var authFlowUseCase: AuthenticationFlowUseCaseProtocol
    
    public override func start() async {
        loop: for await state in authFlowUseCase.statePublisher.values {
            switch state {
            case .login:
                await showLoginScreen()
            case .verificationCode(let email):
                await showVerificationScreen(email: email)
            case .pinCode(let email, _):
                await showLockScreen(email: email)
            case .authenticated:
                break loop
            }
        }
    }
}

Pattern 2: Tab Bar Coordinator

public final class MainTabBarCoordinatorImpl: Coordinator<MainTabBarRoutes> {
    
    public override func start() async {
        await startFlow(
            route: .mainTabBar(
                coordinator: self,
                homeCoordinator: homeTabCoordinator,
                cardsCoordinator: cardsTabCoordinator,
                transactionCoordinator: transactionTabCoordinator,
                transferCoordinator: transferTabCoordinator,
                menuCoordinator: menuTabCoordinator
            )
        )
    }
}

Pattern 3: Modal Presentation

extension FirstAppCoordinatorImpl: LoginScreenCoordinatorProtocol {
    
    public func showForgotPassword() async {
        @Injected var forgotPasswordScreen: ForgotPasswordScreenProtocol
        await router.navigate(
            toRoute: .forgotPasswordBottomSheetScreen(
                coordinator: self,
                forgotPasswordScreen: forgotPasswordScreen
            )
        )
    }
}

4.6.13 Verification Checklist

Before considering a coordinator complete, verify:

  • SUICoordinator is added to CoordinatorAPI Package.swift
  • SUICoordinator is added to CoordinatorImpl Package.swift
  • Coordinator protocol extends CoordinatorType from SUICoordinator
  • Routes enum conforms to RouteType
  • Routes define presentationStyle for each case
  • Routes define body view for each case
  • Coordinator implementation extends Coordinator<Routes>
  • Coordinator implements start() method
  • Coordinator implements all required coordinator protocols
  • Coordinator is registered in DIRegisterar.swift
  • Coordinator uses @MainActor
  • Screen coordinator protocols are defined in ScreenAPI packages
  • Screen dependencies include coordinator protocol

5. Dependency Injection

This project uses Resolver for dependency injection. Resolver is an ultralight Dependency Injection / Service Locator framework for Swift that provides automatic type inference, scopes, and property wrapper support.

Why Resolver?

  • Ultralight implementation (~700 lines of code)
  • Automatic type inference (40-60% less code)
  • Thread-safe
  • Property wrapper support (@Injected, @LazyInjected)
  • Multiple scopes (Application, Cached, Graph, Shared, Unique)
  • Protocol support
  • Well-tested and production-ready

All dependencies are registered in a central location: PayRed/DI/DIRegisterar.swift.

5.1 Adding Resolver to SPM Packages

⚠️ CRITICAL RULE: Resolver must be added to ALL SPM packages EXCEPT API packages.

API packages ({Component}API) should NOT include Resolver because:

  • API packages contain only protocols and interfaces
  • They should remain framework-agnostic
  • They define contracts, not implementations

Impl packages ({Component}Impl) MUST include Resolver because:

  • They contain implementations that need to resolve dependencies
  • They use Resolver to inject dependencies via Resolver.resolve() or property wrappers

Step-by-Step: Adding Resolver to an Impl Package

When creating a new Impl package (UseCaseImpl, RepositoryImpl, ScreenImpl, etc.), follow these steps:

  1. Add Resolver to Package.swift dependencies:

    dependencies: [
        .package(path: "../{Component}API"),  // API package dependency
        .package(
            url: "https://github.com/hmlongco/Resolver.git",
            from: "1.5.1"
        ),
    ],
  2. Add Resolver to target dependencies:

    targets: [
        .target(
            name: "{Component}Impl",
            dependencies: [
                .product(name: "{Component}API", package: "{Component}API"),
                .product(name: "Resolver", package: "Resolver")
            ]
        )
    ]
  3. Import Resolver in your implementation files:

    import Resolver
    import {Component}API

Complete Example: Package.swift for UseCaseImpl

// swift-tools-version: 6.2
import PackageDescription

let package = Package(
    name: "LoginUseCaseImpl",
    platforms: [.iOS(.v17)],
    products: [
        .library(
            name: "LoginUseCaseImpl",
            targets: ["LoginUseCaseImpl"]
        ),
    ],
    dependencies: [
        .package(
            name: "LoginUseCaseAPI",
            path: "../LoginUseCaseAPI"
        ),
        .package(
            url: "https://github.com/hmlongco/Resolver.git",
            from: "1.5.1"
        ),
    ],
    targets: [
        .target(
            name: "LoginUseCaseImpl",
            dependencies: [
                .product(
                    name: "LoginUseCaseAPI",
                    package: "LoginUseCaseAPI"
                ),
                .product(
                    name: "Resolver",
                    package: "Resolver"
                )
            ]
        ),
    ]
)

Complete Example: Package.swift for RepositoryImpl

// swift-tools-version: 6.2
import PackageDescription

let package = Package(
    name: "LoginRepositoryImpl",
    platforms: [.iOS(.v17)],
    products: [
        .library(
            name: "LoginRepositoryImpl",
            targets: ["LoginRepositoryImpl"]
        ),
    ],
    dependencies: [
        .package(
            name: "LoginRepositoryAPI",
            path: "../LoginRepositoryAPI"
        ),
        .package(
            name: "LoginUseCaseAPI",
            path: "../../UseCases/LoginUseCase/LoginUseCaseAPI"
        ),
        .package(
            url: "https://github.com/hmlongco/Resolver.git",
            from: "1.5.1"
        ),
    ],
    targets: [
        .target(
            name: "LoginRepositoryImpl",
            dependencies: [
                .product(
                    name: "LoginRepositoryAPI",
                    package: "LoginRepositoryAPI"
                ),
                .product(
                    name: "LoginUseCaseAPI",
                    package: "LoginUseCaseAPI"
                ),
                .product(
                    name: "Resolver",
                    package: "Resolver"
                )
            ]
        ),
    ]
)

Verification Checklist for Resolver Integration

Before considering a package complete, verify:

  • Resolver is NOT added to API packages ({Component}API)
  • Resolver IS added to all Impl packages ({Component}Impl)
  • Resolver is added to dependencies array in Package.swift
  • Resolver is added to target dependencies array
  • import Resolver is added to implementation files that use it
  • Package builds successfully with Resolver dependency

5.2 Registration Pattern

All dependencies are registered in PayRed/DI/DIRegisterar.swift using Resolver's registration API. The registration follows the ResolverRegistering protocol pattern.

Registration File Structure:

// PayRed/DI/DIRegisterar.swift
import Resolver
// ... import all API and Impl packages

extension Resolver: @retroactive ResolverRegistering {
    public static func registerAllServices() {
        // All registrations go here
    }
}

Example: Registering Data Layer Implementations

Data implementations implement multiple Data Protocols from RepositoryAPI packages:

// Register MobileGatewayService (implements multiple RemoteDataProtocols)
register {
    MobileGatewayServiceImpl()
}
.implements(LoginRepositoryRemoteDataProtocol.self)  // From LoginRepositoryAPI
.implements(CardListRepositoryRemoteDataProtocol.self)  // From CardListRepositoryAPI
.implements(TransferRepositoryRemoteDataProtocol.self)  // From TransferRepositoryAPI
.implements(HomeTabRepositoryRemoteDataProtocol.self)  // From HomeTabRepositoryAPI
// ... implements many more protocols
.scope(.application)

// Register Database (implements multiple DataBaseDataProtocols)
register {
    DataBase()
}
.implements(LoginRepositoryDataBaseDataProtocol.self)  // From LoginRepositoryAPI
.implements(CardListRepositoryDataBaseDataProtocol.self)  // From CardListRepositoryAPI
.implements(AuthenticationFlowRepositoryDataBaseDataProtocol.self)  // From AuthenticationFlowRepositoryAPI
// ... implements many more protocols
.scope(.application)

// Register Keychain (implements multiple KeyChainDataProtocols)
register {
    KeyChainDataImpl()
}
.implements(LockRepositoryKeyChainDataProtocol.self)  // From LockRepositoryAPI
.implements(AuthenticationFlowRepositoryKeyChainDataProtocol.self)  // From AuthenticationFlowRepositoryAPI
.implements(ChangeAppPinRepositoryKeyChainDataProtocol.self)  // From ChangeAppPinRepositoryAPI
// ... implements more protocols
.scope(.application)

// Register Image Cache (used directly by Presentation Layer)
register {
    ImageCacheImpl()
}
.implements(UserProfileRepositoryImageCacheDataProtocol.self)  // From UserProfileRepositoryAPI
.implements(EditProfilePictureRepositoryImageCacheDataProtocol.self)  // From EditProfilePictureRepositoryAPI
.implements(HomeTabRepositoryImageCacheDataProtocol.self)  // From HomeTabRepositoryAPI
// ... implements more protocols
.scope(.application)

Example: Registering Repository Implementations

Repository implementations implement Repository Protocols from UseCaseAPI packages:

// Register LoginRepository (implements Repository Protocol from UseCaseAPI)
register {
    LoginRepositoryImpl()
}
.implements(LoginUseCaseRepositoryProtocol.self)  // From LoginUseCaseAPI
.scope(.shared)

// Register AuthenticationFlowRepository
register {
    AuthenticationFlowRepositoryImpl()
}
.implements(AuthenticationFlowUseCaseRepositoryProtocol.self)  // From AuthenticationFlowUseCaseAPI
.scope(.shared)

Example: Registering UseCase Implementations

UseCase implementations implement UseCase Protocols:

// Register LoginUseCase (implements UseCase Protocol)
register(LoginUseCaseProtocol.self) {
    LoginUseCaseImpl()
}
.scope(.shared)

// Register AuthenticationFlowUseCase
register {
    AuthenticationFlowUseCaseImpl()
}
.implements(AuthenticationFlowUseCaseProtocol.self)
.scope(.application)

Example: Registering Presentation Implementations

Screen implementations implement Screen Protocols:

// Register LoginScreen (implements Screen Protocol)
register(LoginScreenProtocol.self) {
    LoginScreenImpl()
}
.scope(.unique)

// Register Coordinators (with @MainActor for UI components)
Resolver.register(MainAppCoordinatorProtocol.self) { @MainActor in
    MainAppCoordinatorImpl()
}
.scope(.application)

Resolver.register(FirstAppCoordinatorImpl.self) { @MainActor in
    FirstAppCoordinatorImpl()
}
.implements(FirstAppCoordinatorProtocol.self)
.implements(SplashScreenCoordinatorProtocol.self)
.implements(LoginScreenCoordinatorProtocol.self)
// ... implements multiple coordinator protocols
.scope(.shared)

5.3 Dependency Scopes

Resolver provides several scopes for managing object lifetimes. This project uses the following scopes:

  • .application: Singleton instance for the app lifetime

    • Used for: Data sources (NetworkService, Database, Keychain, ImageCache)
    • Example: MobileGatewayServiceImpl, DataBase(), KeyChainDataImpl()
  • .shared: Shared instance within a feature or module

    • Used for: UseCases, Repositories, Coordinators
    • Example: LoginUseCaseImpl, LoginRepositoryImpl, MainAppCoordinatorImpl
  • .unique: New instance each time it's resolved

    • Used for: Screens, ViewModels (when they need fresh state)
    • Example: LoginScreenImpl, LockScreenImpl

Resolver also supports:

  • .cached: Cached for the duration of the resolution graph
  • .graph: New instance per resolution graph

Note: For @MainActor types (like Coordinators and Screens), use @MainActor closure annotation:

Resolver.register(MainAppCoordinatorProtocol.self) { @MainActor in
    MainAppCoordinatorImpl()
}
.scope(.application)

5.4 Dependency Resolution

Resolver provides multiple ways to resolve dependencies. This project uses both explicit resolution (using Resolver.resolve()) and property wrappers (@Injected, @LazyInjected).

Method 1: Explicit Resolution with Resolver.resolve()

In UseCases (UseCaseImpl resolves Repository Protocol):

// Features/Login/UseCases/LoginUseCase/LoginUseCaseImpl/Sources/LoginUseCaseImpl/LoginUseCaseImpl.swift
import Resolver
import LoginUseCaseAPI

public final class LoginUseCaseImpl: @unchecked Sendable {
    private let loginRepository: LoginUseCaseRepositoryProtocol

    public init() {
        // UseCaseImpl USES Repository Protocol - explicit resolution
        self.loginRepository = Resolver.resolve(LoginUseCaseRepositoryProtocol.self)
    }
}

In Repositories (RepositoryImpl resolves Data Protocols):

// Features/Login/Repositories/LoginRepository/LoginRepositoryImpl/Sources/LoginRepositoryImpl/LoginRepositoryImpl.swift
import Resolver
import LoginRepositoryAPI
import LoginUseCaseAPI

public final class LoginRepositoryImpl: @unchecked Sendable {
    private let remoteData: LoginRepositoryRemoteDataProtocol
    private let databaseData: LoginRepositoryDataBaseDataProtocol

    public init() {
        // RepositoryImpl USES Data Protocols - explicit resolution
        self.remoteData = Resolver.resolve(LoginRepositoryRemoteDataProtocol.self)
        self.databaseData = Resolver.resolve(LoginRepositoryDataBaseDataProtocol.self)
    }
}

Method 2: Property Wrappers (@Injected, @LazyInjected)

Property wrappers provide automatic dependency injection. Use @LazyInjected for dependencies that should be resolved lazily (on first access).

In ViewModels (using property wrappers):

// Features/Login/Presentation/LoginScreen/LoginScreenImpl/Sources/LoginScreenImpl/LoginViewModel.swift
import Resolver
import LoginUseCaseAPI

final class LoginViewModel: ObservableObject {
    // ViewModel receives UseCase Protocol via constructor injection
    private let loginUseCase: LoginUseCaseProtocol
    
    init(dependencies: LoginScreenDependencies, loginUseCase: LoginUseCaseProtocol) {
        self.dependencies = dependencies
        self.loginUseCase = loginUseCase
    }
}

In Coordinators (using @Injected for MainActor types):

// Coordinator/AppMainCoordinator/MainAppCoordinatorImpl/Sources/MainAppCoordinatorImpl/MainAppCoordinatorImpl.swift
import Resolver

@MainActor
public final class MainAppCoordinatorImpl: MainAppCoordinatorProtocol {
    // Lazy injection for UseCases
    @LazyInjected private var authFlowUseCase: AuthenticationFlowUseCaseProtocol
    @LazyInjected private var updateAppUseCase: UpdateAppUseCaseProtocol
    @LazyInjected private var notificationManagerUseCase: NotificationManagerUseCaseProtocol
    
    // Injected for coordinators (resolved immediately)
    func showFirstApp() {
        @Injected var firstAppCoordinator: any FirstAppCoordinatorProtocol
        // Use coordinator
    }
}

In ViewModels (for Image Cache - using @LazyInjected):

// Example from ViewModels that use Image Cache
import Resolver

final class ProfileViewModel: ObservableObject {
    // Lazy injection for Image Cache (used directly by Presentation Layer)
    @LazyInjected private var imageCache: ImageCacheProtocol
    
    func loadProfileImage(url: URL) async {
        if let cachedImage = await imageCache.getCachedImage(for: url) {
            self.profileImage = cachedImage
        } else {
            let image = await imageCache.downloadAndCacheImage(from: url)
            self.profileImage = image
        }
    }
}

In App Entry Point (PayRedApp.swift):

// PayRed/PayRedApp.swift
import Resolver

@main
struct PayRedApp: App {
    @Injected private var mainCoordinator: (any MainAppCoordinatorProtocol)
    
    init() {
        // Register all dependencies
        Resolver.registerAllServices()
        
        // Resolve dependencies that need to be initialized at app startup
        _ = Resolver.resolve(NotificationManagerUseCaseProtocol.self)
    }
    
    var body: some Scene {
        WindowGroup {
            mainCoordinator.start()
        }
    }
}

When to Use Each Method

  • Resolver.resolve(): Use in initializers when you need the dependency immediately
  • @Injected: Use for properties that should be resolved immediately (typically for @MainActor types)
  • @LazyInjected: Use for properties that should be resolved on first access (saves memory, better for optional dependencies)

5.5 Advanced Resolver Features

Multiple Protocol Registration

A single implementation can implement multiple protocols. This is commonly used for data layer implementations:

// Register MobileGatewayService implementing multiple RemoteDataProtocols
register {
    MobileGatewayServiceImpl()
}
.implements(LoginRepositoryRemoteDataProtocol.self)
.implements(CardListRepositoryRemoteDataProtocol.self)
.implements(TransferRepositoryRemoteDataProtocol.self)
.implements(HomeTabRepositoryRemoteDataProtocol.self)
// ... can implement many more protocols
.scope(.application)

Named Registrations

Resolver supports named registrations for differentiating between multiple instances of the same type:

// Register with a name
register(name: "production") {
    NetworkService(baseURL: "https://api.production.com")
}

register(name: "staging") {
    NetworkService(baseURL: "https://api.staging.com")
}

// Resolve by name
let productionService: NetworkService = Resolver.resolve(name: "production")

Argument Passing

Resolver supports passing arguments during resolution:

// Register with factory that accepts arguments
register { (userId: String) in
    UserProfileService(userId: userId)
}

// Resolve with arguments
let service = Resolver.resolve(UserProfileService.self, args: "user123")

Conditional Registration

Use conditional registration for different build configurations:

#if DEBUG
register {
    MockNetworkService()
}
.implements(NetworkServiceProtocol.self)
.scope(.application)
#else
register {
    ProductionNetworkService()
}
.implements(NetworkServiceProtocol.self)
.scope(.application)
#endif

Custom Scopes

While this project primarily uses .application, .shared, and .unique, Resolver also supports custom scopes:

// Create a custom scope
extension ResolverScope {
    static let session = ResolverScopeCache()
}

// Register with custom scope
register {
    SessionManager()
}
.scope(.session)

5.6 Resolver Best Practices

1. Registration Order

Register dependencies in dependency order (data sources first, then repositories, then use cases, then presentation):

public static func registerAllServices() {
    // 1. Register Data Layer implementations first
    register { MobileGatewayServiceImpl() }
        .implements(LoginRepositoryRemoteDataProtocol.self)
        .scope(.application)
    
    register { DataBase() }
        .implements(LoginRepositoryDataBaseDataProtocol.self)
        .scope(.application)
    
    // 2. Register Repository implementations
    register {
        LoginRepositoryImpl()
    }
    .implements(LoginUseCaseRepositoryProtocol.self)
    .scope(.shared)
    
    // 3. Register UseCase implementations
    register(LoginUseCaseProtocol.self) {
        LoginUseCaseImpl()
    }
    .scope(.shared)
    
    // 4. Register Presentation implementations last
    register(LoginScreenProtocol.self) {
        LoginScreenImpl()
    }
    .scope(.unique)
}

2. Scope Selection Guidelines

  • .application: Use for singletons that live for the app lifetime

    • Data sources (NetworkService, Database, Keychain, ImageCache)
    • Services that maintain global state
    • Expensive-to-create objects
  • .shared: Use for objects that should be shared within a feature/module

    • UseCases (business logic)
    • Repositories (data access)
    • Coordinators (navigation)
  • .unique: Use for objects that need fresh state each time

    • Screens (UI components)
    • ViewModels (when they need fresh state)
    • Temporary objects

3. Protocol-Based Registration

Always register implementations against their protocol interfaces, not concrete types:

// ✅ Good: Register against protocol
register(LoginUseCaseProtocol.self) {
    LoginUseCaseImpl()
}
.scope(.shared)

// ❌ Bad: Register against concrete type
register {
    LoginUseCaseImpl()
}
.scope(.shared)

4. MainActor Support

For @MainActor types (Coordinators, Screens), use @MainActor closure annotation:

register(MainAppCoordinatorProtocol.self) { @MainActor in
    MainAppCoordinatorImpl()
}
.scope(.application)

5. Thread Safety

Resolver is thread-safe, but be aware of:

  • Use @MainActor for UI-related types
  • Use @unchecked Sendable for types that resolve dependencies in init()
  • Prefer property wrappers (@Injected, @LazyInjected) for thread-safe access

6. Testing with Resolver

For testing, you can register mock implementations:

// In test setup
Resolver.register(LoginUseCaseProtocol.self) {
    MockLoginUseCase()
}
.scope(.unique)

// In test teardown
Resolver.root = Resolver()

5.7 Common Resolver Patterns

Pattern 1: Repository Resolving Data Protocols

// RepositoryImpl resolves multiple Data Protocols
public final class LoginRepositoryImpl: LoginUseCaseRepositoryProtocol {
    private let remoteData: LoginRepositoryRemoteDataProtocol
    private let databaseData: LoginRepositoryDataBaseDataProtocol
    private let keychainData: LoginRepositoryKeyChainDataProtocol
    
    public init() {
        self.remoteData = Resolver.resolve(LoginRepositoryRemoteDataProtocol.self)
        self.databaseData = Resolver.resolve(LoginRepositoryDataBaseDataProtocol.self)
        self.keychainData = Resolver.resolve(LoginRepositoryKeyChainDataProtocol.self)
    }
}

Pattern 2: UseCase Resolving Repository Protocol

// UseCaseImpl resolves Repository Protocol
public final class LoginUseCaseImpl: LoginUseCaseProtocol {
    private let loginRepository: LoginUseCaseRepositoryProtocol
    
    public init() {
        self.loginRepository = Resolver.resolve(LoginUseCaseRepositoryProtocol.self)
    }
}

Pattern 3: Coordinator Using Property Wrappers

// Coordinator uses @LazyInjected for UseCases
@MainActor
public final class MainAppCoordinatorImpl: MainAppCoordinatorProtocol {
    @LazyInjected private var authFlowUseCase: AuthenticationFlowUseCaseProtocol
    @LazyInjected private var updateAppUseCase: UpdateAppUseCaseProtocol
    
    // Local injection for coordinators
    func showFirstApp() {
        @Injected var firstAppCoordinator: any FirstAppCoordinatorProtocol
        // Use coordinator
    }
}

Pattern 4: Screen Receiving Dependencies via Constructor

// Screen receives UseCase via constructor (not Resolver)
public struct LoginScreenImpl: LoginScreenProtocol {
    private let dependencies: LoginScreenDependencies
    private let loginUseCase: LoginUseCaseProtocol
    
    public init(
        dependencies: LoginScreenDependencies,
        loginUseCase: LoginUseCaseProtocol
    ) {
        self.dependencies = dependencies
        self.loginUseCase = loginUseCase
    }
}

// Coordinator resolves and passes dependencies
@MainActor
public final class FirstAppCoordinatorImpl: FirstAppCoordinatorProtocol {
    func showLogin() {
        let loginUseCase = Resolver.resolve(LoginUseCaseProtocol.self)
        let screen = Resolver.resolve(LoginScreenProtocol.self)
        // Configure and present screen
    }
}

6. Data Flow

6.1 Complete Flow Example: User Login

Note: The diagram below is also available as a Mermaid file: docs/diagrams/data-flow-diagram.mmd

flowchart TD
    Start([User Action]) --> View[View: User taps Continue button]
    View --> ViewModel[ViewModel.onTapContinue]
    
    ViewModel --> UseCaseCall[ViewModel calls loginUseCase.login]
    
    UseCaseCall --> UseCaseProcess[UseCase Processing]
    UseCaseProcess --> Validate[Validates email format]
    Validate --> UpdateState[Updates state: .loggingIn]
    UpdateState --> CallRepo[Calls repository: loginRepository.login]
    
    CallRepo --> RepoProcess[Repository Processing]
    RepoProcess --> CallRemote[Calls remote: remoteData.login]
    CallRemote --> HTTPRequest[NetworkService performs HTTP request]
    HTTPRequest --> RemoteResult[Returns: LoginOrRegisterRemoteResult]
    RemoteResult --> Transform[Transforms to: LoginResponse]
    
    Transform --> RepoReturn[Repository → UseCase]
    RepoReturn --> HandleResponse[UseCase handles response]
    HandleResponse --> SaveState[Calls: loginRepository.saveUserLoginState]
    SaveState --> SaveDB[Repository saves to database]
    SaveDB --> UpdateLoggedIn[Updates state: .loggedIn]
    
    UpdateLoggedIn --> StatePublisher[State publisher emits: .loggedIn]
    StatePublisher --> ViewModelSub[ViewModel subscribes to state changes]
    ViewModelSub --> UpdatePublished[Updates @Published properties]
    
    UpdatePublished --> SwiftUI[SwiftUI observes @Published changes]
    SwiftUI --> ReRender[View re-renders]
    ReRender --> Navigation[ViewModel triggers navigation via coordinator]
    Navigation --> End([End])
    
    style Start fill:#4caf50
    style End fill:#4caf50
    style UseCaseProcess fill:#fff4e1
    style RepoProcess fill:#e8f5e9
    style HTTPRequest fill:#fce4ec
Loading

6.2 State Flow Diagram

Note: The diagram below is also available as a Mermaid file: docs/diagrams/state-flow-diagram.mmd

stateDiagram-v2
    [*] --> idle: Initial State
    
    idle --> loggingIn: User Action<br/>(ViewModel → UseCase)
    loggingIn --> Repository: Calls Repository
    Repository --> DataLayer: Calls Data Layer
    
    DataLayer --> loggedIn: Success
    DataLayer --> error: Failure
    
    loggedIn --> ViewModelUpdate: State Publisher Emits
    error --> ViewModelUpdate: State Publisher Emits
    
    ViewModelUpdate --> ViewRender: Updates @Published
    ViewRender --> [*]: View Re-renders
    
    note right of idle
        Initial state
        Waiting for user input
    end note
    
    note right of loggingIn
        UseCase validates input
        Updates state to loading
    end note
    
    note right of Repository
        Orchestrates data sources
        Transforms data
    end note
    
    note right of DataLayer
        Remote API calls
        Database operations
        Keychain access
    end note
Loading

7. Boundary Interfaces

Clean Architecture Principle: Use Cases define boundaries (interfaces) that outer layers implement.

7.1 Input Boundaries (Repository Interfaces)

Defined by: Use Cases
Implemented by: Repository Implementations

// UseCaseAPI defines the interface (input boundary)
public protocol LoginUseCaseRepositoryProtocol {
    func login(email: String) async throws -> LoginUseCaseNameSpace.LoginResponse
}

// RepositoryImpl implements it
extension LoginRepositoryImpl: LoginUseCaseRepositoryProtocol {
    // Implementation
}

Key Points:

  • Use Case defines what it needs
  • Repository provides what Use Case needs
  • Dependency points inward (Repository depends on Use Case's interface)

7.2 Output Boundaries (Result Models)

Defined by: Use Cases
Used by: Interface Adapters (ViewModels)

// UseCaseAPI defines output boundary
extension LoginUseCaseNameSpace {
    public enum States {
        case idle(email: String, acceptedTerms: Bool)
        case loggingIn(email: String, acceptedTerms: Bool)
        case loggedIn(email: String, registration: RegistrationState, acceptedTerms: Bool)
        case error(email: String, acceptedTerms: Bool, error: Errors)
    }
}

// ViewModel consumes the output boundary
viewModel.subscribe(to: useCase.statePublisher) // Receives States enum

Key Points:

  • Use Case defines output format
  • ViewModel consumes output format
  • Use Case doesn't know about ViewModel
  • ViewModel adapts Use Case output to UI format

8. Composition Root

Clean Architecture Principle: Object composition happens at the outermost layer, once, at application startup.

8.1 What is Composition Root?

The Composition Root is where all dependencies are wired together. It lives in the Frameworks & Drivers layer (outermost).

Location: App/AppDelegate.swift or App/PayRedApp.swift

8.2 Composition Root Responsibilities

  1. Wire all dependencies - Connect interfaces to implementations
  2. Create object graph - Build the dependency tree
  3. Happens once - At application startup
  4. Outermost layer only - Only the app entry point knows about all implementations

8.3 Example (Using Resolver)

In this project, the Composition Root is split into two parts:

  1. Registration: PayRed/DI/DIRegisterar.swift - Contains all dependency registrations
  2. Initialization: PayRed/PayRedApp.swift - Calls registration at app startup

Registration File (PayRed/DI/DIRegisterar.swift):

import Resolver
// ... import all API and Impl packages

extension Resolver: @retroactive ResolverRegistering {
    public static func registerAllServices() {
        // Register Data Layer implementations
        register {
            MobileGatewayServiceImpl()
        }
        .implements(LoginRepositoryRemoteDataProtocol.self)
        .implements(CardListRepositoryRemoteDataProtocol.self)
        // ... implements many more protocols
        .scope(.application)
        
        register {
            DataBase()
        }
        .implements(LoginRepositoryDataBaseDataProtocol.self)
        .implements(CardListRepositoryDataBaseDataProtocol.self)
        // ... implements many more protocols
        .scope(.application)
        
        // Register Repository implementations
        register {
            LoginRepositoryImpl()
        }
        .implements(LoginUseCaseRepositoryProtocol.self)
        .scope(.shared)
        
        // Register UseCase implementations
        register(LoginUseCaseProtocol.self) {
            LoginUseCaseImpl()
        }
        .scope(.shared)
        
        // Register Presentation implementations
        register(LoginScreenProtocol.self) {
            LoginScreenImpl()
        }
        .scope(.unique)
        
        // ... all other registrations
    }
}

App Entry Point (PayRed/PayRedApp.swift):

import Resolver

@main
struct PayRedApp: App {
    @Injected private var mainCoordinator: (any MainAppCoordinatorProtocol)
    
    init() {
        // Wire all dependencies here - happens once at app startup
        Resolver.registerAllServices()
        
        // Optionally resolve dependencies that need initialization at startup
        _ = Resolver.resolve(NotificationManagerUseCaseProtocol.self)
    }
    
    var body: some Scene {
        WindowGroup {
            mainCoordinator.start()
        }
    }
}

Key Rules:

  • ✅ Composition Root knows about all implementations (in DIRegisterar.swift)
  • ✅ Inner layers don't know about Resolver (they use protocols)
  • ✅ Constructors receive abstractions (protocols) via Resolver
  • ✅ Wiring happens once at startup via Resolver.registerAllServices()
  • ✅ Resolver uses the ResolverRegistering protocol pattern for automatic registration discovery

9. Testing Strategy (Per Layer)

Clean Architecture enables testability by design. Each layer has different testing requirements.

9.1 Entities (Innermost)

Test Type: Pure unit tests
Dependencies: None
Framework: XCTest

// Pure business logic tests
func testEmailValidation() {
    let email = Email(value: "test@example.com")
    XCTAssertTrue(email.isValid)
}

Characteristics:

  • Fast (no I/O)
  • No mocks needed
  • Pure business logic validation

9.2 Use Cases (Center)

Test Type: Business rule tests
Dependencies: Mock repositories
Framework: XCTest

// Test business rules with mocked dependencies
func testLoginWithInvalidEmail() {
    let mockRepository = MockLoginRepository()
    let useCase = LoginUseCaseImpl(repository: mockRepository)
    
    XCTAssertThrowsError(try await useCase.login(email: "invalid"))
}

Characteristics:

  • Test business rules
  • Mock repository interfaces
  • No framework dependencies
  • Fast execution

9.3 Interface Adapters (ViewModels)

Test Type: Mapping/formatting tests
Dependencies: Mock Use Cases
Framework: XCTest

// Test data transformation
func testViewModelFormatsError() {
    let mockUseCase = MockLoginUseCase()
    let viewModel = LoginViewModel(useCase: mockUseCase)
    
    // Test that ViewModel formats UseCase error correctly
}

Characteristics:

  • Test data transformation
  • Test UI formatting
  • Mock Use Cases
  • No business logic tests here

9.4 Frameworks & Drivers

Test Type: Integration tests (minimal)
Dependencies: Real frameworks
Framework: XCTest + Test doubles

// Minimal integration tests
func testNetworkServiceMakesRequest() {
    // Test actual network call with test server
}

Characteristics:

  • Minimal testing (frameworks are details)
  • Integration tests only
  • Can be slow
  • Test that implementations work

9.5 Testing Infrastructure: Stubs, Mocks, and Spies

Clean Architecture Principle: Test doubles (stubs, mocks, spies) must be flexible and configurable to enable comprehensive testing without framework dependencies.

9.5.1 Test Target Structure for API Packages

For each API package (containing protocols and data models), create a separate test target that provides test doubles.

Package Structure:

{Feature}API/
├── Package.swift
├── Sources/
│   └── {Feature}API/
│       ├── {Feature}Protocol.swift
│       ├── {Feature}RepositoryProtocol.swift
│       └── {Feature}NameSpace.swift
└── Tests/
    └── {Feature}APITestDoubles/          # Separate test target
        └── Sources/
            └── {Feature}APITestDoubles/
                ├── {Feature}Stub.swift
                ├── {Feature}Mock.swift
                └── {Feature}Spy.swift

Package.swift Configuration:

// Package.swift
let package = Package(
    name: "LoginUseCaseAPI",
    products: [
        .library(name: "LoginUseCaseAPI", targets: ["LoginUseCaseAPI"]),
    ],
    targets: [
        .target(name: "LoginUseCaseAPI"),
        .target(
            name: "LoginUseCaseAPITestDoubles",
            dependencies: ["LoginUseCaseAPI"],  // Depends on main target
            path: "Tests/LoginUseCaseAPITestDoubles"
        ),
    ]
)

9.5.2 Stub, Mock, and Spy Pattern

For each protocol in an API package, create flexible test doubles that can be configured in tests.

Pattern: Combine Stub, Mock, and Spy into a single flexible test double when possible, or separate them if needed.

Example: Flexible Test Double (Stub + Mock + Spy Combined)

// LoginUseCaseAPITestDoubles/Sources/LoginUseCaseAPITestDoubles/LoginUseCaseStub.swift
import LoginUseCaseAPI
import Foundation

/// Flexible test double that combines Stub, Mock, and Spy capabilities
public final class LoginUseCaseStub: LoginUseCaseProtocol {
    
    // MARK: - Configurable Behavior (Stub)
    
    /// Configurable return value for login method
    public var loginResult: Result<Void, Error> = .success(())
    
    /// Configurable state publisher
    public var statePublisherValue: AnyPublisher<LoginUseCaseNameSpace.States, Never> {
        stateSubject.eraseToAnyPublisher()
    }
    
    private let stateSubject = CurrentValueSubject<LoginUseCaseNameSpace.States, Never>(
        .idle(email: "", acceptedTerms: false)
    )
    
    // MARK: - Call Tracking (Spy)
    
    /// Tracks if login was called
    public private(set) var loginCalled = false
    
    /// Tracks login call count
    public private(set) var loginCallCount = 0
    
    /// Tracks last email passed to login
    public private(set) var lastLoginEmail: String?
    
    /// Tracks all emails passed to login
    public private(set) var loginEmails: [String] = []
    
    /// Tracks if acceptTerms was called
    public private(set) var acceptTermsCalled = false
    
    /// Tracks last email passed to acceptTerms
    public private(set) var lastAcceptTermsEmail: String?
    
    /// Tracks if rejectTerms was called
    public private(set) var rejectTermsCalled = false
    
    /// Tracks if clear was called
    public private(set) var clearCalled = false
    
    // MARK: - Configurable Closures (Mock)
    
    /// Closure to execute when login is called (allows custom behavior)
    public var onLogin: ((String) async throws -> Void)?
    
    /// Closure to execute when acceptTerms is called
    public var onAcceptTerms: ((String) async throws -> Void)?
    
    /// Closure to execute when rejectTerms is called
    public var onRejectTerms: ((String) async throws -> Void)?
    
    // MARK: - Reset Method
    
    /// Reset all tracking and configuration
    public func reset() {
        loginCalled = false
        loginCallCount = 0
        lastLoginEmail = nil
        loginEmails = []
        acceptTermsCalled = false
        lastAcceptTermsEmail = nil
        rejectTermsCalled = false
        clearCalled = false
        loginResult = .success(())
        onLogin = nil
        onAcceptTerms = nil
        onRejectTerms = nil
        stateSubject.send(.idle(email: "", acceptedTerms: false))
    }
    
    // MARK: - LoginUseCaseProtocol Implementation
    
    public var statePublisher: AnyPublisher<LoginUseCaseNameSpace.States, Never> {
        statePublisherValue
    }
    
    public func login(email: String) async throws {
        // Spy: Track call
        loginCalled = true
        loginCallCount += 1
        lastLoginEmail = email
        loginEmails.append(email)
        
        // Mock: Execute custom closure if provided
        if let onLogin = onLogin {
            try await onLogin(email)
            return
        }
        
        // Stub: Return configured result
        switch loginResult {
        case .success:
            // Update state to simulate success
            stateSubject.send(.loggingIn(email: email, acceptedTerms: true))
            stateSubject.send(.loggedIn(
                email: email,
                registertaion: .alreadyRegistered,
                acceptedTerms: true
            ))
        case .failure(let error):
            throw error
        }
    }
    
    public func acceptTerms(email: String) async throws {
        acceptTermsCalled = true
        lastAcceptTermsEmail = email
        
        if let onAcceptTerms = onAcceptTerms {
            try await onAcceptTerms(email)
            return
        }
        
        stateSubject.send(.idle(email: email, acceptedTerms: true))
    }
    
    public func rejectTerms(email: String) async throws {
        rejectTermsCalled = true
        
        if let onRejectTerms = onRejectTerms {
            try await onRejectTerms(email)
            return
        }
        
        stateSubject.send(.idle(email: email, acceptedTerms: false))
    }
    
    public func clear() {
        clearCalled = true
        stateSubject.send(.idle(email: "", acceptedTerms: false))
    }
    
    // MARK: - Helper Methods for Testing
    
    /// Manually update state (useful for testing state transitions)
    public func setState(_ state: LoginUseCaseNameSpace.States) {
        stateSubject.send(state)
    }
    
    /// Simulate error state
    public func simulateError(_ error: LoginUseCaseNameSpace.Errors, email: String, acceptedTerms: Bool) {
        stateSubject.send(.error(email: email, acceptedTerms: acceptedTerms, error: error))
    }
}

Example: Repository Test Double

// LoginRepositoryAPITestDoubles/Sources/LoginRepositoryAPITestDoubles/LoginRepositoryStub.swift
import LoginUseCaseAPI
import Foundation

public final class LoginRepositoryStub: LoginUseCaseRepositoryProtocol {
    
    // MARK: - Configurable Behavior
    
    public var loginResult: Result<LoginUseCaseNameSpace.LoginResponse, Error> = .success(.userExist)
    public var saveUserLoginStateResult: Result<Void, Error> = .success(())
    public var doesUserExistResult: Result<Bool, Error> = .success(true)
    
    // MARK: - Call Tracking
    
    public private(set) var loginCalled = false
    public private(set) var loginCallCount = 0
    public private(set) var lastLoginEmail: String?
    
    public private(set) var saveUserLoginStateCalled = false
    public private(set) var lastSaveUserLoginStateEmail: String?
    public private(set) var lastSaveUserLoginStateToken: String?
    public private(set) var lastSaveUserLoginStateUserExist: Bool?
    
    public private(set) var doesUserExistCalled = false
    public private(set) var lastDoesUserExistEmail: String?
    
    // MARK: - Configurable Closures
    
    public var onLogin: ((String) async throws -> LoginUseCaseNameSpace.LoginResponse)?
    public var onSaveUserLoginState: ((String, String?, Bool) async throws -> Void)?
    public var onDoesUserExist: ((String) async throws -> Bool)?
    
    // MARK: - Reset
    
    public func reset() {
        loginCalled = false
        loginCallCount = 0
        lastLoginEmail = nil
        saveUserLoginStateCalled = false
        lastSaveUserLoginStateEmail = nil
        lastSaveUserLoginStateToken = nil
        lastSaveUserLoginStateUserExist = nil
        doesUserExistCalled = false
        lastDoesUserExistEmail = nil
        loginResult = .success(.userExist)
        saveUserLoginStateResult = .success(())
        doesUserExistResult = .success(true)
        onLogin = nil
        onSaveUserLoginState = nil
        onDoesUserExist = nil
    }
    
    // MARK: - LoginUseCaseRepositoryProtocol Implementation
    
    public func login(email: String) async throws -> LoginUseCaseNameSpace.LoginResponse {
        loginCalled = true
        loginCallCount += 1
        lastLoginEmail = email
        
        if let onLogin = onLogin {
            return try await onLogin(email)
        }
        
        switch loginResult {
        case .success(let response):
            return response
        case .failure(let error):
            throw error
        }
    }
    
    public func saveUserLoginState(email: String, temporaryToken: String?, userExist: Bool) async throws {
        saveUserLoginStateCalled = true
        lastSaveUserLoginStateEmail = email
        lastSaveUserLoginStateToken = temporaryToken
        lastSaveUserLoginStateUserExist = userExist
        
        if let onSaveUserLoginState = onSaveUserLoginState {
            try await onSaveUserLoginState(email, temporaryToken, userExist)
            return
        }
        
        switch saveUserLoginStateResult {
        case .success:
            return
        case .failure(let error):
            throw error
        }
    }
    
    public func doesUserExist(email: String) async throws -> Bool {
        doesUserExistCalled = true
        lastDoesUserExistEmail = email
        
        if let onDoesUserExist = onDoesUserExist {
            return try await onDoesUserExist(email)
        }
        
        switch doesUserExistResult {
        case .success(let exists):
            return exists
        case .failure(let error):
            throw error
        }
    }
}

9.5.3 Using Test Doubles in Implementation Tests

Implementation package test target imports both the API package and its test doubles:

// LoginUseCaseImplTests/LoginUseCaseImplTests.swift
import XCTest
import LoginUseCaseAPI
import LoginUseCaseAPITestDoubles  // Import test doubles
import LoginUseCaseImpl

final class LoginUseCaseImplTests: XCTestCase {
    
    var sut: LoginUseCaseImpl!
    var repositoryStub: LoginRepositoryStub!
    
    override func setUp() {
        super.setUp()
        repositoryStub = LoginRepositoryStub()
        sut = LoginUseCaseImpl(repository: repositoryStub)
    }
    
    override func tearDown() {
        repositoryStub.reset()  // Reset between tests
        repositoryStub = nil
        sut = nil
        super.tearDown()
    }
    
    func testLoginWithValidEmail() async throws {
        // Arrange: Configure stub behavior
        repositoryStub.loginResult = .success(.userExist)
        
        // Act
        try await sut.login(email: "test@example.com")
        
        // Assert: Verify behavior and check spy data
        XCTAssertTrue(repositoryStub.loginCalled)
        XCTAssertEqual(repositoryStub.loginCallCount, 1)
        XCTAssertEqual(repositoryStub.lastLoginEmail, "test@example.com")
        XCTAssertTrue(repositoryStub.saveUserLoginStateCalled)
    }
    
    func testLoginWithCustomBehavior() async throws {
        // Arrange: Use closure for custom behavior
        var customBehaviorCalled = false
        repositoryStub.onLogin = { email in
            customBehaviorCalled = true
            return .userNotExist(temporaryToken: "token123")
        }
        
        // Act
        try await sut.login(email: "test@example.com")
        
        // Assert
        XCTAssertTrue(customBehaviorCalled)
        XCTAssertTrue(repositoryStub.loginCalled)
    }
    
    func testLoginHandlesError() async throws {
        // Arrange: Configure error
        repositoryStub.loginResult = .failure(URLError(.notConnectedToInternet))
        
        // Act & Assert
        do {
            try await sut.login(email: "test@example.com")
            XCTFail("Should have thrown error")
        } catch {
            XCTAssertTrue(repositoryStub.loginCalled)
        }
    }
}

9.5.4 Key Principles for Test Doubles

  1. Flexibility: All behavior should be configurable

    • Configurable return values
    • Configurable closures for custom behavior
    • Ability to reset state between tests
  2. Spy Capabilities: Track all interactions

    • Method call tracking
    • Parameter tracking
    • Call count tracking
  3. Stub Capabilities: Provide default behavior

    • Default return values
    • Configurable success/failure scenarios
  4. Mock Capabilities: Allow custom behavior

    • Closure-based customization
    • Dynamic behavior per test
  5. Separation: Test doubles live in API package test targets

    • Implementation tests import API + TestDoubles
    • No test doubles in implementation packages
    • Reusable across all implementation tests

9.5.5 Package.swift Example for Implementation Tests

// LoginUseCaseImpl/Package.swift
let package = Package(
    name: "LoginUseCaseImpl",
    products: [
        .library(name: "LoginUseCaseImpl", targets: ["LoginUseCaseImpl"]),
    ],
    dependencies: [
        .package(path: "../LoginUseCaseAPI"),
    ],
    targets: [
        .target(
            name: "LoginUseCaseImpl",
            dependencies: ["LoginUseCaseAPI"]
        ),
        .testTarget(
            name: "LoginUseCaseImplTests",
            dependencies: [
                "LoginUseCaseImpl",
                "LoginUseCaseAPI",
                .product(name: "LoginUseCaseAPITestDoubles", package: "LoginUseCaseAPI"),  // Import test doubles
            ]
        ),
    ]
)

9.5.6 Benefits of This Approach

Reusability: Test doubles defined once, used everywhere
Flexibility: Fully configurable behavior per test
Maintainability: Changes to protocols automatically reflected in test doubles
Type Safety: Test doubles are strongly typed
Separation: Test infrastructure separate from implementation
Easy Testing: Simple setup and teardown with reset methods


10. Anti-Patterns (Things We Explicitly Avoid)

This section documents common violations of Clean Architecture that we explicitly avoid.

❌ Anti-Pattern 1: Fat ViewModels

What it is: ViewModels containing business logic, validation, or decision-making.

Why it's wrong: Violates Clean Architecture - business rules belong in Use Cases.

Example (WRONG):

class LoginViewModel {
    func validateEmail(_ email: String) -> Bool {
        // ❌ Business validation in ViewModel
        return email.contains("@")
    }
}

Correct approach:

// ✅ Validation in Use Case
class LoginUseCase {
    func login(email: String) async throws {
        guard isValidEmail(email) else {
            throw LoginErrors.invalidEmail
        }
        // ...
    }
}

❌ Anti-Pattern 2: God Repositories

What it is: Repositories that contain business logic or make business decisions.

Why it's wrong: Repositories are abstractions, not business logic containers.

Example (WRONG):

class UserRepository {
    func createUser(email: String) {
        // ❌ Business logic in Repository
        if email.contains("admin") {
            // Business decision
        }
    }
}

Correct approach:

// ✅ Business logic in Use Case
class CreateUserUseCase {
    func createUser(email: String) async throws {
        // Business decision here
        if email.contains("admin") {
            throw UserErrors.adminEmailNotAllowed
        }
        try await repository.saveUser(email: email)
    }
}

❌ Anti-Pattern 3: Domain Models with Codable

What it is: Domain entities conforming to Codable or containing API/database concerns.

Why it's wrong: Domain models must be pure - no framework or persistence pollution.

Example (WRONG):

// ❌ Domain model polluted with framework concerns
struct User: Codable {
    let id: Int  // Database ID
    let email: String
    let createdAt: Date
}

Correct approach:

// ✅ Pure domain model
struct User {
    let email: Email  // Value object
    let createdAt: Date
}

// ✅ Separate DTO for API
struct UserDTO: Codable {
    let id: Int
    let email: String
    let createdAt: String
}

❌ Anti-Pattern 4: SwiftUI Types in Business Logic

What it is: Using SwiftUI types (View, @Published, etc.) in Use Cases or Domain.

Why it's wrong: Frameworks are details - inner layers must not know about them.

Example (WRONG):

// ❌ Framework type in Use Case
import SwiftUI

class LoginUseCase {
    @Published var state: LoginState  // ❌ SwiftUI in Use Case
}

Correct approach:

// ✅ Pure reactive interface (framework-agnostic)
class LoginUseCase {
    var statePublisher: AnyPublisher<LoginState, Never> {
        // Pure Combine, no SwiftUI
    }
}

❌ Anti-Pattern 5: Passing URLSession Inward

What it is: Passing framework types (URLSession, CoreData context) to inner layers.

Why it's wrong: Inner layers must not depend on framework details.

Example (WRONG):

// ❌ Framework type passed to Use Case
class LoginUseCase {
    init(urlSession: URLSession) {  // ❌ Framework dependency
        // ...
    }
}

Correct approach:

// ✅ Abstract interface
protocol NetworkClient {
    func request(_ endpoint: String) async throws -> Data
}

class LoginUseCase {
    init(networkClient: NetworkClient) {  // ✅ Abstraction
        // ...
    }
}

❌ Anti-Pattern 6: Use Cases Knowing About ViewModels

What it is: Use Cases directly referencing or depending on ViewModels.

Why it's wrong: Use Cases are the center - they don't know about outer layers.

Example (WRONG):

// ❌ Use Case knows about ViewModel
class LoginUseCase {
    func login(email: String, viewModel: LoginViewModel) {  // ❌
        // ...
    }
}

Correct approach:

// ✅ Use Case defines output boundary, ViewModel consumes it
class LoginUseCase {
    var statePublisher: AnyPublisher<LoginState, Never> {
        // ViewModel subscribes, Use Case doesn't know about it
    }
}

11. Error Handling (Architected)

Clean Architecture Principle: Errors must be modeled as business concepts, not framework types.

11.1 Error Hierarchy

Domain Errors (UseCaseNameSpace.Errors)
    ↓
Use Case transforms Repository errors to Domain errors
    ↓
ViewModel formats Domain errors for UI
    ↓
View displays formatted error

11.2 Domain Errors (Business Concepts)

Defined in: Use Cases
Type: Enum in UseCaseNameSpace

extension LoginUseCaseNameSpace {
    public enum Errors: LocalizedError {
        case invalidEmailFormat
        case termsNotAccepted
        case userNotFound
        case networkUnavailable
        
        public var errorDescription: String? {
            // Business-friendly error messages
        }
    }
}

Key Points:

  • Errors are business concepts, not HTTP codes
  • Defined in Use Case namespace
  • Framework-agnostic
  • Business-meaningful

11.3 Error Translation

Repository LayerUse Case Layer:

// Repository throws framework error
catch let error as URLError {
    // Use Case translates to domain error
    throw LoginUseCaseNameSpace.Errors.networkUnavailable
}

Use Case LayerViewModel:

// Use Case throws domain error
catch let error as LoginUseCaseNameSpace.Errors {
    // ViewModel formats for UI
    self.errorMessage = error.localizedDescription
}

Forbidden:

  • ❌ Passing HTTP status codes to Use Cases
  • ❌ Using NSError in domain layer
  • ❌ Framework error types in business logic

12. Key Design Patterns

7.1 Protocol-Oriented Programming

  • All layers communicate through protocols
  • Enables easy testing and mocking
  • Supports multiple implementations

7.2 Reactive Programming

  • UseCases use Combine publishers for state
  • ViewModels subscribe to UseCase publishers
  • SwiftUI automatically updates on @Published changes

12.3 Dependency Inversion (The Dependency Rule)

Clean Architecture Core Principle: The Dependency Rule

Source code dependencies can only point inward. Nothing in an inner circle can know anything about something in an outer circle.

How it works:

  1. Inner layers define interfaces (Use Cases define Repository interfaces)
  2. Outer layers implement interfaces (Repository implementations, Data sources)
  3. Dependencies point inward (Repository depends on Use Case's interface, not vice versa)
  4. DI happens at boundaries (Composition Root wires implementations to interfaces)

Example:

// ✅ CORRECT: Use Case (inner) defines interface
// UseCaseAPI
protocol LoginUseCaseRepositoryProtocol {
    func login(email: String) async throws -> LoginResponse
}

// ✅ CORRECT: Repository (outer) implements interface
// RepositoryImpl
extension LoginRepositoryImpl: LoginUseCaseRepositoryProtocol {
    // Implementation
}

// ✅ CORRECT: Dependency points inward
// UseCaseImpl depends on protocol (defined by itself)
class LoginUseCaseImpl {
    private let repository: LoginUseCaseRepositoryProtocol  // Depends on own interface
}

Forbidden:

  • ❌ Use Case depending on RepositoryImpl (concrete class)
  • ❌ Use Case depending on Data implementations
  • ❌ Inner layers knowing about outer layer implementations

12.4 Separation of Concerns (Clean Architecture)

Clean Architecture Separation:

  • Entities (Innermost): Pure business objects, no dependencies
  • Use Cases (Center): Application business rules, depends only on Entities
  • Interface Adapters: Data transformation, depends on Use Cases
  • Frameworks & Drivers (Outermost): Framework details, depends on Interface Adapters

Key Principle: Each layer has a single, well-defined responsibility and depends only on inner layers.


8. Namespace Pattern

Each feature uses a namespace to avoid naming conflicts:

// UseCase namespace
extension LoginUseCaseNameSpace {
    public enum States { ... }
    public enum Errors { ... }
}

// Repository namespace
extension LoginRepositoryNameSpace {
    public struct LoginOrRegisterRemoteResult { ... }
}

8.1 Result Models for Void Methods

When a protocol method returns nothing (void), create a Result model to avoid method signature conflicts. This ensures that methods with the same name from different protocols don't get mixed up.

Pattern: For methods that return Void, create a Result model named {MethodName}Result and return it instead.

Example:

// Protocol definition
public protocol ProfileRepositoryProtocol {
    func fetchProfile() async throws -> ProfileRepositoryNameSpace.FetchProfileResult
}

// Namespace with Result model
extension ProfileRepositoryNameSpace {
    public struct FetchProfileResult {
        public init() {}
    }
}

// Implementation
extension ProfileRepositoryImpl: ProfileRepositoryProtocol {
    public func fetchProfile() async throws -> ProfileRepositoryNameSpace.FetchProfileResult {
        // Perform fetch operation
        // ...
        return ProfileRepositoryNameSpace.FetchProfileResult()
    }
}

Why this pattern?

  • Prevents method signature conflicts when the same method name exists in multiple protocols
  • Makes method signatures unique and distinguishable
  • Allows for future extension if the method needs to return data later
  • Maintains type safety and clarity

Alternative for methods that actually return data:

// If the method returns actual data, use a meaningful Result model
public protocol ProfileRepositoryProtocol {
    func fetchProfile() async throws -> ProfileRepositoryNameSpace.FetchProfileResult
}

extension ProfileRepositoryNameSpace {
    public struct FetchProfileResult {
        public let profile: ProfileEntity
        public let lastUpdated: Date
        
        public init(profile: ProfileEntity, lastUpdated: Date) {
            self.profile = profile
            self.lastUpdated = lastUpdated
        }
    }
}

9. Error Handling

9.1 Error Propagation

Note: The diagram below is also available as a Mermaid file: docs/diagrams/error-propagation-diagram.mmd

flowchart TD
    Start([Data Layer Error]) --> RepoTransform[Repository transforms<br/>to domain error]
    RepoTransform --> UseCaseHandle[UseCase handles error<br/>and updates state]
    UseCaseHandle --> ViewModelFormat[ViewModel formats<br/>error for UI]
    ViewModelFormat --> ViewDisplay[View displays<br/>error alert]
    ViewDisplay --> End([User sees error])
    
    subgraph ErrorTypes["Error Types"]
        DataError["Data Layer Errors<br/>Network, Database, etc."]
        DomainError["Domain Errors<br/>UseCaseNameSpace.Errors"]
        UIError["UI Errors<br/>Formatted in ViewModel"]
    end
    
    Start -.-> DataError
    RepoTransform -.-> DomainError
    ViewModelFormat -.-> UIError
    
    style Start fill:#f44336,color:#fff
    style End fill:#4caf50
    style DataError fill:#ff9800
    style DomainError fill:#ff9800
    style UIError fill:#ff9800
Loading

9.2 Error Types

  • Data Layer Errors: Network errors, database errors, etc.
  • Domain Errors: Defined in UseCase namespace (e.g., LoginUseCaseNameSpace.Errors)
  • UI Errors: Formatted in ViewModel for user display

10. Testing Infrastructure Summary

10.1 Test Doubles Organization

For API Packages:

  • Each API package has a separate test target: {Feature}APITestDoubles
  • Contains Stubs, Mocks, and Spies for all protocols
  • Flexible and fully configurable

For Implementation Packages:

  • Test targets import both API package and its TestDoubles
  • Use test doubles from API packages
  • No test doubles defined in implementation packages

See Section 9.5 for detailed testing infrastructure documentation including:

  • Package structure for test doubles
  • Stub/Mock/Spy pattern implementation
  • Usage examples in implementation tests
  • Configuration and flexibility patterns

10.2 Testability Principles

  • Protocols enable easy mocking - All dependencies are protocols
  • Test doubles are reusable - Defined once in API packages, used everywhere
  • Fully configurable - All behavior can be customized per test
  • Spy capabilities - Track all method calls and parameters
  • Dependency Injection - Allows easy substitution of test doubles
  • State publishers - Can be tested with Combine testing utilities

11. Best Practices

⚠️ Critical Requirements (MUST FOLLOW)

  1. SPM Package Organization (MANDATORY):

    • CRITICAL: When implementing ANY feature, you MUST create SPM packages - this is NOT optional
    • ✅ Each API and Impl must be a separate SPM package with its own Package.swift file
    • DO NOT create Swift files without creating the corresponding SPM package structure first
    • ✅ Follow the feature module structure: Features/{Feature}/{Layer}/{Component}/
    • ✅ API packages define protocols; Impl packages implement them
    • ❌ Never put implementation code in API packages
    • 📖 See Section "CRITICAL REQUIREMENT: SPM Packages Are Mandatory" above for step-by-step instructions
  2. SPM Dependency Rules (ENFORCED AT COMPILE TIME):

    • ✅ API packages can only depend on other API packages
    • ✅ Impl packages depend on their corresponding API package and other API packages
    • ❌ Impl packages never depend on other Impl packages directly
    • ✅ SPM enforces these rules at compile time
  3. Dependency Direction (THE DEPENDENCY RULE):

    • ✅ Dependencies always point inward (Presentation → UseCase → Repository → Data)
    • ✅ Inner layers define interfaces; outer layers implement them
    • ✅ Use Cases define Repository interfaces (not vice versa)

Implementation Guidelines

  1. Protocol-Based Communication:

    • ✅ Always use protocols for inter-layer communication
    • Impl SPM packages implement protocols (interfaces) from API packages
    • Impl SPM packages use (depend on) protocols from API packages in lower layers
  2. Layer Responsibilities:

    • UseCases: Pure business logic, no UI dependencies, define repository interfaces
    • Repositories: Orchestrate data sources, don't put business logic here
    • ViewModels: Thin adapters, delegate to UseCases, format for UI only
    • Views: Display data, handle user input, use Image Cache directly
  3. Technical Standards:

    • ✅ Use async/await for all async operations
    • ✅ Publish state changes through reactive frameworks (Combine, RxSwift, etc.)
    • ✅ Handle errors gracefully at each layer
    • ✅ Use namespaces to avoid naming conflicts
    • ✅ Register dependencies in dependency injection container
  4. Naming Conventions:

    • ✅ Follow pattern: {Feature}{Layer}API and {Feature}{Layer}Impl as SPM packages
    • ✅ Example: LoginUseCaseAPI, LoginUseCaseImpl, LoginRepositoryAPI, LoginRepositoryImpl
  5. Image Handling:

    • Repositories/UseCases: Only pass image URLs, never download images
    • Presentation Layer: Use Image Cache directly to download and cache images from URLs
    • ✅ Image downloading is a UI concern, not a business logic concern

Quick Reference Checklist

When implementing a feature, verify:

  • All components are SPM packages with Package.swift files
  • API packages contain only protocols (no implementation)
  • Impl packages implement protocols from corresponding API packages
  • Dependencies point inward only
  • Use Cases define repository interfaces
  • ViewModels are thin (delegate to UseCases)
  • All dependencies registered in DI container
  • Async operations use async/await
  • State changes published via Combine
  • Errors handled at each layer

12. Example: Complete Feature Implementation

Login Feature Structure (SPM Packages)

Each component is a separate SPM package:

Features/Login/
├── Presentation/
│   └── LoginScreen/
│       ├── LoginScreenAPI/          # SPM Package: Protocol & Dependencies
│       │   ├── Package.swift
│       │   └── Sources/LoginScreenAPI/
│       │       ├── LoginScreenProtocol.swift
│       │       └── LoginScreenDependencies.swift
│       └── LoginScreenImpl/         # SPM Package: View & ViewModel
│           ├── Package.swift
│           └── Sources/LoginScreenImpl/
│               ├── LoginScreenView.swift
│               └── LoginScreenViewModel.swift
│
├── Repositories/
│   └── LoginRepository/
│       ├── LoginRepositoryAPI/      # SPM Package: Data Protocols
│       │   ├── Package.swift
│       │   └── Sources/LoginRepositoryAPI/
│       │       ├── LoginRepositoryRemoteDataProtocol.swift
│       │       ├── LoginRepositoryDataBaseDataProtocol.swift
│       │       └── LoginRepositoryNameSpace.swift
│       └── LoginRepositoryImpl/     # SPM Package: Repository Implementation
│           ├── Package.swift
│           └── Sources/LoginRepositoryImpl/
│               └── LoginRepositoryImpl.swift
│
└── UseCases/
    └── LoginUseCase/
        ├── LoginUseCaseAPI/         # SPM Package: UseCase Protocol & Repository Protocol
        │   ├── Package.swift
        │   └── Sources/LoginUseCaseAPI/
        │       ├── LoginUseCaseProtocol.swift
        │       ├── LoginUseCaseRepositoryProtocol.swift
        │       └── LoginUseCaseNameSpace.swift
        └── LoginUseCaseImpl/         # SPM Package: UseCase Implementation
            ├── Package.swift
            └── Sources/LoginUseCaseImpl/
                └── LoginUseCaseImpl.swift

Data Layer Packages (SPM)

Data layer implementations are SPM packages that implement protocols from RepositoryAPI packages:

Data/
├── MobileGatewayService/
│   └── MobileGatewayServiceImpl/    # SPM Package
│       ├── Package.swift
│       └── Sources/MobileGatewayServiceImpl/
│           ├── NetworkService.swift
│           ├── NetworkService+LoginRepository.swift  # Implements LoginRepositoryRemoteDataProtocol
│           └── NetworkService+CardListRepository.swift
│
├── DataBase/
│   └── DataBase/                    # SPM Package
│       ├── Package.swift
│       └── Sources/DataBase/
│           ├── Database.swift
│           ├── Database+LoginRepository.swift  # Implements LoginRepositoryDataBaseDataProtocol
│           └── Database+CardListRepository.swift
│
├── Keychain/
│   └── KeyChainDataImpl/            # SPM Package
│       ├── Package.swift
│       └── Sources/KeyChainDataImpl/
│           └── KeychainService.swift
│
└── ImageCache/
    └── ImageCacheImpl/               # SPM Package
        ├── Package.swift
        └── Sources/ImageCacheImpl/
            └── ImageCacheImpl.swift

Dependency Flow Summary

Note: The diagram below is also available as a Mermaid file: docs/diagrams/dependency-flow-diagram.mmd

graph TB
    ViewModel["ViewModel"] -->|uses| UseCaseProtocol["UseCaseProtocol<br/>(from UseCaseAPI)"]
    
    UseCaseImpl["UseCaseImpl"] -.implements.-> UseCaseProtocol
    UseCaseImpl -->|uses| RepoProtocol["RepositoryProtocol<br/>(from UseCaseAPI)"]
    
    RepoImpl["RepositoryImpl"] -.implements.-> RepoProtocol
    RepoImpl -->|uses| DataProtocols["DataProtocols<br/>(from RepositoryAPI)"]
    
    RemoteData["Remote Data<br/>Implementation"] -.implements.-> DataProtocols
    DatabaseData["Database<br/>Implementation"] -.implements.-> DataProtocols
    KeychainData["Keychain<br/>Implementation"] -.implements.-> DataProtocols
    
    subgraph Packages["Protocol Packages"]
        UseCaseAPIPkg["UseCaseAPI Package<br/>Contains: UseCaseProtocol<br/>Contains: RepositoryProtocol"]
        RepoAPIPkg["RepositoryAPI Package<br/>Contains: DataProtocols"]
    end
    
    UseCaseProtocol -.defined in.-> UseCaseAPIPkg
    RepoProtocol -.defined in.-> UseCaseAPIPkg
    DataProtocols -.defined in.-> RepoAPIPkg
    
    style ViewModel fill:#e1f5ff
    style UseCaseImpl fill:#fff4e1
    style RepoImpl fill:#e8f5e9
    style RemoteData fill:#fce4ec
    style DatabaseData fill:#fce4ec
    style KeychainData fill:#fce4ec
    style UseCaseAPIPkg fill:#fff9c4
    style RepoAPIPkg fill:#fff9c4
Loading

Key Points:

  • SPM Package Structure: Each API and Impl is a separate SPM package with its own Package.swift
  • Protocol Locations (SPM Packages):
    • RepositoryProtocol is defined in UseCaseAPI SPM package
    • DataProtocols are defined in RepositoryAPI SPM package
  • Implements (SPM Package Dependencies):
    • UseCaseImpl SPM package implements UseCaseProtocol (from UseCaseAPI package)
    • RepositoryImpl SPM package implements RepositoryProtocol (from UseCaseAPI package)
    • Data SPM packages implement DataProtocols (from RepositoryAPI package)
  • Uses/Depends on (SPM Package Dependencies):
    • UseCaseImpl SPM package depends on and uses RepositoryProtocol (from UseCaseAPI package)
    • RepositoryImpl SPM package depends on and uses DataProtocols (from RepositoryAPI package)
    • ScreenImpl SPM package depends on and uses UseCaseProtocol (from UseCaseAPI package)
  • Image Cache: Used directly by Presentation Layer (ScreenImpl packages), not through Repository/UseCase
  • SPM Enforces Boundaries: SPM's dependency resolution ensures the dependency rule is enforced at compile time

📝 Quick Reference Summary for AI/Cursor

When Implementing a Feature - Follow This Order

  1. Create SPM Package Structure (MANDATORY)

  2. Implementation Order (Bottom-Up)

    UseCaseAPI → UseCaseImpl → RepositoryAPI → RepositoryImpl → ScreenAPI → ScreenImpl
    
  3. Key Dependencies

    • UseCaseImpl depends on UseCaseAPI (implements protocol)
    • UseCaseImpl uses RepositoryProtocol from UseCaseAPI (not RepositoryImpl!)
    • RepositoryImpl depends on RepositoryAPI (implements protocol)
    • RepositoryImpl uses DataProtocols from RepositoryAPI
    • ScreenImpl depends on ScreenAPI (implements protocol)
    • ScreenImpl uses UseCaseProtocol from UseCaseAPI (not UseCaseImpl!)
  4. Critical Rules

    • ✅ Use Cases define Repository interfaces (inner defines, outer implements)
    • ✅ Dependencies point inward only
    • ✅ API packages = protocols only, no implementation
    • ✅ Impl packages never depend on other Impl packages
    • ❌ Never put implementation in API packages
    • ❌ Never make outer layers define interfaces for inner layers

Package.swift Quick Templates

API Package:

// swift-tools-version: 5.9
import PackageDescription
let package = Package(
    name: "{Component}API",
    platforms: [.iOS(.v16)],
    products: [.library(name: "{Component}API", targets: ["{Component}API"])],
    dependencies: [], // Only other API packages
    targets: [.target(name: "{Component}API", dependencies: [])]
)

Impl Package:

// swift-tools-version: 5.9
import PackageDescription
let package = Package(
    name: "{Component}Impl",
    platforms: [.iOS(.v16)],
    products: [.library(name: "{Component}Impl", targets: ["{Component}Impl"])],
    dependencies: [.package(path: "../{Component}API")],
    targets: [
        .target(
            name: "{Component}Impl",
            dependencies: [.product(name: "{Component}API", package: "{Component}API")]
        )
    ]
)

Layer Responsibilities

Layer Defines Implements Uses
UseCaseAPI UseCase protocol
Repository protocol
- -
UseCaseImpl - UseCase protocol Repository protocol
RepositoryAPI Data protocols - -
RepositoryImpl - Repository protocol Data protocols
ScreenAPI Screen protocol - -
ScreenImpl - Screen protocol UseCase protocol

Common Patterns

UseCase Pattern:

  • Define protocol in UseCaseAPI
  • Define repository protocol in UseCaseAPI
  • Implement in UseCaseImpl
  • Use Combine publishers for state

Repository Pattern:

  • Implement repository protocol from UseCaseAPI
  • Use data protocols from RepositoryAPI
  • Orchestrate data sources (remote, database, keychain)

ViewModel Pattern:

  • Thin adapter layer
  • Depends on UseCase protocol (not impl)
  • Formats UseCase output for UI
  • No business logic

Conclusion: Clean Architecture Principles

This architecture follows Clean Architecture as defined by Robert C. Martin. It is not "MVVM with layers" — it is a strict application of the Dependency Rule.

Core Principles Enforced

  1. The Dependency Rule: Source code dependencies point inward only
  2. Use Cases Are The Center: All business rules live in Use Cases
  3. Frameworks Are Details: The architecture doesn't know about SwiftUI, URLSession, or CoreData
  4. Boundaries Are Explicit: Input and output boundaries are clearly defined
  5. Testability By Design: Each layer is independently testable

What Makes This Clean Architecture

Inner layers define interfaces (Use Cases define Repository interfaces)
Outer layers implement interfaces (Repositories, Data sources)
Business rules in Use Cases only (not in ViewModels or Repositories)
Framework independence (Use Cases don't know about SwiftUI, Combine specifics)
Pure domain models (no Codable, no framework types)
Composition Root (dependency wiring at outermost layer)
Error modeling (business concepts, not HTTP codes)

What This Is NOT

Not MVVM - ViewModels are interface adapters, not business logic containers
Not "Layered Architecture" - It's dependency-rule architecture
Not Framework-Driven - Frameworks are details, not the architecture
Not "Best Practices" - It's strict architectural rules

The Test

If you can:

  • Replace SwiftUI with UIKit without changing Use Cases → ✅ Clean Architecture
  • Replace URLSession with Alamofire without changing Use Cases → ✅ Clean Architecture
  • Test Use Cases without any framework dependencies → ✅ Clean Architecture
  • Change database without affecting business rules → ✅ Clean Architecture

Then you have Clean Architecture.


Remember: Clean Architecture is not about "what works" — it's about dependency rules and boundaries. The architecture must be independent of frameworks, UI, databases, and external agencies.

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