Created
January 14, 2025 07:05
-
-
Save ZekeSnider/00f0c6bbdada67910886896b7c58e6c5 to your computer and use it in GitHub Desktop.
SwiftData Model Monitor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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