Become a MacRumors Supporter for $50/year with no ads, ability to filter front page stories, and private forums.

0002378

Suspended
Original poster
May 28, 2017
675
671
EDIT - Please skip to the last (most recent) post in this thread to see the relevant problem.

Short version
: What is the NSOutlineView's equivalent of NSTableView's noteNumberOfRowsChanged(), in order to reflect nodes being added/removed ?

(noteNumberOfRowsChanged() has no effect on NSOutlineView) i.e. what is the most optimal way to partially refresh an NSOutlineView in response to a change in its model ?

Long version:

Hello, I have a hierarchical view in an NSOutlineView. It is 2 levels deep, below the (imaginary) root node, i.e. there may be multiple expandable items at the top level (i.e. just below the root), and any number of child nodes at most one level down. This is illustrated in the example image below:

outlineView.png


Items may be added, removed, or updated over time, in response to user actions. The total number of items can be as large as a few hundred or even a 1000 (there is no constraint). In other words, calling reloadData() each time a node is added is really not an (good) option.

How do I partially refresh this NSOutlineView, in response to a single new top-level row being added ? Let's say I added a new name "Ralph" at the top level. What would be the most efficient/optimal way to refresh the NSOutlineView to get "Ralph" (and his child nodes) to show up ?

In an NSTableView, this is easily achieved through noteNumberOfRowsChanged(). But, that method seems to have no effect on the NSOutlineView. I really do not want to call reloadData() every time a node is added, as it is not optimal in terms of performance.

I'm aware of reloadItem() but this doesn't help for new items being added to the root. Calling reloadItem() on the root node is equivalent to reloadData(), so it doesn't help me.

Thanks !
 
Last edited:
If you take another look at the API reference for NSOutlineView, there are these methods:

func insertItems(at: IndexSet, inParent: Any?, withAnimation: NSTableView.AnimationOptions = [])
func removeItems(at: IndexSet, inParent: Any?, withAnimation: NSTableView.AnimationOptions = [])

They allow your controller to insert and remove items in the NSOutlineView in response to changes in your model. The NSOutlineView will not reload the entire view when you use these methods.

You can also use func reloadItem(Any?) to respond to model items changing.
 
  • Like
Reactions: 0002378
If you take another look at the API reference for NSOutlineView, there are these methods:

func insertItems(at: IndexSet, inParent: Any?, withAnimation: NSTableView.AnimationOptions = [])
func removeItems(at: IndexSet, inParent: Any?, withAnimation: NSTableView.AnimationOptions = [])

They allow your controller to insert and remove items in the NSOutlineView in response to changes in your model. The NSOutlineView will not reload the entire view when you use these methods.

You can also use func reloadItem(Any?) to respond to model items changing.

Excellent answer ... exactly what I was looking for ! Thank you. (Hits Like button 5 times)

Funny thing is ... I discovered these methods yesterday shortly after posting this thread :) I was just about to post my own solution here when I saw your post.
 
Excellent answer ... exactly what I was looking for ! Thank you. (Hits Like button 5 times)

Funny thing is ... I discovered these methods yesterday shortly after posting this thread :) I was just about to post my own solution here when I saw your post.

Glad it all worked out - good luck with your app!
 
  • Like
Reactions: 0002378
[doublepost=1508746151][/doublepost]
Glad it all worked out - good luck with your app!

Hi Erendiox,

While it was working initially, I'm now running into a runtime exception when calling insertItems(...). I've attached relevant code and stack trace snippets (below dashed line).

Any idea what I'm doing wrong ? I can share more source code / details if necessary.

Thanks very much for your help.

--------------------------

I'm running into a runtime exception when, as suggested above, inserting new items into an NSOutlineView.

I'm including the relevant snippet of code, snippet of the stack trace, and the full stack trace.

My code:

Code:
// groupIndex is the index of the new item to be inserted, within a data array

self.playlistView.insertItems(at: IndexSet(integer: groupIndex), inParent: nil, withAnimation: .effectFade)

Snippet of stack trace:

