Add SwiftData
Hub › iOS › Advanced › Add SwiftData
Goal
Define @Model classes, replace the URLSession-only ViewModel with one that persists to SwiftData, and verify data survives app restarts.
Prerequisites
Define the model
Create a new Swift file CachedItem.swift:
import Foundation
import SwiftData
@Model
final class CachedItem {
var id: Int
var title: String
var detail: String
var isFavorite: Bool
var cachedAt: Date
init(id: Int, title: String, detail: String) {
self.id = id
self.title = title
self.detail = detail
self.isFavorite = false
self.cachedAt = .now
}
}The @Model macro makes the class observable by SwiftData. Every stored property becomes a column in the underlying SQLite database.
SwiftData-backed ViewModel
Replace the intermediate ObservableObject ViewModel with one that loads from SwiftData first, then refreshes from the network:
import SwiftUI
import SwiftData
@MainActor
@Observable
final class ItemViewModel {
private var modelContext: ModelContext
var items: [CachedItem] = []
var isLoading = false
var error: Error?
init(modelContext: ModelContext) {
self.modelContext = modelContext
}
func load() async {
isLoading = true
error = nil
defer { isLoading = false }
// 1. Load cached items immediately
let descriptor = FetchDescriptor<CachedItem>(
sortBy: [SortDescriptor(\.id)]
)
items = (try? modelContext.fetch(descriptor)) ?? []
// 2. Refresh from network
do {
let remote = try await fetchRemoteItems()
// Merge: keep favorites, update titles/details
for remoteItem in remote {
if let existing = items.first(where: { $0.id == remoteItem.id }) {
existing.title = remoteItem.title
existing.detail = remoteItem.detail
} else {
let new = CachedItem(id: remoteItem.id,
title: remoteItem.title,
detail: remoteItem.detail)
modelContext.insert(new)
items.append(new)
}
}
try modelContext.save()
} catch {
// Network failed — cached items still available
self.error = error
}
}
func toggleFavorite(_ item: CachedItem) {
item.isFavorite.toggle()
try? modelContext.save()
}
private func fetchRemoteItems() async throws -> [(id: Int, title: String, detail: String)] {
// Same URLSession call as the intermediate tier
let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([RemoteItem].self, from: data)
.map { (id: $0.id, title: $0.title, detail: $0.body) }
}
}
private struct RemoteItem: Decodable {
let id: Int
let title: String
let body: String
}Update ContentView
import SwiftUI
import SwiftData
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@State private var viewModel: ItemViewModel?
var body: some View {
Group {
if let vm = viewModel {
List(vm.items, id: \.id) { item in
HStack {
VStack(alignment: .leading) {
Text(item.title).font(.headline)
Text(item.detail).font(.caption)
}
Spacer()
Button(item.isFavorite ? "★" : "☆") {
vm.toggleFavorite(item)
}
}
}
.overlay {
if vm.isLoading { ProgressView() }
}
.task { await vm.load() }
} else {
ProgressView()
}
}
.onAppear {
viewModel = ItemViewModel(modelContext: modelContext)
}
}
}Checkpoint
Run the app in the simulator. The first launch fetches from the network and caches items. Tap a star to favorite it. Stop the app (Cmd+Q) and relaunch — the favorite persists and cached data loads immediately without a network spinner.
Next: Sign in with Apple — authenticate users with their Apple ID.