Swift & iOS

Build a modern iOS codebase around feature boundaries, repositories, injected dependencies, and testable composition.

~17 min reading
Suggest content

Modern iOS Codebase Blueprint

🟪 Syntax · 🟦 Types/APIs · 🟩 Correct pattern · 🟥 Bug · 🟨 Gotcha · 🧠 Explanation

This is a small, practical reference layout for a SwiftUI-first app with a repository boundary. Keep the same boundaries in a UIKit app; replace the view layer and keep the domain/data responsibilities.

Current toolchain snapshot

Reviewed October 11, 2026: Swift 6.4, Xcode 27, and the iOS 27 SDK are the current major toolchain baseline. The deployment target is a separate product decision. Mark newer APIs with availability checks and test the oldest supported OS. Apple had iOS 27.2 beta available in September; do not make a beta-only API part of a production contract without an explicit reason.

Example feature structure

TEXT
App/
  AppEntry.swift
  AppDependencies.swift
Core/
  Networking/
  Persistence/
  DesignSystem/
Features/
  Catalog/
    Data/
    Domain/
    Presentation/
  Profile/
    Data/
    Domain/
    Presentation/
Packages/
  CatalogFeature/
  SharedNetworking/

Start with a small module set. Split into Swift packages when ownership, build time, reuse, or independent testing justifies the boundary. Avoid both one giant Core dumping ground and a package for every tiny file.

Dependency direction

TEXT
SwiftUI Screen -> Screen Model -> Feature Repository Protocol
                                   ^
                                   |
                         Live Data Implementation
                                   |
                     API Client / SwiftData / Keychain

The UI depends on a screen-facing API. The repository implementation owns DTO decoding, transport, persistence, caching, and mapping decisions. Domain types should not depend on URLSession, SwiftData, or API field names. For the full rationale and tradeoffs behind the seam, see the canonical dependency inversion and injection answer.

Domain model and DTO

SWIFT
import Foundation
​
struct Product: Identifiable, Equatable, Sendable {
    let id: UUID
    let name: String
    let price: Decimal
}
​
struct ProductDTO: Decodable, Sendable {
    let id: UUID
    let displayName: String
    let priceInCents: Int
​
    func domainModel() -> Product {
        Product(
            id: id,
            name: displayName,
            price: Decimal(priceInCents) / 100
        )
    }
}

Keep transport shape in the DTO. Convert cents to a domain price at the boundary; do not make every view remember the API's unit or spelling.

API client and repository

SWIFT
import Foundation
​
enum APIError: Error, Equatable {
    case invalidResponse
    case httpStatus(Int)
}
​
protocol APIClient: Sendable {
    func get<Response: Decodable & Sendable>(
        _ url: URL,
        as responseType: Response.Type
    ) async throws -> Response
}
​
struct URLSessionAPIClient: APIClient {
    let session: URLSession
​
    func get<Response: Decodable & Sendable>(
        _ url: URL,
        as responseType: Response.Type
    ) async throws -> Response {
        let (data, response) = try await session.data(from: url)
        guard let http = response as? HTTPURLResponse else {
            throw APIError.invalidResponse
        }
        guard (200..<300).contains(http.statusCode) else {
            throw APIError.httpStatus(http.statusCode)
        }
        return try JSONDecoder().decode(responseType, from: data)
    }
}
​
protocol ProductRepository: Sendable {
    func products() async throws -> [Product]
}
​
struct LiveProductRepository: ProductRepository {
    let api: any APIClient
    let endpoint: URL
​
    func products() async throws -> [Product] {
        let response: [ProductDTO] = try await api.get(endpoint, as: [ProductDTO].self)
        return response.map { $0.domainModel() }
    }
}

In production, add request construction, authentication, timeout policy, typed decoding errors, retry rules, and observability at the networking boundary. Do not retry every error automatically; retry only operations and failures that are safe to repeat.

Screen model and explicit view state

SWIFT
import Observation
​
enum CatalogState: Equatable {
    case idle
    case loading
    case loaded([Product])
    case failed(message: String)
}
​
@MainActor
@Observable
final class CatalogModel {
    private(set) var state: CatalogState = .idle
    private let repository: any ProductRepository
​
    init(repository: any ProductRepository) {
        self.repository = repository
    }
​
    func load() async {
        state = .loading
        do {
            let products = try await repository.products()
            state = .loaded(products)
        } catch is CancellationError {
            state = .idle
        } catch {
            state = .failed(message: "Couldn't load products.")
        }
    }
}