Code:
2017-10-23 00:58:03.550569-0700 Aural[2253:38218] [General] An uncaught exception was raised
2017-10-23 00:58:03.550609-0700 Aural[2253:38218] [General] (null) should not be expanded already!
2017-10-23 00:58:03.550739-0700 Aural[2253:38218] [General] (
    0   CoreFoundation                      0x00007fffa78f157b __exceptionPreprocess + 171
    1   libobjc.A.dylib                     0x00007fffbcb351da objc_exception_throw + 48
    2   CoreFoundation                      0x00007fffa78f6132 +[NSException raise:format:arguments:] + 98
    3   Foundation                          0x00007fffa935dc80 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 195
    4   AppKit                              0x00007fffa546b157 -[NSOutlineView _expandItemEntry:expandChildren:startLevel:] + 1300
    5   AppKit                              0x00007fffa54608d9 -[NSOutlineView _uncachedNumberOfRows] + 548
    6   AppKit                              0x00007fffa5460b23 -[_NSTableRowHeightStorage numberOfRows] + 62
    7   AppKit                              0x00007fffa5542f2f -[NSTableRowData _validateIndexesForInsertion:] + 61
    8   AppKit                              0x00007fffa5542de3 -[NSTableRowData _insertUpdateItem:atIndexes:] + 46
    9   AppKit                              0x00007fffa546bccb -[NSTableRowData insertRowsAtIndexes:withRowAnimation:] + 273
    10  AppKit                              0x00007fffa546ba12 -[NSTableView insertRowsAtIndexes:withAnimation:] + 237
    11  AppKit                              0x00007fffa546b91f -[NSOutlineView insertRowsAtIndexes:withAnimation:] + 39
    12  AppKit                              0x00007fffa5a1a638 -[NSOutlineView _insertItemsAtIndexes:inParentRowEntry:withAnimation:] + 1662
    13  AppKit                              0x00007fffa546bb3b -[NSTableView _doUpdatedWorkWithHandler:] + 227
    14  AppKit                              0x00007fffa5a1bc51 -[NSOutlineView insertItemsAtIndexes:inParent:withAnimation:] + 338
    15  Aural                               0x00000001000c82af _TFFC5Aural30GroupingPlaylistViewController19consumeAsyncMessageFPS_12AsyncMessage_T_U_FT_T_ + 1439

Full stack trace:

Code:
2017-10-23 00:58:03.550569-0700 Aural[2253:38218] [General] An uncaught exception was raised
2017-10-23 00:58:03.550609-0700 Aural[2253:38218] [General] (null) should not be expanded already!
2017-10-23 00:58:03.550739-0700 Aural[2253:38218] [General] (
    0   CoreFoundation                      0x00007fffa78f157b __exceptionPreprocess + 171
    1   libobjc.A.dylib                     0x00007fffbcb351da objc_exception_throw + 48
    2   CoreFoundation                      0x00007fffa78f6132 +[NSException raise:format:arguments:] + 98
    3   Foundation                          0x00007fffa935dc80 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 195
    4   AppKit                              0x00007fffa546b157 -[NSOutlineView _expandItemEntry:expandChildren:startLevel:] + 1300
    5   AppKit                              0x00007fffa54608d9 -[NSOutlineView _uncachedNumberOfRows] + 548
    6   AppKit                              0x00007fffa5460b23 -[_NSTableRowHeightStorage numberOfRows] + 62
    7   AppKit                              0x00007fffa5542f2f -[NSTableRowData _validateIndexesForInsertion:] + 61
    8   AppKit                              0x00007fffa5542de3 -[NSTableRowData _insertUpdateItem:atIndexes:] + 46
    9   AppKit                              0x00007fffa546bccb -[NSTableRowData insertRowsAtIndexes:withRowAnimation:] + 273
    10  AppKit                              0x00007fffa546ba12 -[NSTableView insertRowsAtIndexes:withAnimation:] + 237
    11  AppKit                              0x00007fffa546b91f -[NSOutlineView insertRowsAtIndexes:withAnimation:] + 39
    12  AppKit                              0x00007fffa5a1a638 -[NSOutlineView _insertItemsAtIndexes:inParentRowEntry:withAnimation:] + 1662
    13  AppKit                              0x00007fffa546bb3b -[NSTableView _doUpdatedWorkWithHandler:] + 227
    14  AppKit                              0x00007fffa5a1bc51 -[NSOutlineView insertItemsAtIndexes:inParent:withAnimation:] + 338
    15  Aural                               0x00000001000c82af _TFFC5Aural30GroupingPlaylistViewController19consumeAsyncMessageFPS_12AsyncMessage_T_U_FT_T_ + 1439
    16  Aural                               0x00000001000c932e _TPA__TFFC5Aural30GroupingPlaylistViewController19consumeAsyncMessageFPS_12AsyncMessage_T_U_FT_T_ + 142
    17  Aural                               0x000000010002add7 _TTRXFo___XFdCb___ + 39
    18  Foundation                          0x00007fffa92bb1a9 __NSBLOCKOPERATION_IS_CALLING_OUT_TO_A_BLOCK__ + 7
    19  Foundation                          0x00007fffa92bae8c -[NSBlockOperation main] + 101
    20  Foundation                          0x00007fffa92b95b4 -[__NSOperationInternal _start:] + 672
    21  Foundation                          0x00007fffa92b546b __NSOQSchedule_f + 201
    22  libdispatch.dylib                   0x0000000100e3b78c _dispatch_client_callout + 8
    23  libdispatch.dylib                   0x0000000100e49ac4 _dispatch_main_queue_callback_4CF + 362
    24  CoreFoundation                      0x00007fffa78a7d69 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
    25  CoreFoundation                      0x00007fffa786904d __CFRunLoopRun + 2221
    26  CoreFoundation                      0x00007fffa7868544 CFRunLoopRunSpecific + 420
    27  HIToolbox                           0x00007fffa6dc8ebc RunCurrentEventLoopInMode + 240
    28  HIToolbox                           0x00007fffa6dc8cf1 ReceiveNextEventCommon + 432
    29  HIToolbox                           0x00007fffa6dc8b26 _BlockUntilNextEventMatchingListInModeWithFilter + 71
    30  AppKit                              0x00007fffa5361a54 _DPSNextEvent + 1120
    31  AppKit                              0x00007fffa5add7ee -[NSApplication(NSEvent) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] + 2796
    32  AppKit                              0x00007fffa53563db -[NSApplication run] + 926
    33  AppKit                              0x00007fffa5320e0e NSApplicationMain + 1237
    34  Aural                               0x0000000100031b4d main + 13
    35  libdyld.dylib                       0x00007fffbd416235 start + 1
)
2017-10-23 00:58:03.561544-0700 Aural[2253:38218] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '(null) should not be expanded already!'
*** First throw call stack:
(
    0   CoreFoundation                      0x00007fffa78f157b __exceptionPreprocess + 171
    1   libobjc.A.dylib                     0x00007fffbcb351da objc_exception_throw + 48
    2   CoreFoundation                      0x00007fffa78f6132 +[NSException raise:format:arguments:] + 98
    3   Foundation                          0x00007fffa935dc80 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 195
    4   AppKit                              0x00007fffa546b157 -[NSOutlineView _expandItemEntry:expandChildren:startLevel:] + 1300
    5   AppKit                              0x00007fffa54608d9 -[NSOutlineView _uncachedNumberOfRows] + 548
    6   AppKit                              0x00007fffa5460b23 -[_NSTableRowHeightStorage numberOfRows] + 62
    7   AppKit                              0x00007fffa5542f2f -[NSTableRowData _validateIndexesForInsertion:] + 61
    8   AppKit                              0x00007fffa5542de3 -[NSTableRowData _insertUpdateItem:atIndexes:] + 46
    9   AppKit                              0x00007fffa546bccb -[NSTableRowData insertRowsAtIndexes:withRowAnimation:] + 273
    10  AppKit                              0x00007fffa546ba12 -[NSTableView insertRowsAtIndexes:withAnimation:] + 237
    11  AppKit                              0x00007fffa546b91f -[NSOutlineView insertRowsAtIndexes:withAnimation:] + 39
    12  AppKit                              0x00007fffa5a1a638 -[NSOutlineView _insertItemsAtIndexes:inParentRowEntry:withAnimation:] + 1662
    13  AppKit                              0x00007fffa546bb3b -[NSTableView _doUpdatedWorkWithHandler:] + 227
    14  AppKit                              0x00007fffa5a1bc51 -[NSOutlineView insertItemsAtIndexes:inParent:withAnimation:] + 338
    15  Aural                               0x00000001000c82af _TFFC5Aural30GroupingPlaylistViewController19consumeAsyncMessageFPS_12AsyncMessage_T_U_FT_T_ + 1439
    16  Aural                               0x00000001000c932e _TPA__TFFC5Aural30GroupingPlaylistViewController19consumeAsyncMessageFPS_12AsyncMessage_T_U_FT_T_ + 142
    17  Aural                               0x000000010002add7 _TTRXFo___XFdCb___ + 39
    18  Foundation                          0x00007fffa92bb1a9 __NSBLOCKOPERATION_IS_CALLING_OUT_TO_A_BLOCK__ + 7
    19  Foundation                          0x00007fffa92bae8c -[NSBlockOperation main] + 101
    20  Foundation                          0x00007fffa92b95b4 -[__NSOperationInternal _start:] + 672
    21  Foundation                          0x00007fffa92b546b __NSOQSchedule_f + 201
    22  libdispatch.dylib                   0x0000000100e3b78c _dispatch_client_callout + 8
    23  libdispatch.dylib                   0x0000000100e49ac4 _dispatch_main_queue_callback_4CF + 362
    24  CoreFoundation                      0x00007fffa78a7d69 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
    25  CoreFoundation                      0x00007fffa786904d __CFRunLoopRun + 2221
    26  CoreFoundation                      0x00007fffa7868544 CFRunLoopRunSpecific + 420
    27  HIToolbox                           0x00007fffa6dc8ebc RunCurrentEventLoopInMode + 240
    28  HIToolbox                           0x00007fffa6dc8cf1 ReceiveNextEventCommon + 432
    29  HIToolbox                           0x00007fffa6dc8b26 _BlockUntilNextEventMatchingListInModeWithFilter + 71
    30  AppKit                              0x00007fffa5361a54 _DPSNextEvent + 1120
    31  AppKit                              0x00007fffa5add7ee -[NSApplication(NSEvent) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] + 2796
    32  AppKit                              0x00007fffa53563db -[NSApplication run] + 926
    33  AppKit                              0x00007fffa5320e0e NSApplicationMain + 1237
    34  Aural                               0x0000000100031b4d main + 13
    35  libdyld.dylib                       0x00007fffbd416235 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
 
[doublepost=1508746151][/doublepost]

Hi Erendiox,

While it was working initially, I'm now running into a runtime exception when calling insertItems(...). I've attached relevant code and stack trace snippets (below dashed line).

Any idea what I'm doing wrong ? I can share more source code / details if necessary.

Unfortunately the exception message isn't terribly descriptive, but I can think of a few things that you could possibly be doing wrong.

Firstly, based on the stack trace, it looks like you're calling this method on a background thread using NSBlockOperation. If that's what you're doing, don't do that. This method and other UIKit APIs should only be called from the main thread. Weird things can happen otherwise.

The error message also seems like it could indicate that the NSOutlineView can't find a corresponding model object for this newly inserted item. Are you certain that you're calling this method *after* the model change has occurred? The updates you send to NSOutlineView should exactly mirror the change that just previously occurred to the model.

Hope that helps! If you're still having trouble, send more of your source code over.
 
  • Like
Reactions: 0002378
Unfortunately the exception message isn't terribly descriptive, but I can think of a few things that you could possibly be doing wrong.

Firstly, based on the stack trace, it looks like you're calling this method on a background thread using NSBlockOperation. If that's what you're doing, don't do that. This method and other UIKit APIs should only be called from the main thread. Weird things can happen otherwise.

The error message also seems like it could indicate that the NSOutlineView can't find a corresponding model object for this newly inserted item. Are you certain that you're calling this method *after* the model change has occurred? The updates you send to NSOutlineView should exactly mirror the change that just previously occurred to the model.

Hope that helps! If you're still having trouble, send more of your source code over.

Thanks for the tips.

I will modify/verify the code based on your suggestions, and get back to you.

For now, I do have one question related to your second point. I can't seem to understand NSOutlineView behavior from reading the docs.

Let's say that a large number of model updates (that affect the NSOutlineView) happen without refreshing the NSOutlineView. Then, I try to insert the first of those into NSOutlineView at index 0 (i.e. index passed to insertItems()) .. will that in itself cause a problem, because my model has 3 new items and not just the one I'm trying to insert ? In other words, do I need to make sure that insertItems() keeps up with every single change to the model ? Can this be a race condition if model changes are happening in the back end (on a background thread) faster than I'm able to update the view on the main thread ?

Again, thanks, and I'll get back to you after doing some due diligence on my end.
 
Hope that helps! If you're still having trouble, send more of your source code over.

Ok, I think I've narrowed down the problem significantly (i.e. taken out much of the original complexity/scope).

I created a separate (and much simpler) project/app just to reproduce the problem in the NSOutlineView. I recreated the view setup from my original app. I have a tab group with multiple NSOutlineView instances, one in each tab. Please see image below.

problem.png


Relevant code snippet:

Code:
@IBAction func addItemAction(_ sender: Any) {

        // Grab a track from the data array (just a temporary container for data)
        let track = tracks.remove(at: 0)
 
        // Insert the new track into both playlists (model)
        let needToInsertNewArtistsGroup: Bool = artists.addTrackForGroupInfo(track)
        let needToInsertNewAlbumsGroup: Bool = albums.addTrackForGroupInfo(track)
 
        // Update the views
 
// If a new artist group was created as a result of the track add, update the artists view
// NOTE - THIS INSERTION SUCCEEDS WHEN ARTISTS TAB IS THE ONE SHOWN
        if needToInsertNewArtistsGroup {
            print("\nAdding artists group")
        artistsView.insertItems(at: IndexSet(integer: artists.getNumberOfGroups() - 1), inParent: nil, withAnimation: NSTableViewAnimationOptions.effectFade)
            print("\tAdded artists group")
        }
 
// If a new album group was created as a result of the track add, update the albums view
// NOTE - THIS INSERTION FAILS WHEN ARTISTS TAB IS THE ONE SHOWN
        if needToInsertNewAlbumsGroup {
        print("\nAdding albums group")
        albumsView.insertItems(at: IndexSet(integer: albums.getNumberOfGroups() - 1), inParent: nil, withAnimation: NSTableViewAnimationOptions.effectFade)
            print("\tAdded albums group")
        }
    }

The runtime exception related to insertItems occurs only when inserting into a view that is not currently displayed. In other words, in my tab group shown above, with the Artists tab shown, the insertion into the Artists view occurs with no problems. The insertion into the Albums view is the only one that fails. And, I verified it the other way around - showing the Albums view (in this case, the Artists view insertion fails).

Summary: The insertion into an NSOutlineView fails if and only if the view is not currently displayed, within a tab group.

P.S. I verified both of the following pre-conditions you mentioned in your previous post:
1 - All view updates occur only in the main thread (my replica project only does work in the main thread).
2 - The model update certainly happens before the view update is performed.

EDIT 1 - One more key bit of info: The update seems to fail only when the view is empty. i.e. only if the inserted item is the very first bit of info being inserted. If the view has been loaded (upon app startup) with reloadData() and then insertItems() is called subsequently, it succeeds regardless of whether or not the view is currently displayed.

EDIT 2 - I figured out a hack to get around this problem. When the app loads, if I simulate the user selecting (and consequently displaying the views under) each of the tabs in the tab group, then, each of the views is somehow "initialized" properly, and this problem never occurs. i.e.:

Code:
tabs.selectTabViewItem(at: 0)
tabs.selectTabViewItem(at: 1)
...
tabs.selectTabViewItem(at: n - 1)

This behavior is consistent with my observation that, for views that are displayed, insertion succeeds while it fails for views that are not currently displayed.

Of course, I would like to figure out a proper solution. But, for now, I guess I will be using this hack :D
 
Last edited:
Thanks for the tips.

Let's say that a large number of model updates (that affect the NSOutlineView) happen without refreshing the NSOutlineView. Then, I try to insert the first of those into NSOutlineView at index 0 (i.e. index passed to insertItems()) .. will that in itself cause a problem, because my model has 3 new items and not just the one I'm trying to insert ? In other words, do I need to make sure that insertItems() keeps up with every single change to the model ? Can this be a race condition if model changes are happening in the back end (on a background thread) faster than I'm able to update the view on the main thread ?

There's potentially a risk of some kind of race condition if you don't time your updates correctly. If you're updating the model on a background thread, I'd recommend scheduling your view updates asynchronously immediately after that model update. Use DispatchQueue.main.async {} This scheduling is serialized by the main queue and guarantees that scheduled updates will run in order.

Regarding your other updates, it sounds like perhaps your views are not being initialized completely. Hard to tell without the complete context. You can always run something like let _ = self.view to force a view to load.


It also sounds like you might not have set up your NSOutlineViews correctly if updates for only one view are crashing. Do you have each NSOutlineView set up with its own NSOutlineViewDataSource (and NSOutlineViewDelegate if needed)? Trying to share a dataSource could potentially lead to problems like this.
 
  • Like
Reactions: 0002378
There's potentially a risk of some kind of race condition if you don't time your updates correctly. If you're updating the model on a background thread, I'd recommend scheduling your view updates asynchronously immediately after that model update. Use DispatchQueue.main.async {} This scheduling is serialized by the main queue and guarantees that scheduled updates will run in order.

Regarding your other updates, it sounds like perhaps your views are not being initialized completely. Hard to tell without the complete context. You can always run something like let _ = self.view to force a view to load.

It also sounds like you might not have set up your NSOutlineViews correctly if updates for only one view are crashing. Do you have each NSOutlineView set up with its own NSOutlineViewDataSource (and NSOutlineViewDelegate if needed)? Trying to share a dataSource could potentially lead to problems like this.

Thanks again ... really !

Yes, I am performing my view updates exactly as you suggested ... scheduling async tasks on the main queue, immediately after adding each item. So, I'm guessing I likely need to do the same thing when removing each item. That could help me debug my current problem with removeItems (new problem I didn't mention here before).

That tip you gave me about _ = self.view will be priceless if it works for me (I haven't tried it out yet). That is much better than the hack I mentioned above.

Just to clarify, only the views that are not (yet) displayed run into this problem. So, it is not necessarily one particular view. It is any view that has not yet been displayed. So, your self.view tip should fix it by forcing it (all views) to load, whether or not they are shown to the user.

Yes, I have a unique data source object for each NSOutlineView. However, I am reusing column names/IDs across all of them, because they are all really displaying similar information in slightly different formats. Is reusing column names a bad practice and a potential problem ?

Sorry for asking so many questions. I generally find Apple's docs to be lacking, esp. for the MacOS platform.

---------------------------------------------

BTW, and I first want to mention ... no pressure here ... only if you find the time/motivation ... all my source code is open. The relevant class is:

https://github.com/maculateConcepti...r/Aural/GroupingPlaylistViewControllers.swift (view controller for my 3 NSOutlineView's)

And the relevant methods are trackAdded() and tracksRemoved().

NOTE - I am smack in the middle of implementing a huge new feature, so part of my codebase is a colossal mess ... improperly named functions, not much commenting, hugely inefficient or redundant code, dead code, etc. Just keep that in mind, if you decide to explore beyond the surface :)
 
Register on MacRumors! This sidebar will go away, and you'll see fewer ads.