Appearance
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.
Navigation
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
@Statefor view-local state (toggle booleans, text field values)@Observableclasses for shared state (user preferences, data models)@Environmentfor dependency injection (model containers, services)@Queryfor 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@ObservableObjectand@Publishedinstead