Question

I'm initializing a simple interface, with an NSTableView bound to an array controller (which manages an array of dictionaries). I want to load the content for the array in the background (it's a very time-consuming process), updating the table view every 100 or 1000 elements. The idea is that the interface is available and responsive. I can't figure out how to also trigger an update / refresh afterwards. The table remains empty. Can anyone offer pointers?

My current approach is:

// In init for my app controller. This seems to work well, but I've tried other methods here.
[self performSelectorInBackground:@selector(loadTable) withObject:nil];


- (void)loadTable {
  tracks = [[NSMutableArray alloc] initWithCapacity:[masters count]];

  // ... create each object one-by-one. Add it to tracks.
  for (... in ...) {
    [tracks addObject:newObject];
  }

  // Now I don't know what to do next. The table remains empty. 
  // Things I've tried (though possibly not in all combinations with the
  // method above):
  // 1. With a suitably-defined reloadData method, which just reloads
  //   the table view and sets needs display.
  [self performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:YES];

  // 2. Reload directly.
  [tv reloadData];
  [tv setNeedsDisplay];
}

If I just load the data directly, and don't try to do that in the background, everything works fine, but it takes almost 30s.

Was it helpful?

Solution

You have the table columns (I assume you meant) bound to an array controller, so that's where the table view gets its data from. The table view may very well be asking for updated arrays, but it's asking the array controller, which doesn't know anything has changed.

The array controller won't simply turn around and ask you for fresh data; that would imply it exists solely to make it harder for you to bind the table view to your array, and that isn't the case. It's a controller; its job is to own (a copy of) the array and maintain its order and the user's selection of some subset of its objects.

Therefore, you need the array controller to find out when you add items to your array. The best way to make this happen is to bind the array controller's contentArray to a property of your controller, and update that property in a KVO-compliant manner.

That means:

  1. Create the mutable array in your init method. (And, of course, release it in dealloc.)
  2. Implement the array accessor methods, plus addTracksObject: and removeTracksObject: (which are technically set accessor methods, so KVO will ignore them for an array property) for your convenience.
  3. To add a track, send yourself an addTracksObject: message. You should respond to that by sending yourself an insertObject:inTracksAtIndex: message (with [self countOfTracks] for the index, unless you want to do an insort), and you should respond to insertObject:inTracksAtIndex: by sending your tracks array an insertObject:atIndex: message.

As I mentioned, KVO will ignore addFooObject: and removeFooObject: when foo is an NSArray property, considering those only NSSet-property accessors, so you need to implement them on top of insertObject:inFooAtIndex: and removeObjectFromFooAtIndex: because those are array accessors, which means KVO will react to them.

Step 3, as I just described it, will be pretty slow, because it will cause the array controller to re-fetch your property and the table view to re-fetch the array controller's arrangedObjects at least once each for every row you add.

So, you should maintain your batch-adding behavior with this alternate step 3:

  • Implement insertTracks:atIndexes:, and pass it an array of one batch of (e.g., 100 or 1000) tracks and an index set formed by [NSIndexSet indexSetWithRange:(NSRange){ [self countOfTracks], countOfBatch }]. You'll also need to implement removeTracksAtIndexes:, only because KVO will ignore each insert method if you don't also have its counterpart.

You probably should have the array controller set to attempt to preserve the selection, so as not to frustrate the user too much while you're still bringing in rows.

Also, you may want to create the objects on a background thread, periodically sending yourself another batch to add using a main-thread perform. I'm ordinarily an advocate of doing things on the main thread run loop whenever possible, but this sort of thing could easily make your interface laggy while your periodic load builds up another batch.

OTHER TIPS

You need to call setNeedsDisplay:YES on your table view on the main thread. Don't call it from a background thread. All Cocoa UI calls must be done on the main thread or weird things happen.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top