# IOS Development

> iOS native development expertise with Swift, SwiftUI, UIKit, and modern Apple platform patterns. Use when building iOS applications, designing UI with SwiftUI, implementing MVVM architecture, or integrating Apple frameworks. Use when this capability is needed.

- Skill: `tomevault-io/ios-development` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/ios-development`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/ios-development/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/ios-development

---


# iOS Development

Structured guidance for building native iOS applications with Swift, SwiftUI, UIKit, and modern Apple platform patterns. Covers project setup, declarative UI, navigation, UIKit interop, architecture, data persistence, networking, Apple framework integration, and testing strategies for production iOS applications.

## When to Use This Skill

Use this skill for:

- Setting up a new iOS project with proper Xcode configuration and Swift Package Manager dependencies
- Building declarative user interfaces with SwiftUI views, modifiers, and state management
- Implementing navigation with NavigationStack, sheets, alerts, and deep linking
- Working with UIKit view controllers, table views, collection views, and Auto Layout
- Designing MVVM architecture with @Observable, coordinators, and dependency injection
- Persisting data with SwiftData, Core Data, UserDefaults, or Keychain
- Building async/await networking layers with URLSession and Codable serialization
- Integrating Apple frameworks such as HealthKit, MapKit, StoreKit 2, or App Intents
- Writing unit tests, UI tests, and snapshot tests for iOS applications

**Trigger phrases**: "iOS app", "SwiftUI", "UIKit", "Swift Package Manager", "MVVM iOS", "SwiftData", "Core Data", "HealthKit", "MapKit", "StoreKit", "App Intents", "XCTest", "XCUITest", "NavigationStack", "async await networking", "Codable", "@Observable", "@State", "@Binding", "UICollectionView", "diffable data source", "Keychain", "push notification", "background task"

## What This Skill Does

Provides iOS development patterns including:

- **Project Setup**: Xcode project configuration, Swift Package Manager, module organization, build settings, Info.plist
- **SwiftUI Fundamentals**: Views, modifiers, property wrappers, @State/@Binding/@Observable, previews
- **Layouts and Navigation**: VStack/HStack/ZStack, LazyStacks, NavigationStack, sheets, alerts, deep linking
- **UIKit Patterns**: View controller lifecycle, diffable data sources, Auto Layout, UIKit-SwiftUI interop
- **Architecture**: MVVM with @Observable, Coordinator pattern, dependency injection, Repository pattern
- **Data and Networking**: SwiftData, Core Data, Keychain, URLSession async/await, Codable
- **Apple Frameworks**: Notifications, background tasks, HealthKit, MapKit, StoreKit 2, App Intents
- **Testing**: XCTest, Swift Testing, XCUITest, snapshot testing, async test patterns, protocol-based mocking

## Instructions

### Step 1: Project Structure and Configuration

A well-organized iOS project separates features into modules, configures build settings for each environment, and manages dependencies through Swift Package Manager.

**Recommended Project Structure**:

```
MyApp/
  MyApp.xcodeproj
  MyApp/
    App/
      MyAppApp.swift              -- @main entry point
      AppDelegate.swift           -- UIKit lifecycle hooks (if needed)
      Info.plist
    Features/
      Home/
        HomeView.swift
        HomeViewModel.swift
      Settings/
        SettingsView.swift
        SettingsViewModel.swift
    Core/
      Networking/
        APIClient.swift
        Endpoint.swift
      Persistence/
        DataStore.swift
      Models/
        User.swift
        Transaction.swift
    SharedUI/
      Components/
        PrimaryButton.swift
        LoadingOverlay.swift
      Modifiers/
        ShimmerModifier.swift
    Resources/
      Assets.xcassets
      Localizable.xcstrings
  MyAppTests/
    Features/
      HomeViewModelTests.swift
    Core/
      APIClientTests.swift
    Helpers/
      TestFixtures.swift
  MyAppUITests/
    HomeFlowTests.swift
    SettingsFlowTests.swift
  Packages/
    MyAppKit/                     -- local Swift package for shared logic
      Package.swift
      Sources/MyAppKit/
      Tests/MyAppKitTests/
```

**App Entry Point** (SwiftUI lifecycle):

```swift
import SwiftUI
import SwiftData

