iosdev

NSFRC’s sections considered harmful

Explanation of the serious bug in sections order when sectionNameKeyPath is not the first item in the FRC’ sortDescriptors.

Imagine a simple sports app model: League entity, with some id, name and displayOrder properties. It has a 1-many relationship to Event entity that has (among other attributes) id, name and of course — reverse to-one relationship back to League.

You write a simple FetchRequest for FetchedResultsController when you are loading events grouped per league, with order defined by League.displayOrder but the section name (title) is League.name.

To get the grouping with proper section titles, your sectionNameKeyPath on the Event entity should be league.name .

But to get the correct order per displayOrder you declare sortDescriptors of the FRC’s fetchRequest to be

NSSortDescriptor(key: "league.displayOrder", ascending: true)
NSSortDescriptor(key: "league.name", ascending: true)
...

This should be fine, right? Order of events will be correct and there should be no issues for Core Data to group per league’s name.

The resulting fetchedObjects array looks correct:

But the order of sections is not correct:

It looks to be sorted per section title. 😣

How bad this is..?

This is a serious issue since you naturally want to process the result per sections / events and feed some collection view, expecting proper order. After all — why would you group per league unless to display them per league, with proper priority order.

Even worse, if you use CompositionalLayout and thus use newer delegate methods like controller(_:didChangeContentWith:) you will naturally want to use given Core Data snapshot to rebuild diffable data source of your collection view.

func controller(_ frc: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {

	//	String==sectionIdentifier => sectionNameKeyPath or UUID (if nil)
	//	NSManagedObjectID of the changed object
	let cds = snapshot as NSDiffableDataSourceSnapshot<String, NSManagedObjectID>

	snapshotEvents(usingCoreDataSnapshot: cds, animated: false)
}

But the sectionIdentifiers of the Core Data snapshot are wrongly ordered:

and even worse — wrong set of itemIdentifiers is associated for each of them:

To make it clearer what’s wrong, here’s side-by-side:

Clearly first 6 events do not belong to “Championship” league and thus are associated with the wrong section.

This can hard-crash your app, depending on how you use this snapshot.

For example, with code like this:

let sectionIdentifiers = cds.sectionIdentifiers
for si in sectionIdentifiers {
  let secitem = ...
	snapshot.appendSections([secitem])

	let items = cds.itemIdentifiers(inSection: si).map {...}
	snapshot.appendItems(items, toSection: secitem)
}

you will get a crash with:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', 
reason: 'Diffable data source detected an attempt to insert or append 1 section identifier that already exists in the snapshot. 
Identifiers in a snapshot must be unique. 

Section identifier that already exists:
EventsListDataSource.SectionItem.league(leagueId: 122, name: "Championship")'

Anything that can lead to hard-crash is severe bug. And I am now revisiting all the places I use sectionNameKeyPath and checking to see if intermittent crashes I get are caused by this issue.

How to get around this?

Both frc.fetchedObjects and cds.itemIdentifiers are in the correct order. Thus both can be used to recreate section/item hierarchy but I’m not aware of any easy way to rebuild sectionIdentifiers.

Re-implementing the FRC.sections iterator is fairly straightforward though, on case-by-case basis. Here:

let leagues = events.map({ $0.league }).unique()
for league in leagues {
	let leagueEvents = validEvents.filter({ 
	  $0.league.leagueId == league.leagueId
  })
}

So that’s what I use and completely ignore Core Data snapshot, for now.