Skip to content

Instantly share code, notes, and snippets.

@stoeffn
Last active December 5, 2022 06:43
Show Gist options
  • Save stoeffn/dafed2b5c671281ab172353749140a46 to your computer and use it in GitHub Desktop.
Save stoeffn/dafed2b5c671281ab172353749140a46 to your computer and use it in GitHub Desktop.
Persistent History Tracking in Core Data—https://stoeffn.de/posts/persistent-history-tracking-in-core-data/
import CoreData
/// Manages Core Data Persistent History.
///
/// When using Core Data in multiple targets, e.g. an app as well as a file provider, it is crucial to merge changes from one
/// target into another because otherwise you would end up with inconsistent state. To that end, this Apple introduced
/// persistent history, which is a linear stream of changes that can be merged into the current context. This service takes
/// advantage of this feature by providing a simple interface for merging and deleting history. The latter is needed to free up
/// space after the history has been consumed by all targets. It uses history transactions' timestamps in order to determine
/// what to delete.
public final class PersistentHistoryService {
/// Current target, whose last transaction timestamp is used and modified.
private var currentTarget: Targets
public init(currentTarget: Targets) {
self.currentTarget = currentTarget
}
/// Returns the latest persistent history transaction timestamp that is common to all targets given.
private func lastCommonTransactionTimestamp(in targets: [Targets]) -> Date? {
let timestamp = targets
.map { $0.lastHistoryTransactionTimestamp ?? .distantPast }
.min() ?? .distantPast
return timestamp > .distantPast ? timestamp : nil
}
/// Delete persistent history that was successfully merged in _all_ of the targets provided. Call this method each time
/// after merging persistent history.
public func deleteHistory(mergedInto targets: [Targets], in context: NSManagedObjectContext) throws {
guard #available(iOSApplicationExtension 11.0, *) else { return }
guard let timestamp = lastCommonTransactionTimestamp(in: targets) else { return }
let deleteHistoryRequest = NSPersistentHistoryChangeRequest.deleteHistory(before: timestamp)
try context.execute(deleteHistoryRequest)
}
/// Merges persistent history into the context given and marks the current target as up-to-date. To delete history that was
/// merged into every target, invoke `deleteHistory(mergedInto:in:)` after calling this method.
///
/// - Parameter context: Managed object context, which should ideally be the view context or a context to be merged into the
/// view context.
/// - Postcondition: The current target's last history transaction timestamp is set to the last transaction timestamp.
public func mergeHistory(into context: NSManagedObjectContext) throws {
guard #available(iOSApplicationExtension 11.0, *) else { return }
let historyFetchRequest = NSPersistentHistoryChangeRequest
.fetchHistory(after: currentTarget.lastHistoryTransactionTimestamp ?? .distantPast)
guard
let historyResult = try context.execute(historyFetchRequest) as? NSPersistentHistoryResult,
let history = historyResult.result as? [NSPersistentHistoryTransaction]
else { fatalError("Cannot convert persistent history fetch result to transactions.") }
for transaction in history {
context.mergeChanges(fromContextDidSave: transaction.objectIDNotification())
}
if let lastTimestamp = history.last?.timestamp {
currentTarget.lastHistoryTransactionTimestamp = lastTimestamp
}
}
}
import CoreData
extension Targets {
/// Timestamp of the last Core Data persistent history merge into the target's view context.
var lastHistoryTransactionTimestamp: Date? {
get {
// TODO: Use your UserDefaults and make sure they are shared across your targets.
let key = UserDefaults.lastHistoryTransactionTimestampKey(for: self)
userDefaults.object(forKey: key) as? Date
}
set {
// TODO: Use your UserDefaults and make sure they are shared across your targets.
let key = UserDefaults.lastHistoryTransactionTimestampKey(for: self)
userDefaults.set(newValue, forKey: key)
}
}
}
/// Available application targets.
public enum Targets: String {
case app, fileProvider, fileProviderUI, tests
}
extension UserDefaults {
static func lastHistoryTransactionTimestampKey(for target: Targets) -> String {
return "lastHistoryTransactionTimestamp-\(target)"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment