same, iOS 14 beta 6 iPhone 11 Pro
Post
Replies
Boosts
Views
Activity
I implement this using (CloudCore for sync, and) an Observer object that reacts to changes in CoreData. Whether the change comes locally or synced, the notifications get queued/presented locally on all the users' devices. For example
final class RemindersObserver: NSObject, NSFetchedResultsControllerDelegate {
private let persistentContainer: NSPersistentContainer
private let frc: NSFetchedResultsController<Reminder>
init(persistentContainer: NSPersistentContainer) {
self.persistentContainer = persistentContainer
let moc = persistentContainer.newBackgroundContext()
moc.automaticallyMergesChangesFromParent = true
let fetchRequest: NSFetchRequest<Reminder> = Reminder.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "moveBy", ascending: false)]
frc = NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: moc,
sectionNameKeyPath: nil,
cacheName: "RemindersObserver")
super.init()
frc.delegate = self
try? frc.performFetch()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
let noteCenter = UNUserNotificationCenter.current()
noteCenter.removeAllPendingNotificationRequests()
guard let reminders = frc.fetchedObjects else { return }
for reminder in reminders {
guard let car = reminder.car, let uuidString = car.uuid else { continue }
for countdown in Countdown.allCases {
let content = UNMutableNotificationContent()
content.categoryIdentifier = "reminderNotification"
content.title = "Move the " + car.name!
if countdown == .expired {
content.subtitle = "Parking has expired!"
} else {
content.subtitle = "Parking expires in " + countdown.timeInterval().toString()
}
let notificationDate = reminder.moveBy! - countdown.timeInterval()
let components: Set<Calendar.Component> = [.second, .minute, .hour, .day]
let dateComponents = Calendar.current.dateComponents(components, from: notificationDate)
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
let request = UNNotificationRequest(identifier: uuidString + countdown.rawValue, content: content, trigger: trigger)
noteCenter.add(request) { error in
//
}
}
}
WidgetCenter.shared.reloadTimelines(ofKind: Identifiers.carWidget)
}
}
fwiw, my experiences with DiffableDataSources and CoreData is that they are great at detecting additions and deletions (because the managed object IDs in the fetched results are different) but don't work for updates (because the ids stay the same, only the properties change). I thought I saw a very brief comment in a video about updating DiffableDataSources due to property changes, but damn if I can remember which video that was.
also fwiw, in UIKit, I solve for this by observing the changed records of the FetchedRsultsController, and specifically reloading them in the diffable data source.
…
func updateSnapshot() {
var diffableDataSourceSnapshot = NSDiffableDataSourceSnapshot<String, Car>()
frc.sections?.forEach { section in
diffableDataSourceSnapshot.appendSections([section.name])
diffableDataSourceSnapshot.appendItems(section.objects as! [Car], toSection: section.name)
}
diffableDataSourceSnapshot.reloadItems(changedCars)
diffableDataSource?.apply(diffableDataSourceSnapshot, animatingDifferences: true)
}
}
extension CarsListViewController: NSFetchedResultsControllerDelegate {
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
if type == .update, let car = anObject as? Car {
changedCars.append(car)
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
self.updateSnapshot()
changedCars.removeAll()
}
}
Note this means I am not using the fancy new FRC delegate callbacks with snapshots, because they only have adds/deletes, no updates.
In CoreData, NSFetchRequest is used to perform a single fetch, whereas NSFetchedResultsController provides a continuous view of data matching your predicates. Furthermore, for SwiftUI, you should look at the property wrapper @FetchRequest, which in fact implements a NSFetchResultsController, and can be used instead of your ItemsModel class. Hope this helps.
change
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "MyNumberDataRegistration")
to
let fetchRequest = NSFetchRequest<MyNumberDataRegistration>(entityName: "MyNumberDataRegistration")
I've had similar issues using the iOS-side discoverAllUsers API for users with large address books. For me, its one call (operation) but underneath the hood, CloudKit is taking chunks of contacts and fetching identities. Would be great is Apple special-cased the discoverAllUsers API to not hit these throttling limits per client-call.
CoreData is definitely not "falling off" :-) Use SwiftUI+CoreData and sync with CloudKit, using either NSPersistentCloudKitContainer, an open-source sync engine like CloudCore, or a third-party sync engine like Ensembles.io
did you add indexes to the fields you're querying on? This has to be done in the console.
sigh…
for reasons that boggle my mind, the snapshot returned by the FRC
let snapshot = snapshotReference as NSDiffableDataSourceSnapshot<Int, NSManagedObjectID>
DOES NOT properly handle object updates, only inserts, deletions, and reorder. Its infuriating, frankly.
So I have resorted to implementing my FRC delegate like this…
var changedCars: [Car] = []
func updateSnapshot() {
var diffableDataSourceSnapshot = NSDiffableDataSourceSnapshot<String, Car>()
frc.sections?.forEach { section in
diffableDataSourceSnapshot.appendSections([section.name])
diffableDataSourceSnapshot.appendItems(section.objects as! [Car], toSection: section.name)
}
diffableDataSourceSnapshot.reloadItems(changedCars)
diffableDataSource?.apply(diffableDataSourceSnapshot, animatingDifferences: true)
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
if type == .update, let car = anObject as? Car {
changedCars.append(car)
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
self.updateSnapshot()
changedCars.removeAll()
}
It is safe to use strong references to managedObjects in table/collection view cells. There is a limited pool of cells that get reused as the table/collection scrolls, and CoreData does far more to manage memory behind the managedObject (i.e. faulting).
There are several issues here.
CoreData is very specifically designed to manage memory efficiently, especially with large data sets. But you are correct that some queries can take a long time, and if performed on the main thread, can cause a hitch. When a query is performed, however, not all the data is loaded. https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreData/FaultingandUniquing.html
When configured with NSManageObjects, DiffableDataSource uses only the object IDs to detect changes, and the snapshots currently returned by FRC (as of iOS 15 beta 1) only include .inserts, .deletes, and .moves, but not .updates. If you want to handle updates, you have to specifically call snapshot.reload or snapshot.refresh for the changes objects.
You can configure an NSFetchedResultsController to use a background NSManagedObjectContext. Any access to properties of the found objects must be done within a moc.perform { } block, or use the objectIDs to retrieve the objects on the main thread viewContext.
Another idea is to re-create the behavior of FRCs by listening to CoreData notifications directly. See https://developer.apple.com/documentation/foundation/nsnotification/name/1506884-nsmanagedobjectcontextobjectsdid
Create a test account to do your development and testing.
It appears that subscribing to the public database is deprecated. CoreData's support for CloudKit public database does polling. See https://developer.apple.com/videos/play/wwdc2020/10650/ with a reference at the 7:30 mark, and a more-detailed discussion starting at 10:20. I would recommend the entire video, even if you're rolling your own CloudKit implementation, as it has lots of insights into how achieve sync without the tombstone magic in CKFetchRecordsZoneChangesOperation.
Do you have a Searchable index on favoriteColors?