AtomicArch is a modern iOS application that demonstrates clean architecture principles and best practices in Swift development. The project showcases a well-structured, modular approach to building iOS applications with a focus on maintainability, testability, and scalability.
- High-Level Design (HLD): docs/HIGH_LEVEL_DESIGN.md
The primary goal of AtomicArch is to serve as a reference implementation of Clean Architecture in a modern iOS application. It is designed to be a practical guide for developers looking to build scalable, maintainable, and testable apps using Swift, Combine, and modern concurrency.
The project follows Clean Architecture principles with a clear separation of concerns across four distinct layers:
-
Presentation Layer (
Features/)- UIKit-based views and view controllers
- ViewModels for state management using Combine
- Coordinators for navigation flow management
- Builders for dependency injection and view creation
-
Domain Layer (
Domain/)- Use case protocols and implementations
- Domain entities (pure business models)
- Repository interfaces
- Domain errors and validation
-
Data Layer (
Data/)- Repository implementations
- Data models (API response models)
- API endpoints and network configuration
- Data transformation logic
-
Infrastructure Layer
Networking/: HTTP client and network utilitiesAtomicLogger/: Logging systemAtomicCore/: Core utilities, protocols, and extensions
classDiagram
direction TB
%% Presentation Layer - ViewModels
class ListUserGitHubViewModel {
-viewState: ViewState
-users: [UserEntity]
-userUseCase: UserUseCase
+transform(input: Input) Output
+loadUsers(since: Int) async
+loadMoreIfNeeded(for: UserEntity)
}
class UserDetailViewModel {
-viewState: ViewState
-user: UserDetailEntity?
-userUseCase: UserUseCase
-username: String
+transform(input: Input) Output
+loadUser() async
}
%% Presentation Layer - ViewControllers
class ListUserGitHubViewController {
-viewModel: ListUserGitHubViewModel
+delegate: ListUserGitHubViewControllerDelegate?
}
class UserDetailViewController {
-viewModel: UserDetailViewModel
+delegate: UserDetailViewControllerDelegate?
}
%% Presentation Layer - Coordinators
class ListUserGitHubCoordinator {
-children: [Coordinator]
-router: Router
-userUseCase: UserUseCase
-delegate: ListUserGitHubCoordinatorDelegate?
+present(animated: Bool, onDismissed: (() -> Void)?)
+didSelectUser(user: UserEntity)
}
class UserDetailCoordinator {
-children: [Coordinator]
-router: Router
-user: UserEntity
-userUseCase: UserUseCase
-delegate: UserDetailCoordinatorDelegate?
+present(animated: Bool, onDismissed: (() -> Void)?)
+userDetailViewControllerDidFinish(_: UserDetailViewController)
}
%% Domain Layer - Protocols and Entities
class UserUseCase {
<<protocol>>
+getListUser(perPage: Int, since: Int) async throws [UserEntity]
+getUser(with loginUsername: String) async throws UserDetailEntity
}
class UserRepository {
<<protocol>>
+getListUser(perPage: Int, since: Int) async throws [UserEntity]
+getUser(with loginUsername: String) async throws UserDetailEntity
}
class UserEntity {
-id: UUID
-login: String
-avatarUrl: String
-htmlUrl: String
}
class UserDetailEntity {
-id: Int
-login: String
-name: String
-company: String
-blog: String
-location: String
-email: String
-bio: String
-publicRepos: Int
-publicGists: Int
-followers: Int
-following: Int
}
%% Data Layer - Implementations
class UserUseCaseImpl {
-repository: UserRepository
+getListUser(perPage: Int, since: Int) async throws [UserEntity]
+getUser(with loginUsername: String) async throws UserDetailEntity
}
class UserRepositoryImpl {
-networkService: NetworkService
+getListUser(perPage: Int, since: Int) async throws [UserEntity]
+getUser(with loginUsername: String) async throws UserDetailEntity
}
%% Data Layer - Models and Endpoints
class UserResponse {
-id: Int
-login: String?
-avatarUrl: String?
-htmlUrl: String?
+toDomain() UserEntity
}
class UserDetailResponse {
-id: Int
-login: String?
-name: String?
-company: String?
-blog: String?
-location: String?
-email: String?
-bio: String?
-publicRepos: Int?
-publicGists: Int?
-followers: Int?
-following: Int?
+toDomain() UserDetailEntity
}
class UserEndpoint {
+getListUser(Int, Int)
+getUserDetail(String)
}
%% Infrastructure Layer
class NetworkService {
<<protocol>>
+request<T: Decodable>(_ target: Target) async throws T
}
class Target {
<<protocol>>
+path: String
+method: HTTPMethod
+task: Task
+headers: [String: String]
}
class NetworkError {
<<enum>>
+noConnection
+invalidResponse
+serverError
+decodingError
+unknown
}
%% Relationships
ListUserGitHubViewController ..> ListUserGitHubViewModel : uses
UserDetailViewController ..> UserDetailViewModel : uses
ListUserGitHubViewModel ..> UserUseCase : uses
UserDetailViewModel ..> UserUseCase : uses
UserUseCaseImpl ..|> UserUseCase : implements
UserUseCaseImpl --> UserRepository : uses
UserRepositoryImpl ..|> UserRepository : implements
UserRepositoryImpl --> NetworkService : uses
UserRepositoryImpl --> UserEndpoint : uses
UserRepositoryImpl --> UserResponse : transforms
UserRepositoryImpl --> UserDetailResponse : transforms
UserResponse --> UserEntity : toDomain()
UserDetailResponse --> UserDetailEntity : toDomain()
NetworkService ..> Target : defines request target
NetworkService ..> NetworkError : throws
ListUserGitHubCoordinator ..|> Coordinator : implements
ListUserGitHubCoordinator --> Router : uses
ListUserGitHubCoordinator --> UserUseCase : uses
ListUserGitHubCoordinator --> ListUserGitHubViewController : presents
ListUserGitHubCoordinator --o UserDetailCoordinator : presents child
ListUserGitHubCoordinator ..> ListUserGitHubCoordinatorDelegate : delegates to
UserDetailCoordinator ..|> Coordinator : implements
UserDetailCoordinator --> Router : uses
UserDetailCoordinator --> UserUseCase : uses
UserDetailCoordinator --> UserDetailViewController : presents
UserDetailCoordinator ..> UserDetailCoordinatorDelegate : delegates to
Coordinator o-- Coordinator : manages children
- User List: Display GitHub users with infinite scroll pagination
- User Details: View comprehensive user profiles with detailed information
- Asynchronous Loading: Efficient image loading and data fetching
- Error Handling: Comprehensive error states with retry mechanisms
- State Management: Reactive UI updates using Combine framework
- Clean Architecture: Strict separation of concerns across layers
- Protocol-Oriented Design: Dependency inversion through protocols
- Async/Await: Modern concurrency patterns throughout the app
- Combine Integration: Reactive programming for state management
- Dependency Injection: Builder pattern for clean dependency management
- Coordinator Pattern: Clean navigation flow management
- Unit Testing: Comprehensive test coverage with mocking
AtomicArch/
βββ Application/ # App lifecycle and configuration
β βββ AppDelegate.swift
β βββ SceneDelegate.swift
β βββ AppConfig.swift
β βββ Environment.swift
βββ Features/ # Feature modules (Presentation layer)
β βββ Users/
β βββ List/
β βββ Detail/
β βββ Builders/
βββ Domain/ # Business logic (Domain layer)
β βββ Entities/
β βββ Interfaces/
β β βββ Repositories/
β βββ UseCase/
β βββ Protocol/
βββ Data/ # Data layer implementation
β βββ Models/
β βββ Repository/
β βββ Network/
β βββ Endpoint/
β βββ Interceptor/
βββ Router/ # Navigation infrastructure
βββ Networking/ # Networking module (Swift Package)
βββ AtomicLogger/ # Logging module (Swift Package)
βββ AtomicCore/ # Core utilities (Swift Package)
βββ AtomicArchTests/ # Test suite
βββ Unit/
β βββ ViewModel/
β βββ UseCase/
β βββ Repository/
βββ Helpers/
βββ Mocks/
- Language: Swift 5.9+
- UI Framework: UIKit with programmatic UI
- Architecture: Clean Architecture with MVVM
- State Management: Combine framework
- Networking: URLSession with async/await
- Dependency Injection: Builder pattern
- Navigation: Coordinator pattern
- Testing: XCTest with comprehensive mocking
- Dependency Management: Swift Package Manager
- Code Quality: SwiftLint, SwiftFormat
The project implements a comprehensive testing strategy focused on business logic and user interactions:
- ViewModel Tests: State management, user interactions, error handling, and pagination logic
- Use Case Tests: Business logic validation and data transformation
- Repository Tests: Data layer integration and error handling
AtomicArchTests/
βββ Unit/
β βββ ViewModel/
β β βββ ListUserGitHubViewModelTests.swift
β β βββ UserDetailViewModelTests.swift
β βββ UseCase/
β β βββ UserUseCaseImplTests.swift
β βββ Repository/
β βββ UserRepositoryImplTests.swift
βββ Helpers/
β βββ TestData.swift
β βββ XCTest+Async.swift
βββ Mocks/
βββ UserUseCaseMock.swift
βββ UserRepositoryMock.swift
A test plan is provided: AtomicBTestPlan.xctestplan.
- Isolation: Each test is independent with proper mocking
- Coverage: Focus on business logic and user interactions
- Maintainability: Clear test structure with descriptive names
- Performance: Fast execution with minimal dependencies
The project uses GitHub Actions for continuous integration:
-
Pre-commit Hooks
- Code formatting (SwiftFormat)
- Linting (SwiftLint)
-
Pull Request Checks
- Lint (SwiftLint using .swiftlint.yaml)
- Build and run tests on macOS runner
-
Main Branch CI
- On push to main, build and run tests
-
Release
- Pushing a tag matching v*.. triggers a release
- Release notes are generated from a template (.github/release-template.md) and changelog
- High-Level Design (HLD): See docs/HIGH_LEVEL_DESIGN.md for the full system design and module interactions
- Architecture Documentation: This README provides comprehensive architecture overview
- API Documentation: Inline documentation for all public APIs
- Testing Guidelines: Clear testing patterns and best practices
- Contribution Guidelines: Development workflow and standards
-
Prerequisites
- Xcode 15.0+
- Swift 5.9+
- iOS 18.5+ Simulator
-
Installation
git clone https://github.com/phanquangcong/AtomicArch.git cd AtomicArch xcodebuild -resolvePackageDependencies -project AtomicArch.xcodeproj -
Running the App
xcodebuild -scheme AtomicArch -destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=latest' buildOr open the project in Xcode, choose the AtomicArch scheme and an iOS 18.5+ simulator, then Run (βR).
-
Running Tests
ruby Scripts/run-tests.rb
The script picks an available simulator (booted first, else first iPhone). Or run directly:
xcodebuild test -scheme AtomicArch -destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=latest'
In Xcode you can use the test plan
AtomicBTestPlan.xctestplanor run tests with βU.
- SwiftFormat and SwiftLint are configured for consistent style and quality.
- Pre-commit is set up to run these tools automatically.
Setup locally:
- Install tools (once): SwiftFormat, SwiftLint, and pre-commit
- Install the hooks: pre-commit install
- Run on all files (optional): pre-commit run --all-files
Configs:
- SwiftLint: .swiftlint.yaml
- SwiftFormat: .swiftformat (with Swift version in .swift-version)
- Follow Clean Architecture principles
- Write comprehensive unit tests
- Maintain code documentation
- Use SwiftLint and SwiftFormat
- Follow the established naming conventions
We welcome contributions! Please follow these steps to get your environment ready and keep changes consistent with the projectβs standards.
Install the code quality tools and Git hooks helper:
# macOS via Homebrew
brew install pre-commit swiftlint swiftformat
# Alternatively (for pre-commit)
pipx install pre-commit # or: pip install --user pre-commitpre-commit install
# Optional: run on the full repo once
pre-commit run --all-filesPre-commit will automatically format with SwiftFormat and lint with SwiftLint on each commit.
# Format
swiftformat .
# Lint
swiftlint lint --config .swiftlint.yaml
# Tests (uses first available simulator)
ruby Scripts/run-tests.rb- Keep changes focused and include tests where applicable
- Ensure pre-commit, lint, and tests pass locally
- Open a PR to
main; CI will run PR checks (lint + build/test)
This project is licensed under the MIT License β see the LICENSE file for details.
