This project uses Clean Architecture with MVI (Model-View-Intent) influence for building maintainable, testable, and scalable iOS applications.
The project follows a layered architecture with clear separation of concerns:
Photos/
├── PhotosApp.swift # App entry point
├── DIContainer.swift # Dependency injection container
├── Domain/ # Business logic layer (pure Swift)
│ ├── Models/ # Domain entities
│ └── UseCases/ # Business use cases
├── Data/ # Data access layer
│ ├── Repositories/ # Repository implementations
│ ├── Models/ # Data transfer objects (DTOs)
│ └── Networking/ # Network abstractions
├── UI/ # Presentation layer (SwiftUI)
│ └── Shared/ # Shared UI components
├── Mocks/ # Mock implementations
│ └── Responses/ # Mock JSON responses
└── Resources/ # App resources
├── Assets.xcassets/
└── Localizable.xcstrings
- Xcode 15.0+
- iOS 17.0+
- Swift 5.9+
- Open
Photos.xcodeprojin Xcode - Build and run the project (⌘R)
Configure these settings in Xcode:
Swift Concurrency:
- Swift Concurrency: Enabled
- Approachable Concurrency: YES
- Default Actor Isolation: MainActor
- Strict Concurrency: Complete
SwiftUI Configuration:
- Enable Previews: YES
- Generate Info.plist: YES
- Scene Manifest Generation: YES
- String Catalog Symbols: YES
For detailed information about the architecture patterns, layer responsibilities, and best practices, see ARCHITECTURE.md.
The app fetches and displays photos from the Lorem Picsum API:
- Endpoint:
https://picsum.photos/v2/list - Features: Photo list with images, author names, and dimensions
- UI: SwiftUI with AsyncImage for photo loading
- Architecture: Full Clean Architecture with Domain, Data, and UI layers
Follow these steps to add a new feature:
// Domain/Models/MyEntity.swift
struct MyEntity: Identifiable, Equatable {
let id: UUID
let name: String
let description: String
}
#if DEBUG
extension MyEntity {
static func fixture(
id: UUID = UUID(),
name: String = "Example",
description: String = "An example entity"
) -> Self {
MyEntity(id: id, name: name, description: description)
}
}
extension Array where Element == MyEntity {
static var fixtures: [MyEntity] {
[
.fixture(name: "First"),
.fixture(name: "Second"),
.fixture(name: "Third")
]
}
}
#endif// Domain/UseCases/MyEntityUseCase.swift
protocol HasMyEntityUseCase {
var myEntityUseCase: MyEntityUseCase { get }
}
protocol MyEntityUseCase {
func fetchEntities() async throws -> [MyEntity]
}
struct DefaultMyEntityUseCase: MyEntityUseCase {
var repository: MyEntityRepository
func fetchEntities() async throws -> [MyEntity] {
try await repository.fetchEntities()
}
}// Data/Repositories/MyEntityRepository.swift
protocol MyEntityRepository {
func fetchEntities() async throws -> [MyEntity]
}
struct DefaultMyEntityRepository: MyEntityRepository {
let session: NetworkSession
func fetchEntities() async throws -> [MyEntity] {
// Implementation here
// See ARCHITECTURE.md for detailed example
}
}// Mocks/MockMyEntityUseCase.swift
#if DEBUG
struct MockMyEntityUseCase: MyEntityUseCase {
var delay: TimeInterval? = nil
var entities: [MyEntity] = .fixtures
var throwError: Bool = false
func fetchEntities() async throws -> [MyEntity] {
if let delay {
try await Task.sleep(for: .seconds(delay))
}
guard !throwError else {
throw MockError.mockError
}
return entities
}
}
#endif// DIContainer.swift
typealias AllUseCases = HasPhotoUseCase & HasMyEntityUseCase
struct DIContainer: AllUseCases {
var photoUseCase: PhotoUseCase
var myEntityUseCase: MyEntityUseCase
}
extension DIContainer {
static func real() -> DIContainer {
let networkSession = URLSession.shared
let photoRepository = DefaultPhotoRepository(session: networkSession)
let photoUseCase = DefaultPhotoUseCase(repository: photoRepository)
let myEntityRepository = DefaultMyEntityRepository(session: networkSession)
let myEntityUseCase = DefaultMyEntityUseCase(repository: myEntityRepository)
return DIContainer(
photoUseCase: photoUseCase,
myEntityUseCase: myEntityUseCase
)
}
static func mock(
mockPhotoUseCase: PhotoUseCase? = nil,
mockMyEntityUseCase: MyEntityUseCase? = nil
) -> DIContainer {
DIContainer(
photoUseCase: mockPhotoUseCase ?? MockPhotoUseCase(),
myEntityUseCase: mockMyEntityUseCase ?? MockMyEntityUseCase()
)
}
}See ARCHITECTURE.md for complete examples of:
- ViewModel with Action/State pattern
- SwiftUI View implementation
- Multiple preview configurations
Run tests with:
# Run all tests
xcodebuild test -scheme Photos
# Run specific test
xcodebuild test -scheme Photos -only-testing:PhotosTests/FeatureViewModelTests- Protocol-Oriented Design: All major components use protocols for testability
- Dependency Injection: Dependencies passed via constructor, configured in DIContainer
- Unidirectional Data Flow: MVI pattern with Actions → State changes
- Layer Separation: UI → Domain → Data (dependencies only flow downward)
- Mock in Main Target: Mocks wrapped in
#if DEBUGfor preview support
- ARCHITECTURE.md - Complete architectural documentation
- Apple Developer Documentation
- Swift.org
[Add your license here]
[Add contribution guidelines here]