The state enum makes loading, content, and failure mutually exclusive. If the feature needs stale content while refreshing, model that explicitly (for example, isRefreshing plus the last successful data) rather than squeezing two meanings into a single boolean.

Compose dependencies once

This is the concrete app-wiring example for the dependency injection concept, not a separate definition of it.

SWIFT
import SwiftUI
​
struct AppDependencies {
    let catalogRepository: any ProductRepository
​
    static func live() -> AppDependencies {
        let api = URLSessionAPIClient(session: .shared)
        let endpoint = URL(string: "https://api.example.com/products")!
        return AppDependencies(
            catalogRepository: LiveProductRepository(api: api, endpoint: endpoint)
        )
    }
}
​
@main
struct StudyApp: App {
    private let dependencies = AppDependencies.live()
​
    var body: some Scene {
        WindowGroup {
            CatalogRoot(repository: dependencies.catalogRepository)
        }
    }
}
​
struct CatalogRoot: View {
    @State private var model: CatalogModel
​
    init(repository: any ProductRepository) {
        _model = State(initialValue: CatalogModel(repository: repository))
    }
​
    var body: some View {
        CatalogScreen(model: model)
            .task { await model.load() }
    }
}
​
struct CatalogScreen: View {
    let model: CatalogModel
​
    var body: some View {
        switch model.state {
        case .idle, .loading:
            ProgressView()
        case .loaded(let products) where products.isEmpty:
            ContentUnavailableView("No products", systemImage: "shippingbox")
        case .loaded(let products):
            List(products) { product in
                Text(product.name)
            }
        case .failed(let message):
            ContentUnavailableView {
                Label("Unable to load", systemImage: "wifi.exclamationmark")
            } description: {
                Text(message)
            } actions: {
                Button("Try Again") {
                    Task { await model.load() }
                }
            }
        }
    }
}

The force unwrap in AppDependencies.live() is only acceptable if the URL is a literal that is guaranteed valid; otherwise validate configuration with a throwing initializer or a failable construction path. In app code, avoid hiding configuration failure behind !.

Persistence choices

DataTypical storage
Small non-sensitive preferencesUserDefaults / @AppStorage
Credentials and tokensKeychain
Structured local model dataSwiftData or Core Data
Large files and mediaFile system; cache with Caches when reproducible
Temporary in-memory resultsA cache with a clear lifetime and concurrency policy

SwiftData integrates with SwiftUI through @Model, .modelContainer, @Query, and ModelContext. It is not automatically the right choice for every app; compare migration, query, history, CloudKit, and concurrency needs with Core Data or a server-owned model.

Recent SwiftData releases add features such as unique constraints, model inheritance, query sectioning, and history observers. Check the framework's deployment availability and migration guidance before using a new schema feature.

Testing boundaries

Choose a test double by the behavior you need to observe: a stub returns controlled data, a fake implements a lightweight working model, a spy records calls, and a mock verifies expectations. These labels are less important than clear tests and stable seams; avoid brittle interaction assertions when a state/result assertion proves the behavior.

SWIFT
import Testing
​
struct StubProductRepository: ProductRepository {
    var result: [Product]
    func products() async throws -> [Product] { result }
}
​
@Test
@MainActor
func loadShowsRepositoryProducts() async {
    let expected = [Product(id: UUID(), name: "Notebook", price: 4.50)]
    let model = CatalogModel(repository: StubProductRepository(result: expected))
​
    await model.load()
​
    #expect(model.state == .loaded(expected))
}

Use Swift Testing (@Test, #expect) for new logic tests where it fits. XCTest remains widely used and is still appropriate for UI tests, existing suites, and framework integrations. Test the boundary that owns behavior: mapping in repositories, state transitions in screen models, and critical user journeys in UI tests.

Inject clocks, ID generation, and network behavior when determinism matters. For cancellation and actor-isolated state, use controllable async operations or a test clock and await a known state transition instead of sleeping for an arbitrary duration.

Current Swift 6.4 and Xcode 27 notes

  • Xcode 27 includes the Swift 6.4 toolchain and iOS 27 SDK. Use Xcode's release notes for supported OS versions and any beta distinctions.
  • Swift 6 language mode enables data-race diagnostics as errors. Adopt it deliberately and make shared-state boundaries explicit.
  • Swift 6.4 adds language/library improvements, but new low-level ownership and collection APIs are for measured performance needs—not a requirement for normal UI features.
  • Keep deployment target and SDK version separate: compiling with the newest SDK does not make every new runtime API available on older iOS versions.
  • Add privacy manifests, entitlements, usage descriptions, and App Store declarations based on the app's actual data and capabilities.

References