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