swiftcore-datauicollectionviewnsfetchedresultscontrollerdiffabledatasource

NSFetchedResultsController didChangeContentWith uses temporaryID of a successfully saved Object


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!


Solution

  • 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.