| name | ios-app-architecture |
| description | Design scalable iOS app architectures. Use when setting up project structure, implementing MVVM/TCA/VIPER patterns, managing dependencies, organizing modules, or designing data flow. Covers clean architecture, composition, testing strategies, and modularization for maintainable iOS apps. |
iOS App Architecture
Design and implement scalable, testable, and maintainable iOS applications.
Architecture Patterns
MVVM (Recommended for SwiftUI)
View โโ ViewModel โโ Model
โ
Services
struct User: Identifiable, Codable {
let id: UUID
var name: String
var email: String
}
@MainActor
class UserViewModel: ObservableObject {
@Published private(set) var users: [User] = []
@Published private(set) var state: ViewState = .idle
private let userService: UserServiceProtocol
init(userService: UserServiceProtocol = UserService()) {
self.userService = userService
}
func loadUsers() async {
state = .loading
do {
users = try await userService.fetchUsers()
state = .loaded
} catch {
state = .error(error)
}
}
}
struct UserListView: View {
@StateObject private var viewModel = UserViewModel()
var body: some View {
Group {
switch viewModel.state {
case .idle, .loading:
ProgressView()
case .loaded:
List(viewModel.users) { user in
UserRow(user: user)
}
case .error(let error):
ErrorView(error: error)
}
}
.task { await viewModel.loadUsers() }
}
}
The Composable Architecture (TCA)
Best for complex apps needing predictable state management.
import ComposableArchitecture
@Reducer
struct UserFeature {
@ObservableState
struct State: Equatable {
var users: [User] = []
var isLoading = false
}
enum Action {
case loadUsers
case usersResponse(Result<[User], Error>)
}
@Dependency(\.userClient) var userClient
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .loadUsers:
state.isLoading = true
return .run { send in
await send(.usersResponse(
Result { try await userClient.fetchUsers() }
))
}
case .usersResponse(.success(let users)):
state.isLoading = false
state.users = users
return .none
case .usersResponse(.failure):
state.isLoading = false
return .none
}
}
}
}
Project Structure
MyApp/
โโโ App/
โ โโโ MyAppApp.swift
โ โโโ AppDelegate.swift
โโโ Features/
โ โโโ Home/
โ โ โโโ HomeView.swift
โ โ โโโ HomeViewModel.swift
โ โ โโโ Components/
โ โโโ Profile/
โ โโโ Settings/
โโโ Core/
โ โโโ Models/
โ โโโ Services/
โ โ โโโ Networking/
โ โ โโโ Persistence/
โ โโโ Utilities/
โโโ UI/
โ โโโ Components/
โ โโโ Styles/
โ โโโ Extensions/
โโโ Resources/
โโโ Assets.xcassets
โโโ Localizable.strings
Dependency Injection
Protocol-Based DI
protocol UserServiceProtocol {
func fetchUsers() async throws -> [User]
}
class UserService: UserServiceProtocol {
func fetchUsers() async throws -> [User] {
}
}
class MockUserService: UserServiceProtocol {
var usersToReturn: [User] = []
func fetchUsers() async throws -> [User] {
usersToReturn
}
}
Environment-Based DI
class Dependencies: ObservableObject {
let userService: UserServiceProtocol
let analyticsService: AnalyticsProtocol
init(
userService: UserServiceProtocol = UserService(),
analyticsService: AnalyticsProtocol = AnalyticsService()
) {
self.userService = userService
self.analyticsService = analyticsService
}
static let preview = Dependencies(
userService: MockUserService(),
analyticsService: MockAnalyticsService()
)
}
@main
struct MyApp: App {
@StateObject private var dependencies = Dependencies()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(dependencies)
}
}
}
Modularization
Swift Package Structure
Packages/
โโโ Core/
โ โโโ Package.swift
โโโ Networking/
โ โโโ Package.swift
โโโ FeatureHome/
โ โโโ Package.swift
โโโ DesignSystem/
โโโ Package.swift
let package = Package(
name: "FeatureHome",
platforms: [.iOS(.v17)],
products: [
.library(name: "FeatureHome", targets: ["FeatureHome"])
],
dependencies: [
.package(path: "../Core"),
.package(path: "../DesignSystem")
],
targets: [
.target(
name: "FeatureHome",
dependencies: ["Core", "DesignSystem"]
),
.testTarget(
name: "FeatureHomeTests",
dependencies: ["FeatureHome"]
)
]
)
Testing Strategy
Unit Tests
@Test
func testUserViewModel() async {
let mockService = MockUserService()
mockService.usersToReturn = [User(id: UUID(), name: "Test", email: "test@test.com")]
let viewModel = UserViewModel(userService: mockService)
await viewModel.loadUsers()
#expect(viewModel.users.count == 1)
#expect(viewModel.state == .loaded)
}
Snapshot Tests
import SnapshotTesting
func testUserRowSnapshot() {
let view = UserRow(user: .preview)
assertSnapshot(of: view, as: .image(layout: .device(config: .iPhone13)))
}
Best Practices
- Single Responsibility - Each component does one thing well
- Dependency Inversion - Depend on abstractions, not implementations
- Composition over Inheritance - Prefer protocols and structs
- Unidirectional Data Flow - State flows down, actions flow up
- Testability First - Design for testing from the start
Resources
See references/architecture-patterns.md for detailed patterns.
See references/testing-guide.md for testing strategies.