@main
struct MyAppApp: App {
    private let container: ModelContainer

    init() {
        do {
            let schema = Schema([User.self, Transaction.self])
            let configuration = ModelConfiguration(
                "MyApp",
                schema: schema,
                isStoredInMemoryOnly: false
            )
            container = try ModelContainer(for: schema, configurations: [configuration])
        } catch {
            fatalError("Failed to create ModelContainer: \(error)")
        }
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
                .modelContainer(container)
        }
    }
}
```

**Swift Package Manager Configuration** (local package):

```swift
// swift-tools-version: 6.0
import PackageDescription

let package = Package(
    name: "MyAppKit",
    platforms: [.iOS(.v17)],
    products: [
        .library(name: "MyAppKit", targets: ["MyAppKit"]),
    ],
    dependencies: [
        .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.0.0"),
        .package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0"),
    ],
    targets: [
        .target(
            name: "MyAppKit",
            dependencies: [
                .product(name: "Dependencies", package: "swift-dependencies"),
                .product(name: "Algorithms", package: "swift-algorithms"),
            ]
        ),
        .testTarget(
            name: "MyAppKitTests",
            dependencies: ["MyAppKit"]
        ),
    ]
)
```

**Build Configuration with xcconfig Files**:

```
// Shared.xcconfig
IPHONEOS_DEPLOYMENT_TARGET = 17.0
SWIFT_VERSION = 6.0
SWIFT_STRICT_CONCURRENCY = complete
ENABLE_USER_SCRIPT_SANDBOXING = YES

// Debug.xcconfig
#include "Shared.xcconfig"
SWIFT_OPTIMIZATION_LEVEL = -Onone
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG
OTHER_SWIFT_FLAGS = -warn-concurrency

// Release.xcconfig
#include "Shared.xcconfig"
SWIFT_OPTIMIZATION_LEVEL = -O
SWIFT_COMPILATION_MODE = wholemodule
ENABLE_TESTABILITY = NO
```

**Info.plist Essentials**:

```xml
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <false/>
</dict>
<key>NSCameraUsageDescription</key>
<string>We need camera access to scan documents.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>We use your location to show nearby results.</string>
<key>UIBackgroundModes</key>
<array>
    <string>fetch</string>
    <string>remote-notification</string>
</array>
```

**Key Project Setup Principles**:

- Set the deployment target to iOS 17+ to use @Observable and SwiftData without backward-compatibility shims
- Enable Swift 6 strict concurrency checking (`SWIFT_STRICT_CONCURRENCY = complete`) from day one to catch data races at compile time
- Use local Swift packages to extract shared logic into testable modules with explicit dependency boundaries
- Keep the main app target thin: it should wire together feature modules but contain minimal logic itself
- Configure separate xcconfig files for Debug and Release to avoid conditional compilation scattered through code

### Step 2: SwiftUI Fundamentals

SwiftUI uses a declarative syntax where views are lightweight value types that describe the desired UI state. The framework diffs the view hierarchy and updates only what changed.

**View Composition and Modifiers**:

```swift
import SwiftUI

struct TransactionRow: View {
    let transaction: Transaction
    @Environment(\.dynamicTypeSize) private var typeSize

    var body: some View {
        HStack(spacing: 12) {
            Image(systemName: transaction.category.iconName)
                .font(.title2)
                .foregroundStyle(transaction.category.color)
                .frame(width: 40, height: 40)
                .background(transaction.category.color.opacity(0.12))
                .clipShape(Circle())

            VStack(alignment: .leading, spacing: 2) {
                Text(transaction.merchantName)
                    .font(.body)
                    .fontWeight(.medium)
                    .lineLimit(1)

                Text(transaction.date.formatted(date: .abbreviated, time: .shortened))
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }

            Spacer()

            Text(transaction.amount, format: .currency(code: transaction.currencyCode))
                .font(.body.monospacedDigit())
                .foregroundStyle(transaction.amount < 0 ? .primary : .green)
        }
        .padding(.vertical, 4)
        .accessibilityElement(children: .combine)
        .accessibilityLabel("\(transaction.merchantName), \(transaction.amount.formatted(.currency(code: transaction.currencyCode))), \(transaction.date.formatted(date: .abbreviated, time: .shortened))")
    }
}
```

**State Management with @State, @Binding, and @Observable**:

```swift
import SwiftUI
import Observation

