Context: I have an app with two Core Data entities (Asset and Group), with a one-to-many relationship (one group contains many assets). The collectionview fetches Group items and uses them as section, the assets being the individual cells. When a user saves a new entry the didChangeContentWith method is called, however the new asset item uses the "temporary id."
Problem/Crash: The UI displays fine, however when the user taps on the cell, the app crashes because it can't find the object with that NSManagedObjectID.
Code:
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
var newSnapshot = NSDiffableDataSourceSnapshot<String, NSManagedObjectID>()
if let sections = controller.sections {
// Iterate through the fetched sections (groups)
for sectionInfo in sections {
if let group = sectionInfo.objects?.first as? Group {
// Append the section (group name) to the snapshot
newSnapshot.appendSections([group.name!])
// Get the objects associated with this Group
if let assets = group.assets {
let assetIDs = assets.map{ $0.objectID }
for id in assetIDs {
print("<<<< ID temp: \(id.isTemporaryID)")
}
// Append the Asset objects as items within the section (group)
newSnapshot.appendItems(assetIDs, toSection: group.name)
}
}
}
}
// Apply the snapshot to the UICollectionViewDiffableDataSource
dataSource.apply(newSnapshot, animatingDifferences: true)
}
Saving function:
context.performAndWait { [unowned self] in
let _ = generateOrUpdateAsset(with: id, name: self.name, priority: self.priority, comments: self.comments, context: context)
// Here the temp id is used
do {
try context.save()
// Here the temp id is not used
} catch {
print("Failed to saving test data: \(error)")
}
}
Fetch function:
func performCoreDataFetch(){
if let context = self.context {
let fetchRequest = Group.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "name", ascending: false)]
fetchRequest.relationshipKeyPathsForPrefetching = ["asset"]
let predicate = NSPredicate(format: "ANY assets != nil")
fetchRequest.predicate = predicate
fetchResultsController = NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: context,
sectionNameKeyPath: "name",
cacheName: nil)
fetchResultsController.delegate = self
try! fetchResultsController.performFetch()
}
}
UPDATE, the crashing code:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let objectID = self.dataSource.itemIdentifier(for: indexPath) else {
collectionView.deselectItem(at: indexPath, animated: true)
return
}
if let context = self.context {
do {
guard let asset = try context.existingObject(with: objectID) as? Asset else { return }
if let assetDetail = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: MasterListViewController.assetDetailIdentifier) as? DetailViewController {
assetDetail.context = self.context
assetDetail.asset = asset
let navController = UINavigationController(rootViewController: assetDetail)
present(navController, animated: true)
}
} catch {
print("error with existing obj: \(error)")
}
}
}
Configuring Datasource:
func configureDataSource() {
let assetCell = UICollectionView.CellRegistration<CustomAssetCell, NSManagedObjectID> { (cell, indexPath, objectID) in
if let asset = try self.context!.existingObject(with: objectID) as? Asset {
let image = asset.image
cell.imageView.image = image
}
}
let headerRegistration = UICollectionView.SupplementaryRegistration<MasterSupplementaryView>(elementKind: MasterListViewController.sectionHeaderElementKind) { (supplementaryView, string, indexPath) in
guard let section = self.fetchResultsController.sections?[indexPath.section] else {return}
if let group = section.objects?.first as? Group {
let numberOfAsset = group.assets?.count ?? 0
supplementaryView.color = group.groupColor
supplementaryView.iconView.backgroundColor = .label
supplementaryView.emojiLabel.text = group.emoji
supplementaryView.label.text = "\(String(describing: group.name!))"
supplementaryView.descriptionLabel.text = "\(numberOfAsset) items total"
}
// Params for delegate
supplementaryView.indexPath = indexPath
supplementaryView.delegate = self
}
dataSource = UICollectionViewDiffableDataSource<String, NSManagedObjectID>(collectionView: collectionView) {
(collectionView, indexPath, item) -> UICollectionViewCell? in
return collectionView.dequeueConfiguredReusableCell(using: assetCell, for: indexPath, item: item)
}
dataSource.supplementaryViewProvider = { (collectionView, kind, index) in
return self.collectionView.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: index)
}
}
Caught error: "error with existing obj: Error Domain=NSCocoaErrorDomain Code=133000 "Attempt to access an object not found in store." UserInfo={objectID=0x282684d80 x-coredata:///Asset/tFDC4025E-6F55-464C-91AD-2406480B95A12} "
Many thanks in advance for your help!
Newly inserted objects don't have a permanent ID. Core Data will create a permanent ID for you when you save the context, or you can obtain one by using obtainPermanentIDs(for: )
. However, that probably isn't the best way to solve your problem.
The best way to solve your problem is to have the snapshot be <String, Asset>
(or whatever the type is, I couldn't see that in your question) rather than <String, NSManagedObjectID>
, then you don't need to re-fetch things from the database or do any conversion steps.