Assertion Failure in UICollectionViewData indexPathForItemAtGlobalIndex - objective-c

I am using performBatchUpdates() to update my collection view, where I am doing a complete refresh, i.e. delete whatever was in it and re-insert everything. The batch updates are done as part of an Observer which is attached to a NSMutableArray (bingDataItems).
cellItems is the array containing items that are or will be inserted into the collection view.
Here is the code:
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
cultARunner *_cultARunner = [cultARunner getInstance];
if ( [[_cultARunner bingDataItems] count] ) {
[self.collectionView reloadData];
[[self collectionView] performBatchUpdates: ^{
int itemSize = [cellItems count];
NSMutableArray *arrayWithIndexPaths = [NSMutableArray array];
// first delete the old stuff
if (itemSize == 0) {
[arrayWithIndexPaths addObject: [NSIndexPath indexPathForRow: 0 inSection: 0]];
}
else {
for( int i = 0; i < cellItems.count; i++ ) {
[arrayWithIndexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]];
}
}
[cellItems removeAllObjects];
if(itemSize) {
[self.collectionView deleteItemsAtIndexPaths:arrayWithIndexPaths];
}
// insert the new stuff
arrayWithIndexPaths = [NSMutableArray array];
cellItems = [_cultARunner bingDataItems];
if ([cellItems count] == 0) {
[arrayWithIndexPaths addObject: [NSIndexPath indexPathForRow: 0 inSection: 0]];
}
else {
for( int i = 0; i < [cellItems count]; i++ ) {
[arrayWithIndexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]];
}
}
[self.collectionView insertItemsAtIndexPaths:arrayWithIndexPaths];
}
completion:nil];
}
}
I get this error, but not all of the times (why ?)
2012-12-16 13:17:59.789 [16807:19703] *** Assertion failure in -[UICollectionViewData indexPathForItemAtGlobalIndex:], /SourceCache/UIKit_Sim/UIKit-2372/UICollectionViewData.m:442
2012-12-16 13:17:59.790 [16807:19703] DEBUG: request for index path for global index 1342177227 when there are only 53 items in the collection view
I checked the only thread that mentioned the same problem here: UICollectionView Assertion failure, but it is not very clear i.e. doing [collectionview reloadData] is not advisable in the performBatchUpdates() block.
Any suggestions on what might be going wrong here ?

Finally! Ok, here's what was causing this crash for me.
As previously noted, I was creating supplementary views in order to provide custom-styled section headers for my collection view.
The problem is this: it appears that the indexPath of a supplementary view MUST correspond to the indexPath of an extant cell in the collection. If the supplementary view's index path has no corresponding ordinary cell, the application will crash. I believe that the collection view attempts to retrieve information for a supplementary view's cell for some reason during the update procedure. It crashes when it cannot find one.
Hopefully this will solve your problem too!

