NSOutlineView.insertItems(at:inParent:withAnimation) not drawing new items

First of all, I'm building my application with Xcode 12.4 (12D4e) and running it on macOS 11.2.3 (20D91).

Most of this background shouldn't be relevant to my issue, but it should be working, so clearly I'm missing something. My application downloads a set of objects from a server (think the users, computers, and groups in an Active Directory database) and displays them in a view-based NSOutlineView in the sidebar. I'm using Core Data. As objects are downloaded, I put them in the managed object context, then on the UI side I have a set of NSFetchedResultsController objects to pull the objects for display in the sidebar. I have three manual sidebar supercategories with a separate FRC for each. I am trying to make a delegate for my FRCs which will insert the items into the sidebar as they are downloaded.

My NSFetchedResultsControllerDelegate conformance looks like this:

Code Block Swift
extension ObjectSidebarController: NSFetchedResultsControllerDelegate {
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
print("NSFRCD beginning updates.")
(myScrollView.documentView! as! NSOutlineView).beginUpdates()
}
func controller(
_ controller: NSFetchedResultsController<NSFetchRequestResult>,
didChange sectionInfo: NSFetchedResultsSectionInfo,
atSectionIndex sectionIndex: Int,
for type: NSFetchedResultsChangeType)
{
let myOutlineView:NSOutlineView = myScrollView.documentView! as! NSOutlineView
switch type {
case .insert:
print("Inserting a section.")
print("Index: \(sectionIndex)")
let parent = myOutlineView.child(topLevelSectionIndex!, ofItem: nil)!
print("inParent: \(String(describing: parent))")
myOutlineView.insertItems(
at: IndexSet([sectionIndex]),
inParent: parent,
withAnimation: .slideLeft)
case .delete:
print("Deleting a section.")
default:
return
}
}
func controller(
_ controller: NSFetchedResultsController<NSFetchRequestResult>,
didChange anObject: Any,
at indexPath: IndexPath?,
for type: NSFetchedResultsChangeType,
newIndexPath: IndexPath?)
{
return
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
print("NSFRCD ending updates.")
(myScrollView.documentView! as! NSOutlineView).endUpdates()
}
}

ObjectSidebarController has an IBOutlet myScrollView which references the scroll view in the sidebar. I build the NSOutlineView programmatically. Again, the NSOutlineView is view-based, not cell-based.

I have a bunch of print statements in my NSOutlineViewDataSource and NSOutlineViewDelegate letting me know when their methods are called. When I make the myOutlineView.insertItems call, I see the data source being asked for the supercategory I just inserted the new section into, and for child 0 of that supercategory:

Code Block text
Inserting a section.
Index: 0
Entering outlineView(_: child:2 ofItem:nil)
inParent: <supercategory>
Entering outlineView(_: child:2 ofItem:nil)
Entering outlineView(_: child:0 ofItem:Optional(<supercategory>))
About to return: <_NSDefaultSectionInfo: 0x600002eb6f80>
Entering outlineView(_: child:2 ofItem:nil)
Entering outlineView(_: child:0 ofItem:Optional(<supercategory>))
About to return: <_NSDefaultSectionInfo: 0x600002eb6f80>
NSFRCD ending updates.

But it doesn't ask the NSOutlineViewDelegate for the views. I have confirmed my code is called in the main thread, not a background thread. When I collapse the supercategory and open it back up, I get this in my logs:

Code Block text
Entering outlineView(_: numberOfChildrenOfItem:Optional(<supercategory>))
Entering outlineView(_: child:0 ofItem:Optional(<supercategory>))
About to return: <_NSDefaultSectionInfo: 0x600002eb6f80>
Entering outlineView(_: viewFor:Optional(<NSTableColumn: 0x6000022152d0> identifier: objectName width: 200) item:<_NSDefaultSectionInfo: 0x600002eb6f80>)

So it seems to me I need to do something to tell the NSOutlineView that I want it to get the views for the supercategory and its child. I have tried calling NSOutlineView.reloadItem(_:reloadChildren:) immediately after inserting the item passing it the same object I just did for the insert's parent, but no change in logs or behavior.

