Search code examples
cocoanstableviewcocoa-bindingsnstablecellview

How can I make a button inside a NSTableViewRow respond to the represented object


I've been fighting with this problem for quite some time. I'm working on a file copy manager module, so far I have been able to make everything work perfectly with the exception of the cancel button. For some reason when I click the cancel button for a specific row, the button action targets several rows simultaneously.

File Copy Manager

After a couple of days of researching the issue I was able to make the object cancel the operation represented by the row successfully using:

-(IBAction)btnCancelOperationClick:(id)sender {
    NSInteger row = [_tableView rowForView:sender];
    if (row != -1) {
        FileCopyOperation *opr = [_fileCopyOperations objectAtIndex:row];
        [opr cancel];
        [_fileCopyOperations removeObjectAtIndex:row];
        [_tableView removeRowsAtIndexes:[NSIndexSet indexSetWithIndex:row] withAnimation:NSTableViewAnimationEffectFade];
    }
}

This works nicely, I'm able to cancel the operation and update my table accordingly. Everything else works as intended but there has to be something wrong with my code or bindings. I'm loading this cell from a nib, then I register this nib using:

[_tableView registerNib:[[NSNib alloc]initWithNibNamed:@"FileCopyCell" bundle:nil] forIdentifier:@"FileCopyCell"];

I made the QueueController the File's Owner and hooked everything like this:

Bindings

I would greatly appreciate if someone could point me in the right direction to get this thing working properly. Thanks in advance!

Edit to add more code sample.

-(NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
    FileCopyCell *cell = [tableView makeViewWithIdentifier:@"FileCopyCell" owner:self];
    FileCopyOperation *opr = [_fileCopyOperations objectAtIndex:row];

    [cell.fileName setStringValue:[NSString stringWithFormat:@"Copying \"%@\"",opr.fName]];
    [cell.progressBar setDoubleValue:((opr.bWritten.doubleValue / opr.fSize.doubleValue) * 100)];
    [cell.totalBytes setStringValue:[NSString stringWithFormat:@"of %@",[NSByteCountFormatter stringFromByteCount:opr.fSize.longLongValue countStyle:NSByteCountFormatterCountStyleFile]]];
    [cell.status setStringValue:[NSString stringWithFormat:@"%@",[NSByteCountFormatter stringFromByteCount:opr.bWritten.longLongValue countStyle:NSByteCountFormatterCountStyleFile]]];
    [cell.icon setImage:[[NSWorkspace sharedWorkspace]iconForFile:opr.srcURL.path]];
    [cell.cancelButton setTarget:self];
    return cell;
}

-(void)windowDidLoad {
    [super windowDidLoad];
    _fileCopyOperations = [NSMutableArray new];
    windowFrame = [self.window frame];
    rows = 0;

    [_tableView registerNib:[[NSNib alloc]initWithNibNamed:@"FileCopyCell" bundle:nil] forIdentifier:@"FileCopyCell"];

    if (!fileCopyManager) {
        fileCopyManager = [FileCopyManager sharedFileCopyManager];
        [fileCopyManager.fileCopyQueue addObserver:self forKeyPath:@"operationCount" options:NSKeyValueObservingOptionNew context:(void*)fileCopyManager];
    }

    [_scrollView setHasHorizontalScroller:NO];
    [_scrollView setHasVerticalScroller:NO];
}

Solution

  • The best approach is to subclass NSTableCellView and let it handle its own actions and represented object. For example, a cell representing a Foo instance could have two properties: foo and fooController. When the (nonatomic) foo setter is called, the cell can update its own UI to represent the passed Foo. When the Foo controller creates the table cell, it can instantiate a FooCell instance, set itself as fooController and assign the Foo instance and let the cell handle itself. The cancel button's target can be its own cell, and when the -cancel: action is called, it can tell its fooController what to do (since the Foo controller is responsible for updating the queue and the table view) and since it has a reference to its foo, it can pass that to the controller via some -cancelFoo:(Foo *)theFoo method without relying on the controller to look up its index (which may not be accurate if you're animating rows appearing and disappearing or the user is rapidly canceling a bunch in a row but their removal is delayed and updated asynchronously).

    Nice and clean. Neat and organized containment / separation of responsibilities. The cell handles its own UI updating and actions and knows its own foo; the foo controller handles its foo collection, its table view, and the assignment of foos to foo cells.