ARC Labs Studio - Swift Architecture Patterns
Instructions
Clean Architecture - Three Layers
┌─────────────────────────────────────────────┐
│ PRESENTATION LAYER │
│ SwiftUI Views • ViewModels • Routers │
│ (User Interface & Navigation) │
├─────────────────────────────────────────────┤
│ DOMAIN LAYER │
│ Entities • Use Cases • Protocols │
│ (Business Logic & Rules) │
├─────────────────────────────────────────────┤
│ DATA LAYER │
│ Repositories • Data Sources • DTOs │
│ (Data Access & External Services) │
└─────────────────────────────────────────────┘
▲ DEPENDENCY RULE: Dependencies flow INWARD only ▲
Key Principle: Inner layers NEVER depend on outer layers.
- Presentation can depend on Domain
- Domain NEVER depends on Presentation or Data
- Data can depend on Domain (implements protocols)
Layer Structure
Sources/
├── Presentation/
│ ├── Features/
│ │ └── [FeatureName]/
│ │ ├── View/ # SwiftUI Views (*View.swift)
│ │ ├── ViewModel/ # ViewModels (*ViewModel.swift)
│ │ └── Router/ # Routers (*Router.swift)
│ └── Shared/ # Reusable UI components
│
├── Domain/
│ ├── Entities/ # Core business objects
│ ├── UseCases/ # Business logic (*UseCase.swift)
│ └── Repositories/ # Protocol definitions only
│
└── Data/
├── Repositories/ # Protocol implementations
├── DataSources/ # API clients, Database managers
└── Models/ # DTOs, API responses
MVVM+C Pattern with ARCNavigation
// Route Definition (enum-based, type-safe)
enum AppRoute: Route {
case home
case profile(userID: String)
case settings
@ViewBuilder
func view() -> some View {
switch self {
case .home: HomeView()
case .profile(let userID): ProfileView(userID: userID)
case .settings: SettingsView()
}
}
}
// View (Presentation Layer - pure presentation)
struct UserListView: View {
@Environment(Router<AppRoute>.self) private var router
@State var viewModel: UserListViewModel
var body: some View {
List(viewModel.users) { user in
Button(user.name) {
router.navigate(to: .profile(userID: user.id))
}
}
}
}
// ViewModel (Presentation Layer - coordinates UI state, NO business logic)
// In an app target: @MainActor at class level (WWDC 2025-268 / 306 pattern)
@MainActor
@Observable
final class UserListViewModel {
private(set) var users: [User] = []
private(set) var isLoading = false
private let getUsersUseCase: GetUsersUseCaseProtocol
init(getUsersUseCase: GetUsersUseCaseProtocol) {
self.getUsersUseCase = getUsersUseCase
}
func loadUsers() async {
isLoading = true
users = (try? await getUsersUseCase.execute()) ?? []
isLoading = false
}
}
// Use Case Protocol (Domain Layer)
protocol GetUsersUseCaseProtocol: Sendable {
func execute() async throws -> [User]
}
// Use Case (Domain Layer - ALL business logic lives here)
final class GetUsersUseCase: GetUsersUseCaseProtocol, Sendable {
private let repository: UserRepositoryProtocol
init(repository: UserRepositoryProtocol) {
self.repository = repository
}
func execute() async throws -> [User] {
let users = try await repository.getUsers()
return users.filter { $0.isActive } // Business rule
}
}
// Entity (Domain Layer - pure business object)
struct User: Identifiable, Equatable, Sendable {
let id: UUID
let name: String
let email: String
let isActive: Bool
}
// Repository Protocol (Domain Layer)
protocol UserRepositoryProtocol: Sendable {
func getUsers() async throws -> [User]
}
// Repository Implementation (Data Layer)
final class UserRepositoryImpl: UserRepositoryProtocol {
private let remoteDataSource: UserRemoteDataSourceProtocol
func getUsers() async throws -> [User] {
let dtos = try await remoteDataSource.fetchUsers()
return dtos.map { $0.toDomain() }
}
}
Use Case Patterns
Single-responsibility (default):
protocol GetUsersUseCaseProtocol: Sendable {
func execute() async throws -> [User]
}
Grouped Use Case (when actions share dependencies and are semantically related):
protocol UserFavoritesUseCaseProtocol: Sendable {
func execute(_ action: UserFavoritesAction) async throws
}
enum UserFavoritesAction: Sendable {
case add(restaurantID: UUID)
case remove(restaurantID: UUID)
case check(restaurantID: UUID)
}
Use grouped Use Cases ONLY when:
- Actions share the same dependencies (same repositories)
- Actions are semantically related (CRUD on the same resource)
- Splitting would create near-duplicate classes with identical dependencies
Testing rule: Every UseCase MUST have corresponding unit tests using Swift Testing.
Concurrency Guidelines (@MainActor)
ARC Labs follows the progressive concurrency path from WWDC 2025-268 "Embracing Swift concurrency" and the ViewModel patterns in WWDC 2025-306 "Optimize SwiftUI performance with Instruments".
In an app target:
- Declare ViewModels as
@MainActor @Observable final class— Apple's recommended pattern for app modules (WWDC 2025-306 Landmarks sample). - Never apply
@MainActorto Use Cases or Repository implementations — they are Domain/Data layer and must stay actor-agnostic. - Move work off the main thread with
@concurrenton a method,actortypes for subsystems, ornonisolatedfor stateless helpers.
In a Swift package:
- Default to
nonisolated— package consumers may call from any actor. - Use
@MainActoronly when justified (SwiftUI navigation primitives, SwiftData@Modelconstraint, UI-bound APIs). - Never apply blanket
@MainActorto a class that can benonisolatedor anactor.
// ✅ App ViewModel — class-level @MainActor (WWDC 2025-306 pattern)
@MainActor
@Observable
final class UserListViewModel {
private(set) var users: [User] = []
private let getUsersUseCase: GetUsersUseCaseProtocol
func loadUsers() async {
users = (try? await getUsersUseCase.execute()) ?? []
}
}
// ✅ Use Case — always actor-agnostic, NEVER @MainActor
final class GetUsersUseCase: GetUsersUseCaseProtocol, Sendable {
func execute() async throws -> [User] { /* ... */ }
}
// ❌ Package class forced to main actor — wrong for reusable code
// @MainActor public final class ARCNetworkClient { }
Swift Design Principles
ARC Labs expresses design through six Swift-native principles (see @references/swift-design-principles.md):
- Value Semantics by Default: Structs first; classes only when identity or inheritance is needed
- Protocol-Driven Abstraction: Protocols are the primary abstraction — not abstract base classes
- Composition Over Inheritance: Protocol extensions + struct composition replace class hierarchies
- Well-Defined Ownership: Every piece of state has one explicit owner;
private(set)enforces this - Structured Concurrency:
async/await+ actors + TaskGroup; no manualDispatchQueue - Compile-Time Correctness: Zero warnings, no force unwraps, no
@unchecked Sendable
SOLID remains a useful reference: S is reinforced by Clean Architecture; O and L are transformed (protocols replace inheritance); I dissolves (Swift protocols are focused by design); D is preserved (protocol injection).
Dependency Injection Pattern
// Composition Root (App level)
@MainActor
final class AppDependencies {
func makeUserListView() -> UserListView {
let repository = RemoteUserRepository()
let useCase = GetUsersUseCase(repository: repository)
let viewModel = UserListViewModel(getUsersUseCase: useCase)
return UserListView(viewModel: viewModel)
}
}
When Singletons Are Appropriate
Use singletons ONLY for truly global, unique resources (hardware, config, analytics). Always wrap with protocols for testability:
protocol LocationServiceProtocol: Sendable {
func getCurrentLocation() async throws -> Location
}
final class LocationService: LocationServiceProtocol {
static let shared = LocationService()
private init() {}
func getCurrentLocation() async throws -> Location { /* ... */ }
}
References
For complete patterns, examples, and guidelines:
- @references/swift-design-principles.md - ARC Labs foundational design principles (value semantics, POP, composition, ownership, concurrency, compile-time correctness)
- @references/clean-architecture.md - Complete Clean Architecture guide with examples
- @references/mvvm-c.md - MVVM+Coordinator pattern implementation details
- @references/solid-principles.md - SOLID principles applied to Swift (read via swift-design-principles.md lens)
- @references/protocol-oriented.md - Protocol-Oriented Programming best practices
- @references/singletons.md - When and how to use singletons safely
- @references/domain.md - Domain layer: Entities, Use Cases, Repository Protocols
Common Mistakes
- Business logic in Views or ViewModels (move to Use Cases)
- ViewModels depending on concrete Repository implementations
- Domain layer importing UIKit or SwiftUI
- Data layer containing business logic
- Massive ViewModels doing everything
- Global singletons without protocol abstraction
- Direct navigation without Router (NavigationLink, .sheet)
- Reverse dependencies (Domain importing Presentation/Data)
- Blanket
@MainActoron entire classes (apply per-method) @MainActoron Use Cases or Repository implementations- Private methods inside type body (use
private extensioninstead)
Examples
Designing a new feature module
User says: "Design the architecture for a restaurant search feature"
- Create Domain layer:
SearchRestaurantsUseCase,Restaurantentity,RestaurantRepositoryProtocol - Create Data layer:
RestaurantRepositoryImpl,RestaurantDTO,RestaurantRemoteDataSource - Create Presentation:
SearchView,SearchViewModelwith@Observable - Wire up via
AppDependenciescomposition root - Result: Complete feature with Clean Architecture layers and DI
Reviewing code for architectural compliance
User says: "Review this ViewModel for architecture issues"
- Check for business logic (filtering, validation) — should be in UseCase
- Check
@MainActorusage — per-method only, not blanket - Check dependencies — protocols, not concrete types
- Check private methods — must be in
private extension - Result: List of violations with recommended fixes
Related Skills
| If you need... | Use |
|---|---|
| Data layer implementation | /arc-data-layer |
| Presentation layer patterns | /arc-presentation-layer |
| Testing patterns | /arc-tdd-patterns |
| Code quality standards | /arc-quality-standards |