Does anybody have any idea what I'm doing wrong? Based on the documentation, it looks like this should work.
Answered by Zimmie in 671994022
Figured it out. My supercategories were defined as a struct with two properties: name and frc. While reading over some forum hits for the umpteenth time, this one finally clicked. Switched them over to use a class instead, and now everything is happy. From:
Code Block Swift
struct sidebarCategoryItem {
let name:String
let frc:NSFetchedResultsController<CPMObject_CD>
}

to this instead:
Code Block Swift
class sidebarCategoryItem {
let name:String
let frc:NSFetchedResultsController<CPMObject_CD>
init(name initName:String, frc initFRC:NSFetchedResultsController<CPMObject_CD>) {
self.name = initName
self.frc = initFRC
}
}

+5 lines of code, and -a million lines added for troubleshooting.
Realized a bit ago I forgot to include the code which sets topLevelSectionIndex. It was in a block of debug prints which I trimmed from the code above:
Code Block Swift
let topLevelSectionIndex:Int? = {
for (index, section) in objectSections.enumerated() {
if controller == section.frc { return index }
}
return nil
}()
let myOutlineView:NSOutlineView = myScrollView.documentView! as! NSOutlineView
switch type {
...

I tried adding this code to my case .insert: just above the insertItems:
Code Block Swift
print("Parent is expandable? \(myOutlineView.isExpandable(parent))")
print("Parent is expanded? \(myOutlineView.isItemExpanded(parent))")
myOutlineView.expandItem(parent)
print("Parent is expanded? \(myOutlineView.isItemExpanded(parent))")

and it gave me this in my logs:
Code Block text
Parent is expandable? true
Parent is expanded? false
Parent is expanded? false

I get the same results whether the supercategory is expanded or collapsed before I download my data and my code is called. This makes me think the parent object I'm getting isn't the parent object the NSOutlineView wants me to use.
Okay, this is definitely something weird about the parent object. I tried this:
Code Block Swift
let parent = myOutlineView.child(topLevelSectionIndex!, ofItem: nil)!
print("parent: \(String(describing: parent))")
print("parent row: \(myOutlineView.row(forItem: parent))")
let row2Parent = myOutlineView.item(atRow: 2)!
print("row2Parent: \(String(describing:row2Parent))")
print("row2Parent row: \(myOutlineView.row(forItem: row2Parent))")

and I get this in my log:
Code Block text
parent: <supercategory>
parent row: -1
row2Parent: <supercategory>
row2Parent row: 2

The String(describing:) of both objects is identical, though the objects are clearly different internally. And if I tell insertItems to insert something into row2Parent, it works! The problem is I only know the row indexes when my application has no data downloaded. As I fetch stuff and as the user expands and collapses sections, the rows for the supercategories will change. I clearly can't get it just by the row number, and it isn't telling me the row number for the object I got from its child(_:ofItem:) method.

I've tried getting the object directly from the NSOutlineViewDataSource method outlineView(_:child:ofItem:). No change. Debug descriptions of the objects are identical, but it still shows row -1.

Tried getting the object directly from the array I use for the supercategories. No change.

Does anybody know what sources insertItems(_:at:inParent:) accepts for the object in the inParent?
Accepted Answer
Figured it out. My supercategories were defined as a struct with two properties: name and frc. While reading over some forum hits for the umpteenth time, this one finally clicked. Switched them over to use a class instead, and now everything is happy. From:
Code Block Swift
struct sidebarCategoryItem {
let name:String
let frc:NSFetchedResultsController<CPMObject_CD>
}

to this instead:
Code Block Swift
class sidebarCategoryItem {
let name:String
let frc:NSFetchedResultsController<CPMObject_CD>
init(name initName:String, frc initFRC:NSFetchedResultsController<CPMObject_CD>) {
self.name = initName
self.frc = initFRC
}
}

+5 lines of code, and -a million lines added for troubleshooting.
NSOutlineView.insertItems(at:inParent:withAnimation) not drawing new items
 
 
Q