Skip to content

SwiftUI Architecture

Data layer, navigation, and state management patterns for personal apps across Apple platforms. Keep it simple — over-engineering a personal app is the most common mistake.

When to use

Use when deciding how to structure a new SwiftUI app, choosing persistence, or setting up navigation and state management.

The pattern

Data layer

SwiftData for structured local storage. It's Apple's modern persistence framework (successor to Core Data), works natively with SwiftUI, and uses Swift macros for model definitions:

swift
@Model
class Transaction {
    var amount: Decimal
    var category: String
    var date: Date
    var note: String
    
    init(amount: Decimal, category: String, date: Date, note: String = "") {
        self.amount = amount
        self.category = category
        self.date = date
        self.note = note
    }
}

SwiftData can sync via iCloud, but it requires enabling the iCloud capability in Xcode (Signing & Capabilities → iCloud → CloudKit) and using a CloudKit container. Once configured, data appears on iPhone, iPad, Mac, and Watch without additional sync code.

UserDefaults for simple key-value preferences (theme, last-opened tab, feature flags). Don't use it for structured data.

Use NavigationSplitView for apps with a list-detail structure — it renders as a sidebar on iPad and Mac, a navigation stack on iPhone:

swift
NavigationSplitView {
    List(items, selection: $selected) { item in
        Text(item.title)
    }
} detail: {
    if let selected {
        DetailView(item: selected)
    }
}

Use TabView for top-level tab structure. On iPadOS 18+, tabs automatically become a sidebar. Use NavigationStack when you only need push-based navigation.

Keep navigation state in a NavigationPath or a simple enum-based router. For personal apps, a flat approach works — don't introduce a coordinator pattern unless the app genuinely needs it.

State management

  • @State for view-local state (toggle booleans, text field values)
  • @Observable classes for shared state (user preferences, data models)
  • @Environment for dependency injection (model containers, services)
  • @Query for SwiftData fetch requests in views

For personal apps, a flat structure with a few @Observable models is fine. You don't need MVVM, coordinators, or a Redux-style store.

Networking

For apps that need API access, use Swift's built-in URLSession with async/await:

swift
func fetchData() async throws -> [Item] {
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode([Item].self, from: data)
}

No need for Alamofire or other networking libraries for personal apps.

Trade-offs

  • SwiftData is newer and has fewer community resources than Core Data — but it's simpler and Apple is actively improving it
  • SwiftData iCloud sync is automatic but opaque — debugging sync issues is harder than manual CloudKit integration
  • Flat architecture is faster to build but harder to refactor later — for a personal app that's an acceptable trade-off
  • @Observable (Observation framework) requires iOS 17+ / macOS 14+ — if targeting older versions, use @ObservableObject and @Published instead