A modern iOS photo browsing application built with SwiftUI, demonstrating Clean Architecture principles with strict Swift 6 concurrency compliance.
This project follows Clean Architecture with MVVM pattern and unidirectional data flow (MVI-inspired).
Full architecture documentation: ARCHITECTURE.md
UI Layer (Presentation)
↓ depends on
Domain Layer (Business Logic)
↓ depends on
Data Layer (Infrastructure)
- UI Layer: SwiftUI Views + ViewModels (marked
@MainActor) - Domain Layer: Models, Use Cases, Repository protocols (nonisolated)
- Data Layer: Repository implementations, DTOs, Networking (nonisolated)
- Default Actor Isolation:
nonisolated - Strict Concurrency: Complete (enabled)
- ViewModels: Explicitly marked with
@MainActor - SwiftUI Views: Implicitly
@MainActor(no annotation needed) - Data/Domain types: Remain nonisolated for flexibility
- Data and Domain layers (60% of codebase) should work from any isolation domain
- ViewModels explicitly declare their MainActor requirement
- Aligns with Clean Architecture: business logic shouldn't depend on UI concerns
- More scalable as data/domain layers grow
- All async operations use structured concurrency (avoid unstructured
Task {}) - Protocol types crossing actor boundaries should be
Sendable - Value types (structs) with immutable properties are automatically
Sendable - Use
.task { }modifier in SwiftUI for proper lifecycle management
- Multi-line formatting: Always use multi-line formatting when there are 2 or more parameters
- Alphabetical ordering: All parameters must be in alphabetical order
- Exception: Trailing closures should remain at the end and are exempt from alphabetical ordering
- Format example:
func fetchPhotos( limit: Int, sortOrder: SortOrder ) async throws -> [Photo] init( repository: PhotoRepository, useCase: PhotoUseCase ) { self.repository = repository self.useCase = useCase } // Trailing closure exception func performTask( delay: TimeInterval, priority: TaskPriority, operation: () async -> Void ) async { // operation closure remains at end despite alphabetically being first }
- Always include tests for new functionality and changes
- Test coverage: Minimum one success case + one failure/error case per public function
- Test location: Tests must be in corresponding test file (e.g.,
PhotoListViewModel→PhotoListViewModelTests) - Project inclusion: When creating new test files, ensure they are added to the Xcode project file (
Photos.xcodeproj) so they are discovered and run by the test runner
- Never use hard-coded strings in user-facing code
- Always use localized strings from the string catalog (
Resources/Localizable.xcstrings) - Modern Swift 6 syntax: Use static member syntax with
LocalizedStringResource - Key format in catalog:
{Feature}.{Context}.{type}(e.g.,PhotoList.EmptyState.title) - Example:
// ❌ Wrong - Hard-coded string Text("No photos available") // ❌ Wrong - Old style with string literal Text("PhotoList.EmptyState.title", bundle: .main) // ✅ Correct - Modern Swift 6 static member syntax Text(.photoListEmptyStateTitle) Button(.commonRetryLabel) { // action }
- Note: Hard-coded strings are acceptable in test code for readability
Photos/
├── Data/ # Infrastructure layer
│ ├── Models/ # DTOs (e.g., PhotoJSON)
│ ├── Networking/ # Network protocols
│ └── Repositories/ # Repository implementations
├── Domain/ # Business logic layer
│ ├── Models/ # Domain entities (e.g., Photo)
│ └── UseCases/ # Business logic coordination
├── UI/ # Presentation layer
│ ├── PhotoList/ # List feature
│ └── PhotoDetail/ # Detail feature
├── Mocks/ # Test doubles
└── Resources/ # Assets, localization
- ViewModels:
{Feature}ViewModel(e.g.,PhotoListViewModel) - Views:
{Feature}View(e.g.,PhotoListView) - Protocols:
{Domain}{Type}(e.g.,PhotoRepository,PhotoUseCase) - Implementations:
Default{Protocol}(e.g.,DefaultPhotoRepository) - Mocks:
Mock{Protocol}(e.g.,MockPhotoRepository)
- Framework: Swift Testing (not XCTest)
- Syntax: Use
@Testattribute (notfunc test...()) - Assertions: Use
#expect()(notXCTAssert) - Async tests: Mark with
asyncand useawait - MainActor tests: Use
@MainActor @Testfor UI-related tests
- String catalog:
Resources/Localizable.xcstrings - Key format:
{Feature}.{Context}.{type}- Example:
Common.Retry.label - Example:
PhotoList.EmptyState.title
- Example:
- Language: Swift 6.0
- Minimum iOS: 17.0+
- UI Framework: SwiftUI
- Concurrency: Swift Concurrency (async/await, actors)
- Architecture: Clean Architecture + MVVM + MVI (unidirectional data flow)
- Testing: Swift Testing framework
- Networking: URLSession with protocol abstraction
- Dependency Injection: Protocol-based with DIContainer
- All code must compile without warnings under strict concurrency checking
- No
@unchecked Sendablewithout clear justification and documentation - Proper actor isolation boundaries
- No force unwrapping in production code (acceptable in tests with justification)
- Error handling should preserve error context (avoid swallowing errors)
- Dependency Inversion: UI depends on Domain, Domain depends on Data abstractions
- Single Responsibility: Each type has one clear purpose
- Protocol-based abstractions for testability
- Immutable value types preferred over mutable reference types
- ViewModels expose State and handle Actions (unidirectional flow)
Use this agent for comprehensive Swift 6 code reviews focusing on:
- Concurrency safety and data race prevention
- Actor isolation correctness
- Sendable conformance
- Modern iOS best practices
When to use: After completing a logical unit of work (new feature, significant refactor)
Example:
Use the swift6-code-reviewer agent to review my PhotoListViewModel implementation
All development work should follow this three-stage workflow:
-
Architect Agent - Plans the implementation approach
- Ask clarifying questions about requirements, constraints, or ambiguities before planning
- Analyzes requirements and existing codebase
- Designs the solution architecture
- Details how the work should be done
- Identifies potential issues and considerations
- Provides a clear implementation plan
-
Implementation - Executes the plan
- Ask clarifying questions if the architectural plan is unclear or incomplete
- Follows the architectural guidance provided
- Writes code according to project conventions
- Creates tests for new functionality
- Makes small, atomic commits
- Ensures each commit compiles and passes tests
-
Reviewer Agent - Validates the implementation
- Ask clarifying questions about design decisions or implementation choices when needed
- Reviews all changes for correctness and quality
- Checks Swift 6 concurrency compliance
- Verifies adherence to project conventions
- Identifies potential improvements or issues
- Ensures tests adequately cover new functionality
When to use this workflow:
- All non-trivial feature implementations
- Significant refactoring or architectural changes
- Bug fixes that require design consideration
- Any work where planning would improve quality
When you can skip it:
- Trivial changes (typo fixes, formatting)
- Documentation updates
- Simple one-line bug fixes with obvious solutions
- Always read existing code before suggesting changes to understand patterns
- Follow existing conventions (naming, file structure, architecture)
- Maintain Swift 6 compliance - no concurrency warnings allowed
- Preserve immutability - use
letfor dependencies, avoidvarwhere possible - Don't add Sendable to protocols unless there's a specific need (structs infer it automatically)
- ViewModels must be @MainActor - this is already done, don't remove it
- Tests should not mutate ViewModels after initialization
- Make small, atomic commits - each commit should:
- Compile successfully without errors or warnings
- Include test coverage for any new or modified functionality
- Represent a single logical change or unit of work
- Be independently reviewable and revertable
-
ViewModel pattern:
@Observable @MainActor class FeatureViewModel { enum Action { ... } struct State: Equatable { ... } let useCases: UseCases var state: State func send(_ action: Action) async { ... } }
-
Repository pattern:
protocol FeatureRepository: Sendable { func fetch() async throws -> [Model] } struct DefaultFeatureRepository: FeatureRepository { let session: NetworkSession }
-
Use Case pattern:
protocol FeatureUseCase: Sendable { func execute() async throws -> Result } struct DefaultFeatureUseCase: FeatureUseCase { let repository: FeatureRepository }
- Switched from
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActortononisolated(Nov 2024) - All ViewModels already have explicit
@MainActorannotations - Data/Domain layers now correctly default to nonisolated
- PhotoJSON and other DTOs no longer need
nonisolatedannotations
- Swift Evolution SE-0302 - Sendable
- Swift Evolution SE-0337 - Concurrency Migration
- Swift Evolution SE-0470 - Isolated Conformances