@Observable
final class AuthViewModel {
    var email = ""
    var password = ""
    var isLoading = false
    var errorMessage: String?

    private let authService: AuthServiceProtocol

    init(authService: AuthServiceProtocol) {
        self.authService = authService
    }

    func signIn() async {
        guard !email.isEmpty, !password.isEmpty else {
            errorMessage = "Email and password are required."
            return
        }

        isLoading = true
        errorMessage = nil

        do {
            try await authService.signIn(email: email, password: password)
        } catch let error as AuthError {
            errorMessage = error.userFacingMessage
        } catch {
            errorMessage = "An unexpected error occurred. Please try again."
        }

        isLoading = false
    }
}

struct SignInView: View {
    @State private var viewModel: AuthViewModel

    init(authService: AuthServiceProtocol) {
        _viewModel = State(initialValue: AuthViewModel(authService: authService))
    }

    var body: some View {
        Form {
            Section {
                TextField("Email", text: $viewModel.email)
                    .textContentType(.emailAddress)
                    .keyboardType(.emailAddress)
                    .autocorrectionDisabled()
                    .textInputAutocapitalization(.never)

                SecureField("Password", text: $viewModel.password)
                    .textContentType(.password)
            }

            if let errorMessage = viewModel.errorMessage {
                Section {
                    Label(errorMessage, systemImage: "exclamationmark.triangle")
                        .foregroundStyle(.red)
                }
            }

            Section {
                Button {
                    Task { await viewModel.signIn() }
                } label: {
                    if viewModel.isLoading {
                        ProgressView()
                            .frame(maxWidth: .infinity)
                    } else {
                        Text("Sign In")
                            .frame(maxWidth: .infinity)
                    }
                }
                .disabled(viewModel.isLoading)
            }
        }
        .navigationTitle("Sign In")
    }
}
```

**Custom Property Wrapper for UserDefaults**:

```swift
import SwiftUI

@propertyWrapper
struct AppStorage<Value: Codable> {
    private let key: String
    private let defaultValue: Value
    private let store: UserDefaults

    init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) {
        self.key = key
        self.defaultValue = wrappedValue
        self.store = store
    }

    var wrappedValue: Value {
        get {
            guard let data = store.data(forKey: key),
                  let decoded = try? JSONDecoder().decode(Value.self, from: data) else {
                return defaultValue
            }
            return decoded
        }
        set {
            if let data = try? JSONEncoder().encode(newValue) {
                store.set(data, forKey: key)
            }
        }
    }
}
```

**SwiftUI Previews with Sample Data**:

```swift
#Preview("Transaction Row - Expense") {
    List {
        TransactionRow(transaction: .preview(
            merchantName: "Whole Foods Market",
            amount: -87.32,
            category: .groceries
        ))
        TransactionRow(transaction: .preview(
            merchantName: "Monthly Salary",
            amount: 5200.00,
            category: .income
        ))
    }
}

extension Transaction {
    static func preview(
        merchantName: String = "Preview Merchant",
        amount: Double = -25.00,
        category: TransactionCategory = .general,
        currencyCode: String = "USD"
    ) -> Transaction {
        Transaction(
            id: UUID(),
            merchantName: merchantName,
            amount: Decimal(amount),
            currencyCode: currencyCode,
            category: category,
            date: .now
        )
    }
}
```

**Key SwiftUI Principles**:

- Use `@Observable` (iOS 17+) instead of `ObservableObject`/`@Published` for simpler, more efficient observation with fine-grained tracking
- Keep views small and composable. Extract subviews when a view exceeds 40 lines or when a section is reused
- Always provide accessibility labels for non-text elements and use `.accessibilityElement(children: .combine)` for composite rows
- Use `@State` for view-local state, `@Binding` for child-to-parent communication, and `@Environment` for shared values
- Prefer the `format:` parameter on `Text` for locale-aware formatting of numbers, dates, and currencies

### Step 3: SwiftUI Layouts and Navigation

SwiftUI provides stack-based layouts for composition, lazy containers for performance with large data sets, and NavigationStack for type-safe, path-based navigation.

**Stack-Based Layouts**:

```swift
import SwiftUI

struct DashboardView: View {
    let accounts: [Account]
    let recentTransactions: [Transaction]

    var body: some View {
        ScrollView {
            VStack(spacing: 20) {
                // Horizontal scrolling account cards
                ScrollView(.horizontal, showsIndicators: false) {
                    LazyHStack(spacing: 16) {
                        ForEach(accounts) { account in
                            AccountCard(account: account)
                                .containerRelativeFrame(
                                    .horizontal,
                                    count: 1,
                                    spacing: 16
                                )
                        }
                    }
                    .scrollTargetLayout()
                }
                .scrollTargetBehavior(.viewAligned)
                .contentMargins(.horizontal, 20)

                // Recent transactions list
                LazyVStack(alignment: .leading, spacing: 0) {
                    Section {
                        ForEach(recentTransactions) { transaction in
                            TransactionRow(transaction: transaction)
                            if transaction.id != recentTransactions.last?.id {
                                Divider()
                                    .padding(.leading, 52)
                            }
                        }
                    } header: {
                        Text("Recent Transactions")
                            .font(.headline)
                            .padding(.horizontal, 20)
                            .padding(.bottom, 8)
                    }
                }
            }
            .padding(.vertical)
        }
    }
}

struct AccountCard: View {
    let account: Account

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            HStack {
                Text(account.name)
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
                Spacer()
                Image(systemName: account.type.iconName)
                    .foregroundStyle(.secondary)
            }

            Text(account.balance, format: .currency(code: account.currencyCode))
                .font(.title.bold().monospacedDigit())

            Text("Updated \(account.lastUpdated, format: .relative(presentation: .named))")
                .font(.caption2)
                .foregroundStyle(.tertiary)
        }
        .padding()
        .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16))
    }
}
```

**NavigationStack with Type-Safe Path-Based Routing**:

```swift
import SwiftUI

enum AppRoute: Hashable {
    case transactionDetail(Transaction.ID)
    case accountDetail(Account.ID)
    case settings
    case profile
}

@Observable
final class Router {
    var path = NavigationPath()

    func navigate(to route: AppRoute) {
        path.append(route)
    }

    func popToRoot() {
        path = NavigationPath()
    }

    func pop() {
        guard !path.isEmpty else { return }
        path.removeLast()
    }
}

struct ContentView: View {
    @State private var router = Router()
    @State private var selectedTab: Tab = .home

    var body: some View {
        TabView(selection: $selectedTab) {
            NavigationStack(path: $router.path) {
                HomeView()
                    .navigationDestination(for: AppRoute.self) { route in
                        switch route {
                        case .transactionDetail(let id):
                            TransactionDetailView(transactionID: id)
                        case .accountDetail(let id):
                            AccountDetailView(accountID: id)
                        case .settings:
                            SettingsView()
                        case .profile:
                            ProfileView()
                        }
                    }
            }
            .tabItem { Label("Home", systemImage: "house") }
            .tag(Tab.home)

            NavigationStack {
                SearchView()
            }
            .tabItem { Label("Search", systemImage: "magnifyingglass") }
            .tag(Tab.search)
        }
        .environment(router)
    }
}
```

**Sheets, Alerts, and Confirmations**:

```swift
import SwiftUI

struct TransactionDetailView: View {
    let transactionID: Transaction.ID
    @State private var showDeleteConfirmation = false
    @State private var showEditSheet = false
    @State private var alertItem: AlertItem?

    var body: some View {
        List {
            // Transaction detail sections...
        }
        .navigationTitle("Transaction")
        .toolbar {
            ToolbarItem(placement: .primaryAction) {
                Menu {
                    Button("Edit", systemImage: "pencil") {
                        showEditSheet = true
                    }
                    Button("Delete", systemImage: "trash", role: .destructive) {
                        showDeleteConfirmation = true
                    }
                } label: {
                    Image(systemName: "ellipsis.circle")
                }
            }
        }
        .sheet(isPresented: $showEditSheet) {
            EditTransactionView(transactionID: transactionID)
                .presentationDetents([.medium, .large])
                .presentationDragIndicator(.visible)
        }
        .confirmationDialog(
            "Delete Transaction",
            isPresented: $showDeleteConfirmation,
            titleVisibility: .visible
        ) {
            Button("Delete", role: .destructive) {
                Task { await deleteTransaction() }
            }
        } message: {
            Text("This action cannot be undone.")
        }
        .alert(item: $alertItem) { item in
            Alert(
                title: Text(item.title),
                message: Text(item.message),
                dismissButton: .default(Text("OK"))
            )
        }
    }

    private func deleteTransaction() async {
        // deletion logic
    }
}

struct AlertItem: Identifiable {
    let id = UUID()
    let title: String
    let message: String
}
```

**Key Layout and Navigation Principles**:

- Use `LazyVStack` and `LazyHStack` for lists with more than a few dozen items. Lazy stacks create views on demand as they scroll into the viewport
- Use `NavigationStack` with `NavigationPath` for programmatic, type-safe navigation. Avoid the deprecated `NavigationView`
- Centralize routing logic in a `Router` object injected via `@Environment` so that any view can trigger navigation without passing closures through the hierarchy
- Use `.presentationDetents` on sheets to control their height. Half-height sheets (`.medium`) work well for quick forms
- Prefer `confirmationDialog` over `alert` for destructive actions because it presents as an action sheet on iPhone

### Step 4: UIKit Patterns

UIKit remains essential for complex custom layouts, advanced collection view compositions, and brownfield projects. Understanding view controller lifecycle, modern diffable data sources, and UIKit-SwiftUI interop is critical.

**View Controller Lifecycle**:

```swift
import UIKit

final class TransactionsViewController: UIViewController {
    private let viewModel: TransactionsViewModel
    private var collectionView: UICollectionView!
    private var dataSource: UICollectionViewDiffableDataSource<Section, Transaction.ID>!

    enum Section: Int, CaseIterable {
        case pending
        case completed
    }

    init(viewModel: TransactionsViewModel) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) is not supported")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        title = "Transactions"
        configureCollectionView()
        configureDataSource()
        bindViewModel()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        Task { await viewModel.loadTransactions() }
    }

    // MARK: - Collection View Setup

    private func configureCollectionView() {
        var configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
        configuration.headerMode = .supplementary
        configuration.trailingSwipeActionsConfigurationProvider = { [weak self] indexPath in
            self?.trailingSwipeActions(for: indexPath)
        }

        let layout = UICollectionViewCompositionalLayout.list(using: configuration)
        collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(collectionView)

        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: view.topAnchor),
            collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])
    }

    private func trailingSwipeActions(for indexPath: IndexPath) -> UISwipeActionsConfiguration? {
        guard let transactionID = dataSource.itemIdentifier(for: indexPath) else {
            return nil
        }
        let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { [weak self] _, _, completion in
            Task {
                await self?.viewModel.deleteTransaction(id: transactionID)
                completion(true)
            }
        }
        return UISwipeActionsConfiguration(actions: [deleteAction])
    }
}
```

**Diffable Data Source with Cell Registration**:

```swift
extension TransactionsViewController {
    private func configureDataSource() {
        let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Transaction.ID> {
            [weak self] cell, indexPath, transactionID in

            guard let transaction = self?.viewModel.transaction(for: transactionID) else { return }

            var content = cell.defaultContentConfiguration()
            content.text = transaction.merchantName
            content.secondaryText = transaction.amount.formatted(
                .currency(code: transaction.currencyCode)
            )
            content.image = UIImage(systemName: transaction.category.iconName)
            content.imageProperties.tintColor = UIColor(transaction.category.color)
            cell.contentConfiguration = content

            cell.accessories = [.disclosureIndicator()]
        }

        let headerRegistration = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(
            elementKind: UICollectionView.elementKindSectionHeader
        ) { [weak self] headerView, _, indexPath in
            guard let section = Section(rawValue: indexPath.section) else { return }
            var content = headerView.defaultContentConfiguration()
            content.text = section == .pending ? "Pending" : "Completed"
            headerView.contentConfiguration = content
        }

        dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) {
            collectionView, indexPath, transactionID in
            collectionView.dequeueConfiguredReusableCell(
                using: cellRegistration, for: indexPath, item: transactionID
            )
        }

        dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
            collectionView.dequeueConfiguredReusableSupplementary(
                using: headerRegistration, for: indexPath
            )
        }
    }

    private func bindViewModel() {
        viewModel.onTransactionsChanged = { [weak self] pending, completed in
            guard let self else { return }
            var snapshot = NSDiffableDataSourceSnapshot<Section, Transaction.ID>()
            snapshot.appendSections(Section.allCases)
            snapshot.appendItems(pending.map(\.id), toSection: .pending)
            snapshot.appendItems(completed.map(\.id), toSection: .completed)
            self.dataSource.apply(snapshot, animatingDifferences: true)
        }
    }
}
```

**UIKit-SwiftUI Interop with UIHostingController**:

```swift
import SwiftUI
import UIKit

// Embedding SwiftUI in UIKit
final class SettingsHostingController: UIHostingController<SettingsView> {
    init(viewModel: SettingsViewModel) {
        let settingsView = SettingsView(viewModel: viewModel)
        super.init(rootView: settingsView)
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) is not supported")
    }
}

// Embedding UIKit in SwiftUI
struct MapViewRepresentable: UIViewRepresentable {
    let region: MKCoordinateRegion
    let annotations: [MKAnnotation]

    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView()
        mapView.delegate = context.coordinator
        return mapView
    }

    func updateUIView(_ mapView: MKMapView, context: Context) {
        mapView.setRegion(region, animated: true)
        mapView.removeAnnotations(mapView.annotations)
        mapView.addAnnotations(annotations)
    }

    func makeCoordinator() -> Coordinator {
        Coordinator()
    }

    final class Coordinator: NSObject, MKMapViewDelegate {
        func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
            let identifier = "CustomPin"
            let view = mapView.dequeueReusableAnnotationView(withIdentifier: identifier)
                ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: identifier)
            view.annotation = annotation
            return view
        }
    }
}
```

**Key UIKit Principles**:

- Mark `init(coder:)` as `@available(*, unavailable)` on programmatic view controllers to prevent accidental storyboard instantiation
- Use `UICollectionViewDiffableDataSource` instead of the older `UITableViewDataSource` delegate pattern. Diffable data sources eliminate index-out-of-bounds crashes and provide smooth animations
- Use compositional layout (`UICollectionViewCompositionalLayout`) for all new collection views. It handles complex grid, list, and waterfall layouts without custom `UICollectionViewFlowLayout` subclasses
- Bridge UIKit views into SwiftUI with `UIViewRepresentable` and SwiftUI views into UIKit with `UIHostingController`. Always implement the coordinator pattern for delegate callbacks

### Step 5: Architecture Patterns

A clean architecture separates UI, business logic, and data access into distinct layers. On iOS, MVVM with @Observable provides the best balance of testability and SwiftUI integration.

**MVVM with @Observable**:

```swift
import Foundation
import Observation

// MARK: - Protocol for dependency injection and testing

protocol TransactionRepositoryProtocol: Sendable {
    func fetchTransactions(for accountID: Account.ID) async throws -> [Transaction]
    func deleteTransaction(id: Transaction.ID) async throws
}

// MARK: - ViewModel

@Observable
@MainActor
final class TransactionListViewModel {
    private(set) var transactions: [Transaction] = []
    private(set) var isLoading = false
    private(set) var error: AppError?

    private let repository: TransactionRepositoryProtocol
    private let accountID: Account.ID

    init(accountID: Account.ID, repository: TransactionRepositoryProtocol) {
        self.accountID = accountID
        self.repository = repository
    }

    func loadTransactions() async {
        isLoading = true
        error = nil

        do {
            transactions = try await repository.fetchTransactions(for: accountID)
        } catch {
            self.error = AppError(underlying: error)
        }

        isLoading = false
    }

    func deleteTransaction(at offsets: IndexSet) async {
        let idsToDelete = offsets.map { transactions[$0].id }

        // Optimistic UI update
        var removedTransactions: [(Int, Transaction)] = []
        for offset in offsets.sorted().reversed() {
            removedTransactions.append((offset, transactions[offset]))
            transactions.remove(at: offset)
        }

        do {
            for id in idsToDelete {
                try await repository.deleteTransaction(id: id)
            }
        } catch {
            // Rollback on failure
            for (index, transaction) in removedTransactions.reversed() {
                transactions.insert(transaction, at: index)
            }
            self.error = AppError(underlying: error)
        }
    }
}

// MARK: - View

struct TransactionListView: View {
    @State private var viewModel: TransactionListViewModel

    init(accountID: Account.ID, repository: TransactionRepositoryProtocol) {
        _viewModel = State(initialValue: TransactionListViewModel(
            accountID: accountID,
            repository: repository
        ))
    }

    var body: some View {
        Group {
            if viewModel.isLoading && viewModel.transactions.isEmpty {
                ProgressView("Loading transactions...")
            } else if let error = viewModel.error, viewModel.transactions.isEmpty {
                ContentUnavailableView {
                    Label("Unable to Load", systemImage: "exclamationmark.triangle")
                } description: {
                    Text(error.userMessage)
                } actions: {
                    Button("Retry") {
                        Task { await viewModel.loadTransactions() }
                    }
                }
            } else {
                List {
                    ForEach(viewModel.transactions) { transaction in
                        TransactionRow(transaction: transaction)
                    }
                    .onDelete { offsets in
                        Task { await viewModel.deleteTransaction(at: offsets) }
                    }
                }
                .refreshable {
                    await viewModel.loadTransactions()
                }
            }
        }
        .task { await viewModel.loadTransactions() }
        .navigationTitle("Transactions")
    }
}
```

**Coordinator Pattern for Navigation**:

```swift
import UIKit

protocol Coordinator: AnyObject {
    var childCoordinators: [any Coordinator] { get set }
    var navigationController: UINavigationController { get }
    func start()
}

final class AppCoordinator: Coordinator {
    var childCoordinators: [any Coordinator] = []
    let navigationController: UINavigationController
    private let dependencyContainer: DependencyContainer

    init(navigationController: UINavigationController, dependencyContainer: DependencyContainer) {
        self.navigationController = navigationController
        self.dependencyContainer = dependencyContainer
    }

    func start() {
        let homeCoordinator = HomeCoordinator(
            navigationController: navigationController,
            dependencyContainer: dependencyContainer
        )
        homeCoordinator.delegate = self
        childCoordinators.append(homeCoordinator)
        homeCoordinator.start()
    }
}

extension AppCoordinator: HomeCoordinatorDelegate {
    func homeCoordinatorDidRequestTransactionDetail(_ coordinator: HomeCoordinator, transactionID: Transaction.ID) {
        let detailCoordinator = TransactionDetailCoordinator(
            navigationController: navigationController,
            transactionID: transactionID,
            dependencyContainer: dependencyContainer
        )
        childCoordinators.append(detailCoordinator)
        detailCoordinator.start()
    }
}
```

**Dependency Container**:

```swift
import Foundation

@MainActor
final class DependencyContainer: Sendable {
    private let apiClient: APIClient
    private let modelContainer: ModelContainer

    init(apiClient: APIClient, modelContainer: ModelContainer) {
        self.apiClient = apiClient
        self.modelContainer = modelContainer
    }

    func makeTransactionRepository() -> TransactionRepositoryProtocol {
        TransactionRepository(apiClient: apiClient, modelContainer: modelContainer)
    }

    func makeAuthService() -> AuthServiceProtocol {
        AuthService(apiClient: apiClient)
    }

    func makeTransactionListViewModel(accountID: Account.ID) -> TransactionListViewModel {
        TransactionListViewModel(
            accountID: accountID,
            repository: makeTransactionRepository()
        )
    }
}
```

**Key Architecture Principles**:

- Define protocols for all services and repositories. View models depend on protocols, not concrete types, enabling test doubles
- Mark view models `@MainActor` and `@Observable`. The `@MainActor` annotation guarantees all property updates happen on the main thread, which SwiftUI requires
- Use optimistic UI updates for delete and toggle operations, rolling back if the server call fails
- Use the `.task` modifier to kick off async work when a view appears. SwiftUI cancels the task automatically when the view disappears
- The Coordinator pattern is most valuable in UIKit-heavy apps. In pure SwiftUI apps, the Router pattern from Step 3 serves the same purpose with less boilerplate

### Step 6: Data Persistence and Networking

iOS apps typically need local persistence for offline support and caching, secure storage for credentials, and a networking layer for API communication.

**SwiftData Model and CRUD Operations**:

```swift
import Foundation
import SwiftData

@Model
final class Transaction {
    @Attribute(.unique) var id: UUID
    var merchantName: String
    var amount: Decimal
    var currencyCode: String
    var category: TransactionCategory
    var date: Date
    var note: String?

    @Relationship(deleteRule: .nullify, inverse: \Account.transactions)
    var account: Account?

    init(
        id: UUID = UUID(),
        merchantName: String,
        amount: Decimal,
        currencyCode: String,
        category: TransactionCategory,
        date: Date,
        note: String? = nil
    ) {
        self.id = id
        self.merchantName = merchantName
        self.amount = amount
        self.currencyCode = currencyCode
        self.category = category
        self.date = date
        self.note = note
    }
}

@Model
final class Account {
    @Attribute(.unique) var id: UUID
    var name: String
    var balance: Decimal
    var currencyCode: String
    var lastUpdated: Date

    @Relationship(deleteRule: .cascade)
    var transactions: [Transaction] = []

    init(id: UUID = UUID(), name: String, balance: Decimal, currencyCode: String) {
        self.id = id
        self.name = name
        self.balance = balance
        self.currencyCode = currencyCode
        self.lastUpdated = .now
    }
}

// SwiftData queries in SwiftUI
struct TransactionListSwiftDataView: View {
    @Query(
        filter: #Predicate<Transaction> { $0.amount < 0 },
        sort: \Transaction.date,
        order: .reverse
    )
    private var expenses: [Transaction]

    @Environment(\.modelContext) private var modelContext

    var body: some View {
        List {
            ForEach(expenses) { transaction in
                TransactionRow(transaction: transaction)
            }
            .onDelete(perform: deleteTransactions)
        }
    }

    private func deleteTransactions(at offsets: IndexSet) {
        for index in offsets {
            modelContext.delete(expenses[index])
        }
    }
}
```

**Keychain Wrapper for Secure Storage**:

```swift
import Foundation
import Security

enum KeychainError: Error {
    case duplicateItem
    case itemNotFound
    case unexpectedStatus(OSStatus)
    case invalidData
}

struct KeychainManager {
    static func save(_ data: Data, for key: String, accessGroup: String? = nil) throws {
        var query: [CFString: Any] = [
            kSecClass: kSecClassGenericPassword,
            kSecAttrAccount: key,
            kSecValueData: data,
            kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
        ]
        if let accessGroup {
            query[kSecAttrAccessGroup] = accessGroup
        }

        let status = SecItemAdd(query as CFDictionary, nil)
        if status == errSecDuplicateItem {
            let updateQuery: [CFString: Any] = [
                kSecClass: kSecClassGenericPassword,
                kSecAttrAccount: key,
            ]
            let updateAttributes: [CFString: Any] = [kSecValueData: data]
            let updateStatus = SecItemUpdate(updateQuery as CFDictionary, updateAttributes as CFDictionary)
            guard updateStatus == errSecSuccess else {
                throw KeychainError.unexpectedStatus(updateStatus)
            }
        } else if status != errSecSuccess {
            throw KeychainError.unexpectedStatus(status)
        }
    }

    static func load(for key: String) throws -> Data {
        let query: [CFString: Any] = [
            kSecClass: kSecClassGenericPassword,
            kSecAttrAccount: key,
            kSecReturnData: true,
            kSecMatchLimit: kSecMatchLimitOne,
        ]

        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)
        guard status == errSecSuccess else {
            if status == errSecItemNotFound {
                throw KeychainError.itemNotFound
            }
            throw KeychainError.unexpectedStatus(status)
        }
        guard let data = result as? Data else {
            throw KeychainError.invalidData
        }
        return data
    }

    static func delete(for key: String) throws {
        let query: [CFString: Any] = [
            kSecClass: kSecClassGenericPassword,
            kSecAttrAccount: key,
        ]
        let status = SecItemDelete(query as CFDictionary)
        guard status == errSecSuccess || status == errSecItemNotFound else {
            throw KeychainError.unexpectedStatus(status)
        }
    }
}
```

**Async/Await Networking Layer**:

```swift
import Foundation

enum HTTPMethod: String {
    case get = "GET"
    case post = "POST"
    case put = "PUT"
    case delete = "DELETE"
}

struct Endpoint {
    let path: String
  

…(truncated)
