Streaming is available in most browsers,
and in the Developer app.
-
Track model changes with SwiftData history
Reveal the history of your model's changes with SwiftData! Use the history API to understand when data store changes occurred, and learn how to use this information to build features like remote server sync and out-of-process change handing in your app. We'll also cover how you can build support for the history API into a custom data store.
Chapters
- 0:00 - Introduction
- 0:45 - Fundamentals
- 5:18 - Transactions and changes
- 12:37 - Custom stores
Resources
Related Videos
WWDC24
-
DownloadArray
-
-
4:57 - Preserve values in history on deletion
// Add .preserveValueOnDeletion to capture unique columns import SwiftData @Model class Trip { #Unique<Trip>([\.name, \.startDate, \.endDate]) @Attribute(.preserveValueOnDeletion) var name: String var destination: String @Attribute(.preserveValueOnDeletion) var startDate: Date @Attribute(.preserveValueOnDeletion) var endDate: Date var bucketList: [BucketListItem] = [BucketListItem]() var livingAccommodation: LivingAccommodation? }
-
6:26 - Fetch transactions from history
private func findTransactions(after token: DefaultHistoryToken?, author: String) -> [DefaultHistoryTransaction] { var historyDescriptor = HistoryDescriptor<DefaultHistoryTransaction>() if let token { historyDescriptor.predicate = #Predicate { transaction in (transaction.token > token) && (transaction.author == author) } } var transactions: [DefaultHistoryTransaction] = [] let taskContext = ModelContext(modelContainer) do { transactions = try taskContext.fetchHistory(historyDescriptor) } catch let error { print(error) } return transactions }
-
7:34 - Process history changes
private func findTrips(in transactions: [DefaultHistoryTransaction]) -> (Set<Trip>, DefaultHistoryToken?) { let taskContext = ModelContext(modelContainer) var resultTrips: Set<Trip> = [] for transaction in transactions { for change in transaction.changes { let modelID = change.changedPersistentIdentifier let fetchDescriptor = FetchDescriptor<Trip>(predicate: #Predicate { trip in trip.livingAccommodation?.persistentModelID == modelID }) let fetchResults = try? taskContext.fetch(fetchDescriptor) guard let matchedTrip = fetchResults?.first else { continue } switch change { case .insert(_ as DefaultHistoryInsert<LivingAccommodation>): resultTrips.insert(matchedTrip) case .update(_ as DefaultHistoryUpdate<LivingAccommodation>): resultTrips.update(with: matchedTrip) case .delete(_ as DefaultHistoryDelete<LivingAccommodation>): resultTrips.remove(matchedTrip) default: break } } } return (resultTrips, transactions.last?.token) }
-
10:19 - Save and use a history token
private func findUnreadTrips() -> Set<Trip> { let tokenData = UserDefaults.standard.data(forKey: UserDefaultsKey.historyToken) var historyToken: DefaultHistoryToken? = nil if let tokenData { historyToken = try? JSONDecoder().decode(DefaultHistoryToken.self, from: tokenData) } let transactions = findTransactions(after: historyToken, author: TransactionAuthor.widget) let (unreadTrips, newToken) = findTrips(in: transactions) if let newToken { let newTokenData = try? JSONEncoder().encode(newToken) UserDefaults.standard.set(newTokenData, forKey: UserDefaultsKey.historyToken) } return unreadTrips }
-
11:30 - Update the user interface
struct ContentView: View { @Environment(\.scenePhase) private var scenePhase @State private var showAddTrip = false @State private var selection: Trip? @State private var searchText: String = "" @State private var tripCount = 0 @State private var unreadTripIdentifiers: [PersistentIdentifier] = [] var body: some View { NavigationSplitView { TripListView(selection: $selection, tripCount: $tripCount, unreadTripIdentifiers: $unreadTripIdentifiers, searchText: searchText) .toolbar { ToolbarItem(placement: .topBarLeading) { EditButton() .disabled(tripCount == 0) } ToolbarItemGroup(placement: .topBarTrailing) { Spacer() Button { showAddTrip = true } label: { Label("Add trip", systemImage: "plus") } } } } detail: { if let selection = selection { NavigationStack { TripDetailView(trip: selection) } } } .task { unreadTripIdentifiers = await DataModel.shared.unreadTripIdentifiersInUserDefaults } .searchable(text: $searchText, placement: .sidebar) .sheet(isPresented: $showAddTrip) { NavigationStack { AddTripView() } .presentationDetents([.medium, .large]) } .onChange(of: selection) { _, newValue in if let newSelection = newValue { if let index = unreadTripIdentifiers.firstIndex(where: { $0 == newSelection.persistentModelID }) { unreadTripIdentifiers.remove(at: index) } } } .onChange(of: scenePhase) { _, newValue in Task { if newValue == .active { unreadTripIdentifiers += await DataModel.shared.findUnreadTripIdentifiers() } else { // Persist the unread trip names for the next launch session. await DataModel.shared.setUnreadTripIdentifiersInUserDefaults(unreadTripIdentifiers) } } } #if os(macOS) .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in Task { unreadTripIdentifiers += await DataModel.shared.findUnreadTripIdentifiers() } } .onReceive(NotificationCenter.default.publisher(for: NSApplication.willTerminateNotification)) { _ in Task { await DataModel.shared.setUnreadTripIdentifiersInUserDefaults(unreadTripIdentifiers) } } #endif } }
-
-
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.