0

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!

robinyapockets
  • 363
  • 5
  • 21
  • Does this single line `apply(snapshot as NSDiffableDataSourceSnapshot, animatingDifferences: true)` in `didChangeContentWith` not work? – vadian Aug 14 '23 at 14:08
  • Not sure what you mean with "not work." – robinyapockets Aug 14 '23 at 14:14
  • I don't understand why you create a new snapshot manually? You can replace the entire body of `didChangeContentWith` with the line in my first comment. – vadian Aug 14 '23 at 14:21
  • Thanks for the clarification! Yes, if I only use the one line, the sections (groups) don't get populated with the associated cells (assets). – robinyapockets Aug 14 '23 at 14:42

1 Answers1

0

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.

jrturton
  • 118,105
  • 32
  • 252
  • 268
  • thanks for the input. I thought the preferred way of handling nsfetchresultscontroller and NSDiffableDataSourceSnapshot is with type which, as you pointed out, I am using. Since the permanent id is successfully set after the try save event, would another approach be to use another perform fetch / reload? – robinyapockets Aug 14 '23 at 14:13
  • Preferred by whom? I've never found it to be anything other than a pain, so always use the object :) . You don't actually show the code that is crashing, so it's hard to comment, but even a temporary ID should work for fetching things from the context that is being worked on. – jrturton Aug 14 '23 at 14:20
  • You're right for asking about the error. Added code and error log to the SO post. I'll see how I can use the temp ID – robinyapockets Aug 14 '23 at 14:48
  • I tried the following method using the tempID: context.object(with: objectID) instead of context.existingObject - which returns an object, but with incomplete attributes. This leads me to think that the object isn't fully saved, which is weird because I can literally see it in the collectionview. See code for configure data source above. – robinyapockets Aug 14 '23 at 15:13
  • This looks like the FRC is fired when the object is created, but not again when the context is saved, which means your temp ID is now not so useful. I respectfully repeat my suggestion to not use the object ID and make your snapshot using the actual objects :) – jrturton Aug 14 '23 at 15:26
  • your suggestion worked! I'm not sure why simply casting the NSManagedObjectID didn't work. But big thanks! – robinyapockets Aug 15 '23 at 12:07