This is the proper workaround to this crash:
Each of your supplementary views are associated with a certain index path. If you don't have a cell at that index path (initial load, you've deleted the row, etc), return a height of 0 for your supplementary view via your layout's delegate.
So, for a flow layout, implement UICollectionViewDelegateFlowLayout's
(CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section
method (and the corresponding footer method, if you're using footers) with the following logic
if ( you-have-a-cell-at-the-row-for-this-section )
return myNormalHeaderSize;
else return CGSizeMake( 0,0 );
Hope this helps!

reloadData doesn't work for me, because the whole purpose of using performBatchUpdates is to get the changes animated. If you use reloadData you only refresh the data, but without animations.
So suggestions of "replace performBatchUpdates with reloadData" is pretty much saying "give up on what you're trying to do."
I'm sorry, I'm just frustrated because this error keeps coming up for me while I'm trying to do some great animated updates and my model is 100 % correct, it's some iOS magic inside getting broken and forcing me to change my solutions completely.
My opinion is that Collection Views are still buggy and can't do complicated animated refreshes, even though they should be able to. Because this used to be the same thing for Table Views but those are now pretty stable (it took time, though).
//Edit (Sep 1, 2013)
The reported bug is closed now so this issues seems to have been resolved by Apple already.

I have been having the same problem.
I have tried a number of variations, but the final one that seems to work is [self.collectionView reloadData], where "self.collectionView"is the name of your collection view.
I have tried the following methods, straight from the "UICollectionView Class Reference": inserting, moving, and deleting items.
These were used at first, to "move" the item from one section to another.
deleteItemsAtIndexPaths:
insertItemsAtIndexPaths:
Next, I tried moveItemAtIndexPath:toIndexPath:.
They all produced the following error:
Assertion failure in -[UICollectionViewData indexPathForItemAtGlobalIndex:], /SourceCache/UIKit_Sim/UIKit-2372/UICollectionViewData.m:442
So, try the "reloadData" method.

If you remove the last cell from a section containing header/footer the bug appears.
I tried to return nil for header/footer size/element at that time and this sometimes fixes the issue.
Options:
Reload the whole table view instead of animating the removal of the last item.
Add an additional invisible, basic cell with a size less than 1.

A cheeseball mistake that can lead to this error is reusing the same UICollectionViewFlowLayout on multiple collectionViews on the same viewcontroller! Just init different flowLayouts for each collectionview and you'll be good to go!

I ran into this problem when I delete one of the cells from my collection view.
The problem was that I use a custom layout, and the call layoutAttributesForElementsInRect was returning more than the number of cells in the collection view after the delete.
Apparently UICollectionView just iterates through the array returned by the method without checking the number of cells.
Modifying the method to return the same number of layout attributes solved the crash.

I still couldn't figure out how the global index was incremented so much, but I solved my problem by inserting a temporary item in the underlying datasource array i.e. cellItems and calling [self.collectionview reloadData] in viewDidLoad().
This inserts a placeholder cell temporarily in the collection view until I trigger the actual process using performBatchUpdates().

Related

Updating NSTableView datasource asynchronously

I have been googling on this subject, but didn't seem able to find a consensus on the solution to this type of problem. When I use a data source with an NSTableView, if I need to populate the data source in background, there're a couple questions that pop in my mind regarding threading. I'm hoping to get some guidance here.
What would happen if I modified the data source between the main threading calling [NSTableView numberOfRowsInTableView:] and [NStableView tableView:objectValueForTableColumn:row:]? If the object the table view is asking for isn't valid anymore, what should I do?
Is making change to data source only on main thread the solution to this situation?
If 2 is the answer, does it apply to the case when binding is used?
If your data source takes some time to populate, and you're currently showing a table with older data, I think you have a couple of options:
Show a spinner over the UI while the re-population occurs, then call [tableView reloadData]
Keep the older data around so the tableView remains responsive, then once the new data has been fetched/computed, tell the datasource about the new NSArray (or whatever object holds the new data), and call [tableView reloadData].
You can't be changing the data backing your datasource on the fly, unless you inform the tableView of each item/row changing as you go.
To address the threading part, you can use a background thread to populate an NSArray of new data, once complete switch to the main thread, and on that call [dataSource setBackingArray:newStuff]; [tableView reloadData];
Many thanks to Graham Perks in comments to one of answers. This actually an answer worth to be written out explicitly. I want just to add small snippet from my project as illustration:
- (void) populateTable
{
DAL *dal = [[DAL alloc] init]; // MySQL engine
NSMutableArray *tmp = [NSMutableArray new];
NSMutableArray *records = [dal RetrieveRecordswithSql:#"select id, serial, scannerid, scans, offloaded, uploaded from scan_set_v3" withColumnsCount:#(6) andColumnsDelimiter:ScanSetRecordColumnDelimiter];
for (NSString *rec in records) {
ScanSetRecord *newRec = [[ScanSetRecord alloc] initWithScanSet:rec];
if (newRec) {
[tmp addObject:newRec];
}
}
self.dataArray = tmp;
[self.tableView reloadData];
}

collectionView:cellForItemAtIndexPath: doesn't get called

I want to add new cells in my collection view, but nothing shows up when I add data.
I have a custom UICollectionViewLayout class, which has been working just fine, and I've been keeping dummy data in my datasource to adjust the layout. Now that I got rid of the dummy data, nothing's showing up.
Since the app didn't break and there weren't any warnings, it was difficult to track down where the problem was, and here's where I found a clue:
(UICollectionViewLayout class)
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSLog(#"ElementsInRect: – Visible cells info: %#", [self.collectionView.visibleCells description]);
...
}
Here, -visibleCells returns an empty array, even when I add data, call -reloadData and invalidate the layout. So I placed a breakpoint in -collectionView:cellForItemAtIndexPath:, and it turns out this method is not called at all. How did the cells show up before?
Any help would be appreciated.
The data source method, collectionView:numberOfItemsInSection:, has to return a non-zero number for collectionView:cellForItemAtIndexPath: to be called. When you had dummy data in your data source, it was. Now that you removed that dummy data, that method is probably returning 0. When you add data, it should put items into your data source, and then a call to reloadData should work. You should put a log in collectionView:numberOfItemsInSection:, and see what it's returning.
Okay, it turns out the issue was in UICollectionViewLayout. I doubt anyone else will be having this problem, but I'll write my answer for the sake of completeness:
I'd been tweaking my custom UICollectionViewLayout class, and after I'd thought that it was working well, I made the code look neat by deleting old code that was commented out, move methods, etc.
While doing that, I recalled having read somewhere that it's good practice to create attributes in -prepareLayout method, and return those attributes when -layoutAttributesForItemAtIndexPath: or -layoutAttributesForElementsInRect: is called. For me, it was a matter of moving a block of code, so I thought no biggie. And during this "cleaning process" I must have made a mistake.
What's really frustrating is that the code itself actually works regardless of where the attributes are created, and I can't tell what went wrong for the last few days.
The following is a snippet of code that I used to create the attributes objects. My initial question was asking why -collectionView:cellForItemAtIndexPath: was not called while executing the 3rd line. I did not change this part of the code, other than moving it around.
for (int i = 0; i < 3; i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:self.topLayer];
UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPath];
if (cell) {
UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:indexPath];
[self.array addObject:attributes];
} else {
NSLog(#"prepLayout: the cell doesn't exist for the index path {%d – %d}", indexPath.section, indexPath.item);
}
}
Number of Rows in Section - the count that can be used will determine if the cellForItemAtIndexPath gets called.
Initially when the view loads this will be called. Within the numberOfItemsInSection, if you have an array, the [array count] might return a nil value.
Complete the procedure where the array is populated, then reload the data in the collection view which will re-assess the numberOfItemsInSection. This can be done with the following code:
[self.myCollectionView reloadData];
"myCollectionView is the name given to the collection view item in your view"

NSTableView doesn't refresh?

I have an NSTableView whose data source is an NSMutableArray. The count of the array is returned in the numberOfRowsInTableView: method.
The table is refreshed by make a selection in other components such as an NSPopupButton, which changes the content of the array and performs the reloadData method.
The problem is that sometimes I am getting kind of data being written all over each other. So for example if the count is 4 and then after an action is performed it is 10, the additional 6 records are written below the first 4. It is only after I manually scroll the table up and down that the data (correct data) is displayed.
The console displays a message that
-[NSCFArray objectAtIndex:]: index(-1(or possibly larger)) beyond bounds (1056).
I understand that the error message seems to point out that it is an array beyond bounds, however how can that be when I am only returning, say [array objectAtIndex: rowIndex] in the
- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex
method?
Also, the count return is returned in the numberOfRowsInTableView is always being correct (I know this since I am logging the count using a timer every few seconds).
Any ideas? Thanks!
Edit 1:
I am returning the data as follows:
pc = [[PrinterClass alloc]init];
pc = [ads_printers_array objectAtIndex:rowIndex];
if (aTableColumn == tc_make)
return [pc make];
if (aTableColumn == tc_model)
return [pc model];
if (aTableColumn == tc_driver_name)
return [pc ppd];
if (aTableColumn == tc_rdp_driver)
{
if ([pc decider] == 1)
return [pc rdp];
else return [pc model];
}
The array that I mention above is filled with object of a class that I created called PrintersClass.
To reload the data, it's just reload data as usual:
[ads_rdp_driver_table reloadData];
With ads_rdp_driver_table being the tableView name.
The thing is that this problem is coming up at random intervals at not at specific points. That is why I can't put my finger on it..
Are you 100% sure you have connected the delegate and datasource ?
You can also do this in code
ads_rdp_driver_table.datasource = self;
ads_rdp_driver_table.delegate = self;
add this is in f.ex. awake from nib.
You also have to connect the tableview itself to an iboutlet, otherwise you can not call it by name.

Opening a gap in NSTableView during drag and drop

I've got a simple, single-column, view-based NSTableView with items in it that can be dragged to reorder them. During drag and drop, I'd like to make it so that a gap for the item-to-be-dropped opens up at the location under the mouse. GarageBand does something like this when you drag to reorder tracks (video here: http://www.screencast.com/t/OmUVHcCNSl). As far as I can tell, there's no built in support for this in NSTableView.
Has anyone else tried to add this behavior to NSTableView and found a good solution? I've thought of and tried a couple approaches without much success. My first thought was to double the height of the row under the mouse during a drag by sending -noteHeightOfRowsWithIndexesChanged: in my data source's -tableView:validateDrop:... method, then returning twice the normal height in -tableView:heightOfRow:. Unfortunately, best I can tell, NSTableView doesn't update its layout during drag and drop, so despite calling noteHeightOfRowsWithIndexesChanged:, the row height isn't actually updated.
Note that I'm using a view-based NSTableView, but my rows are not so complex that I couldn't move to a cell-based table view if doing so helped accomplish this. I'm aware of the easy, built-in ability to animate a gap for the dropped item after a drag is complete. I'm looking for a way to open a gap while the drag is in progress. Also, this is for an app to be sold in the Mac App Store, so it must not use private API.
EDIT: I've just filed an enhancement request with Apple requesting built in support for this behavior: http://openradar.appspot.com/12662624. Dupe if you'd like to see it too. Update: The enhancement I requested was implemented in OS X 10.9 Mavericks, and this behavior is now available using NSTableView API. See NSTableViewDraggingDestinationFeedbackStyleGap.
I feel bizarre for doing this, but there's an extremely thorough answer in the queue here that appears to have been deleted by its author. In it, they provided the correct links to a working solution, which I feel need to be presented as an answer for someone else to take and run with, inclusive of them if they desire to do so.
From the documentation for NSTableView, the following caveats are tucked away for row animation effects:
Row Animation Effects
Optional constant that specifies that the tableview will use a fade for row or column removal. The effect can be combined with any NSTableViewAnimationOptions constant.
enum {
NSTableViewAnimationEffectFade = 0x1,
NSTableViewAnimationEffectGap = 0x2,
};
Constants:
...
NSTableViewAnimationEffectGap
Creates a gap for newly inserted rows. This is useful for drag and drop animations that animate to a newly opened gap and should be used in the delegate method tableView:acceptDrop:row:dropOperation:.
Going through the example code from Apple, I find this:
- (void)_performInsertWithDragInfo:(id <NSDraggingInfo>)info parentNode:(NSTreeNode *)parentNode childIndex:(NSInteger)childIndex {
// NSOutlineView's root is nil
id outlineParentItem = parentNode == _rootTreeNode ? nil : parentNode;
NSMutableArray *childNodeArray = [parentNode mutableChildNodes];
NSInteger outlineColumnIndex = [[_outlineView tableColumns] indexOfObject:[_outlineView outlineTableColumn]];
// Enumerate all items dropped on us and create new model objects for them
NSArray *classes = [NSArray arrayWithObject:[SimpleNodeData class]];
__block NSInteger insertionIndex = childIndex;
[info enumerateDraggingItemsWithOptions:0 forView:_outlineView classes:classes searchOptions:nil usingBlock:^(NSDraggingItem *draggingItem, NSInteger index, BOOL *stop) {
SimpleNodeData *newNodeData = (SimpleNodeData *)draggingItem.item;
// Wrap the model object in a tree node
NSTreeNode *treeNode = [NSTreeNode treeNodeWithRepresentedObject:newNodeData];
// Add it to the model
[childNodeArray insertObject:treeNode atIndex:insertionIndex];
[_outlineView insertItemsAtIndexes:[NSIndexSet indexSetWithIndex:insertionIndex] inParent:outlineParentItem withAnimation:NSTableViewAnimationEffectGap];
// Update the final frame of the dragging item
NSInteger row = [_outlineView rowForItem:treeNode];
draggingItem.draggingFrame = [_outlineView frameOfCellAtColumn:outlineColumnIndex row:row];
// Insert all children one after another
insertionIndex++;
}];
}
I'm unsure if it's really this simple, but it's at least worth inspection and outright refutal if it doesn't meet your needs.
Edit: see this answer's comments for the steps followed to the right solution. The OP has posted a more complete answer, which should be referred to by anyone looking for solutions to the same problem.
Note: The behavior this question and answer describes are now available using built in API in NSTableView on OS X 10.9 Mavericks and later. See NSTableViewDraggingDestinationFeedbackStyleGap.
This answer may still be useful if this behavior is needed in an app targeting OS X 10.8 or earlier.
Original answer below:
I've implemented this now. My basic approach looks like this:
#interface ORSGapOpeningTableView : NSTableView
#property (nonatomic) NSInteger dropTargetRow;
#property (nonatomic) CGFloat heightOfDraggedRows;
#end
#implementation ORSGapOpeningTableView
#pragma mark - Dragging
- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender
{
NSInteger oldDropTargetRow = self.dropTargetRow;
NSDragOperation result = [super draggingUpdated:sender];
CGFloat imageHeight = [[sender draggedImage] size].height;
self.heightOfDraggedRows = imageHeight;
NSMutableIndexSet *changedRows = [NSMutableIndexSet indexSet];
if (oldDropTargetRow > 0) [changedRows addIndex:oldDropTargetRow-1];
if (self.dropTargetRow > 0) [changedRows addIndex:self.dropTargetRow-1];
[self noteHeightOfRowsWithIndexesChanged:changedRows];
return result;
}
- (void)draggingExited:(id<NSDraggingInfo>)sender
{
self.dropTargetRow = -1;
[self noteHeightOfRowsWithIndexesChanged:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [self numberOfRows])]];
[super draggingExited:sender];
}
- (void)draggingEnded:(id<NSDraggingInfo>)sender
{
self.dropTargetRow = -1;
self.heightOfDraggedRows = 0.0;
self.draggedRows = nil;
[self noteHeightOfRowsWithIndexesChanged:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [self numberOfRows])]];
}
- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender
{
self.dropTargetRow = -1;
self.heightOfDraggedRows = 0.0;
self.draggedRows = nil;
[self noteHeightOfRowsWithIndexesChanged:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [self numberOfRows])]];
return [super performDragOperation:sender];
}
// In my delegate and data source:
- (NSDragOperation)tableView:(NSTableView *)tableView validateDrop:(id<NSDraggingInfo>)info proposedRow:(NSInteger)row proposedDropOperation:(NSTableViewDropOperation)dropOperation
{
if (dropOperation == NSTableViewDropOn)
{
dropOperation = NSTableViewDropAbove;
[self.tableView setDropRow:++row dropOperation:dropOperation];
}
NSDragOperation result = [self.realDataSource tableView:tableView validateDrop:info proposedRow:row proposedDropOperation:dropOperation];
if (result != NSDragOperationNone)
{
self.tableView.dropTargetRow = row;
}
else
{
self.tableView.dropTargetRow = -1; // Don't open a gap
}
return result;
}
- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row
{
CGFloat result = [tableView rowHeight];
if (row == self.tableView.dropTargetRow - 1 && row > -1)
{
result += self.tableView.heightOfDraggedRows;
}
return result;
}
Note that this is simplified code, not a verbatim copy/paste from my program. I actually ended up making this all contained within an NSTableView subclass that uses proxy delegate and data source objects so the code in data source/delegate methods above is actually inside the proxies' intercept of the calls to the real delegate and data source. That way, the real data source and delegate don't have to do anything special to get the gap opening behavior. Also, there's sometimes a little flakiness with the table view animations, and this doesn't work for drags above the first row (no gap is opened since there's no row to make taller). All in all, despite the room for further improvement, this approach works reasonably well.
I'd still like to try a similar approach, but insert a blank row (as Caleb suggested) instead of changing the row height.
As of Mac OS X 10.9 (Mavericks), there's a much easier solution to animating drag & drop in a NSTableView:
[aTableView setDraggingDestinationFeedbackStyle:NSTableViewDraggingDestinationFeedbackStyleGap];
The table view will automatically insert gaps with animation as a row is dragged which is much nicer than the old blue line insertion point method.
One way to accomplish what you're asking is to insert an empty row at the proposed drop point (that is, between the two nearest rows). It sounds like you've been looking at using NSTableViewAnimationEffectGap, which as you note is really meant for animating the insertion when the drop is accepted in -tableView:acceptDrop:row:dropOperation:.
Since you want to open up the gap before the user releases the mouse button to actually do the drop, you could instead insert a blank row using -insertRowsAtIndexes:withAnimation: from your table's -draggingUpdate: method and at the same time delete any blank row you previously inserted for this drag using -removeRowsAtIndexes:withAnimation:. Use NSTableViewAnimationSlideUp and NSTableViewAnimationSlideDown as the animations for these operations, as appropriate.

reloadData in NSTableView but keep current selection

I have anNSTableView showing the contents of a directory. I watch for FSEvents, and each time I get an event I reload my table view.
Unfortunately, the current selection then disappears. Is there a way to avoid that?
Well, you can save selection before calling reloadData and restore it after that.
NSInteger row = [self.tableView selectedRow];
[self.tableView reloadData];
[self.tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
This worked for me in some cases. But if you insert some items BEFORE the selected row, you should andjust your row variable by adding count of added items to it.
Swift 4.2
Create an extension and add a method which preserves selection.
extension NSTableView {
func reloadDataKeepingSelection() {
let selectedRowIndexes = self.selectedRowIndexes
self.reloadData()
self.selectRowIndexes(selectedRowIndexes, byExtendingSelection: false)
}
}
Do this in case you use the traditional way of populating table views (not NSArrayController).
It depends on how you populate your NSTableView.
If you have the table view bound to an NSArrayController, which in turn contain the items that your table view is displaying, then the NSArrayController has an option to preserve the selection. You can select it (or not) from within Interface Builder as a property on the NSArrayController. Or you can use the setPreservesSelection method in code.
However, if you completely replace the array of items that the NSArrayController manages each time you get your FSEvents, then maybe the preservation of selection cannot work. Unfortunately the Apple docs on this property of NSArrayController are a bit vague as to when it can and cannot preserve the selection.
If you are not using an NSArrayController, but maybe using a dataSource to populate the table view, then I think you'll have to manage the selection yourself.
In the case of using Data Source, Apple Documentation in the header file on reloadData() is that
The selected rows are not maintained.
To get around, you can use reloadDataForRowIndexes(rowIndexes: NSIndexSet, columnIndexes: NSIndexSet). As mentioned in the same header file
For cells that are visible, appropriate dataSource and delegate methods will be called and the cells will be redrawn.
Thus the data will be reloaded, and the selection is kept as well.
A variant on #silvansky's answer.
This one has no need to keep track of count of items inserted/deleted. And it maintains multiple selection.
The idea is to...
1. create an array of selected objects/nodes from the current selection.
2. refresh the table using reloadData
3. for each object obtained in step 1, find/record it's new index
4. tell the table view/outline view to select the updated index set
- (void)refresh {
// initialize some variables
NSIndexSet *selectedIndexes = [self.outlineView selectedRowIndexes];
NSMutableArray *selectedNodes = [NSMutableArray array];
NSMutableIndexSet *updatedSelectedIndex = [NSMutableIndexSet indexSet];
// 1. enumerate all selected indexes and record the nodes/objects
[selectedIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
[selectedNodes addObject:[self.outlineView itemAtRow:idx]];
}];
// 2. refresh the table which may add new objects/nodes
[self.outlineView reloadData];
// 3. for each node in step 1, find the new indexes
for (id selectedNode in selectedNodes) {
[updatedSelectedIndex addIndex:[self.outlineView rowForItem:selectedNode]];
}
// 4. tell the outline view to select the updated index set
[self.outlineView selectRowIndexes:updatedSelectedIndex byExtendingSelection:NO];
}