The purpose of my app is to download subtitles files for WWDC videos. The subtitles are available as WebVTT files named “fileSequence0.webvtt”, “fileSequence1.webvtt”, etc., in a grand-subdirectory of the directory where the video and slides files are.
If you have any WWDC video (or slides) file, you can get the URL you downloaded it from, strip the filename, and add the requisite extra path components to make a subtitles URL, and then download that subtitles file—and that's what my app does.
Although I initially envisioned a two-tiered model, I eventually arrived at a single flat list, so I'm just using the outline view as a plain table view at the moment—no items have any children.
My items are Download Sources, and each one has the following properties:
bool
), henceforth referred to as “Done”When I feed a video file into my app, it creates a Download Source that yields each subtitles file's remote URL in turn. My app downloads each subtitles file, updating state of the Download Source as it goes.
My app has no way to know how many subtitles files there are; all it knows is that they are numbered. So, when it gets a 404, it knows that it has downloaded all of the subtitles files for that video file, so it marks that Download Source as Done and proceeds to the next Download Source (if there is one).
My window controller is my outline view's data source and delegate.
The outline view is view-based. Within the row view, three of the four cell views are bound to a property of their object value:
The fourth column holds an image view, and that's where the difficulty comes in.
Because I don't want to pollute my model with a “doneImage
” property (I feel that that is controller business), I have the controller setting the image view's image when it returns the cell view:
- (NSView *) outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item {
NSTableCellView *cellView = [outlineView makeViewWithIdentifier:tableColumn.identifier owner:self];
PRHVideoFileDownloadSource *downloadSource = item;
cellView.objectValue = downloadSource;
if ([tableColumn.identifier isEqualToString:@"done"]) {
NSLog(@"Source %@ is done: %@", downloadSource, downloadSource.complete ? @"true" : @"false");
cellView.imageView.image = downloadSource.complete ? [NSImage imageNamed:@"Done"] : nil;
}
return cellView;
}
Initially, this works fine. If I take out the condition that tests whether a model object is done, the Done image appears fine (so I know that the image is being copied, it's a valid icon, the column's identifier is set correctly, etc.).
But, with that condition in place, the image never appears, even after a Download Source becomes Done.
You can see that I added an NSLog. This only appears once (per row):
2013-07-22 15:46:18.028 WWDC Subtitles Fetcher[7980:1307] Source <PRHVideoFileDownloadSource 0x7fd6f533d340 "404-Advances in Objective-C.pdf"> is done: false
2013-07-22 15:46:18.035 WWDC Subtitles Fetcher[7980:1307] Source <PRHVideoFileDownloadSource 0x10b372190 "405-Interface Builder Core Concepts-SD.mov"> is done: false
The cells that are populated by binding to properties of the cell view's objectValue
all work fine. Only this cell, whose content view is not bound, does not update.
As you see above, I've tried logging when outlineView:viewForTableColumn:item:
is called. It is called exactly once for each row; never again.
So, maybe I need to cue it to reload those rows, right? I added a reloadItem:
message immediately after the line that marks a Download source as done. No dice—it is definitely reached (I had another bug, since fixed, that caused an exception when I did that), but the outline view does not take the hint and ask me to recreate/update this view.
I also tried binding the image view to the cell view's objectValue
and implementing outlineView:objectValueForTableColumn:byItem:
:
//Revised outlineView:viewForTableColumn:item: that doesn't set the cell view's objectValue
- (NSView *) outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item {
NSTableCellView *cellView = [outlineView makeViewWithIdentifier:tableColumn.identifier owner:self];
return cellView;
}
//Return the Done image for the “done” column; return the Download Source for all other columns (whose cells' content views are bound to properties of the Download Source)
- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item {
PRHVideoFileDownloadSource *downloadSource = item;
if ([tableColumn.identifier isEqualToString:@"done"]) {
NSLog(@"Source %@ is done: %@", downloadSource, downloadSource.complete ? @"true" : @"false");
return downloadSource.complete ? [NSImage imageNamed:@"Done"] : nil;
}
return downloadSource;
}
Although this is cleaner, to the point that I think I'll keep it, it does not solve the problem—the objectValueForTableColumn:byItem:
method is likewise only ever called once, despite reloadItem:
, and consequently does not get a chance to dispense the image as that cell's new objectValue
.
I need a way (that works) to tell the outline view “this particular row has changed; please update its objectValue
s”—or, even better, a way to tell it that a particular cell in that row has changed.
Rather than returning the checkmark image for the "done" column and the download source for the other columns, return the download source for all columns. Then bind the image view's "hidden" binding to the download source's "complete" property with an NSNegateBoolean value transformer. This will allow the image to show or hide itself when your model fires KVO notifications, so you don't have to refresh the item and hope that will update things the way you want.