Skip to content

Instantly share code, notes, and snippets.

@ZekeSnider
Created January 14, 2025 07:05
Show Gist options
  • Save ZekeSnider/00f0c6bbdada67910886896b7c58e6c5 to your computer and use it in GitHub Desktop.
Save ZekeSnider/00f0c6bbdada67910886896b7c58e6c5 to your computer and use it in GitHub Desktop.
SwiftData Model Monitor
import Foundation
import SwiftData
import WidgetKit
import os
@ModelActor final actor DataMonitor {
private var logger: Logger {
Loggable.getLogger(Self.self)
}
func subscribeToModelChanges() async {
for await _ in NotificationCenter.default.notifications(
named: .NSPersistentStoreRemoteChange
).map({ _ in () }) {
logger.info("Reloading widget timelines because of NSPersistentStoreRemoteChange!")
await processNewTransactions()
}
}
func processNewTransactions() async {
let tokenData = UserDefaults.standard.data(forKey: "historyToken")
var historyToken: DefaultHistoryToken? = nil
if let tokenData {
historyToken = try? JSONDecoder().decode(DefaultHistoryToken.self, from: tokenData)
}
let transactions = findTransactions(after: historyToken)
let (updatedModelIds, newHistoryToken) = findUpdatedModelIds(in: transactions)
if let newHistoryToken {
let newTokenData = try? JSONEncoder().encode(newHistoryToken)
UserDefaults.standard.set(newTokenData, forKey: "historyToken")
}
if let historyToken {
try? deleteTransactions(before: historyToken)
}
await maybeUpdateWidgets(relevantTo: updatedModelIds)
}
private func findUpdatedModelIds(in transactions: [DefaultHistoryTransaction]) -> (Set<UUID>, DefaultHistoryToken?) {
let taskContext = ModelContext(modelContainer)
var updatedModelIds: Set<UUID> = []
for transaction in transactions {
for change in transaction.changes {
let transactionModifiedID = change.changedPersistentIdentifier
let fetchDescriptor = FetchDescriptor<SongArtworkViewModel>(predicate: #Predicate { model in
model.persistentModelID == transactionModifiedID
})
let fetchResults = try? taskContext.fetch(fetchDescriptor)
guard let matchedModel = fetchResults?.first else {
continue
}
switch change {
case .insert(_ as DefaultHistoryInsert<SongArtworkViewModel>):
break
case .update(_ as DefaultHistoryUpdate<SongArtworkViewModel>):
updatedModelIds.update(with: matchedModel.id)
case .delete(_ as DefaultHistoryDelete<SongArtworkViewModel>):
updatedModelIds.update(with: matchedModel.id)
default: break
}
}
}
return (updatedModelIds, transactions.last?.token)
}
private func maybeUpdateWidgets(relevantTo modelIds: Set<UUID>) async {
let configurations = try? await WidgetCenter.shared.currentConfigurations()
guard let configurations else { return }
let relevantConfigurationKinds = configurations.filter { configuration in
let config = configuration.widgetConfigurationIntent(of: SongConfigurationAppIntent.self)
guard let config else {
return false
}
if config.mode == .random {
return true
}
guard let entityId = config.specificSong?.id else {
return false
}
return modelIds.contains(entityId)
}.map { $0.kind }
Array(Set(relevantConfigurationKinds)).forEach { kind in
WidgetCenter.shared.reloadTimelines(ofKind: kind)
}
}
private func findTransactions(after token: DefaultHistoryToken?) -> [DefaultHistoryTransaction] {
var historyDescriptor = HistoryDescriptor<DefaultHistoryTransaction>()
if let token {
historyDescriptor.predicate = #Predicate { transaction in
(transaction.token > token)
}
}
var transactions: [DefaultHistoryTransaction] = []
do {
transactions = try modelContext.fetchHistory(historyDescriptor)
} catch {
logger.error("Error while fetching history transactions \(error, privacy: .public)")
}
return transactions
}
private func deleteTransactions(before token: DefaultHistoryToken) throws {
var descriptor = HistoryDescriptor<DefaultHistoryTransaction>()
descriptor.predicate = #Predicate {
$0.token < token
}
let context = ModelContext(modelContainer)
try context.deleteHistory(descriptor)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment