0

I am trying to setup a NSTableView with a custom cell using an ArrayController and Bindings. To accomplish this I added a subview to the custom cell. The data connection seems to work somewhat. Though, there seems to be a redraw problem which I cannot fix. When I load the application only some of the cells are rendered. When I scroll through the rows or select one the rendering changes.

I created an example project on github to illustrate what the problem is.

Screenshot

The actual source code for the cell rendering can be found here:

// CustomCell.m
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {

  if (![m_view superview]) {
    [controlView addSubview:m_view];
  }

  // The array controller only gets wrapped data items pack by the NSObjectTransformer.
  // Therefore, objectValue returns a NSObjectWrapper.
  // Unpack the wrapper to retreive the data item.
  DataItem* dataItem = [(NSObjectWrapper*)[self objectValue] original];
  [[m_view name] setStringValue:dataItem.name];
  [[m_view occupation] setStringValue:dataItem.occupation];
  [m_view setFrame:cellFrame];
}

It seems as if the parent controlView does not redraw. Can I force it somehow?

JJD
  • 50,076
  • 60
  • 203
  • 339

2 Answers2

3

This is almost certainly not a best practice way of doing this, and I'll explain why afterwards: however, it does seem to work. Replace your cell class's drawInteriorWithFrame:inView: method with the following:

- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
    DataItem* dataItem = [(NSObjectWrapper*)[self objectValue] original];
    [[m_view name] setStringValue:dataItem.name];
    [[m_view occupation] setStringValue:dataItem.occupation];
    [m_view setFrame:cellFrame];

    NSData *d = [m_view dataWithPDFInsideRect:[m_view bounds]];
    NSImage *i = [[NSImage alloc] initWithData:d];
    [i setFlipped:YES];

    [i drawInRect:cellFrame fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
}

The problem is that only one NSCell is created for the entire table. That's how cells are meant to work: the table view creates a cell, and calls setObject… followed by drawInterior… over and over again to get the cell to draw the whole table. That's great from an efficiency perspective (the NSCell class was designed back when 25mhz was a fast computer, so it aimed to minimise the number of object allocations), but causes problems here.

In your code, you populate a view with values, and set its frame, adding it as a subview of the table view if needed. However, since you've only got one instance of NSCell, there can only be one view: you took the single view that you had and merely moved it down the rows of the table.

To do this properly, you'd need some data structure to track all the views you added as subviews of your NSTableView, and when the cell is updating one in the drawInterior… method you'd need to look up which the correct one was and update that. You'd also need to allocate all these views in code (or at least move the view to a separate nib which you could load multiple copies of), because as it is you've only got one in your nib and copying a view is a pain.

The code I wrote is a kludge, since it's really inefficient. What I did was each time the view needs to draw, I drew the view into an off screen image buffer, and then drew the buffer into the correct place in the table view. In doing so, I avoided the problem of only having one view, since the code just takes and draws a new copy of its contents whenever it is needed. enter image description here

Amy Worrall
  • 16,250
  • 3
  • 42
  • 65
  • Please correct me if I am wrong: I thought, by providing only `NSObjectWrapper` objects to the table view, the table view can produce copies as needed (using `copyWithZone:`). That means it never touches the wrapped `CustomCell`. The `CustomCell` will only expose its subview when `drawInteriorWithFrame` is called. - I am not sure if I understand the problem. – JJD May 10 '11 at 16:20
  • OK, the way I checked this was downloading your project and logging the memory address of any CustomCell object (with `NSLog(@"%x", self);`). Only one of them was created. It's used like a rubber stamp to go and draw each row in turn. I think another point of confusion is that cells themselves can't have subviews: you were adding a subview to the table view itself. If you do that, you've got to keep track of all the subviews you add somewhere, perhaps in a mutable array. Because there's only one cell, you need to check the row number and instantiate a new view for each row. – Amy Worrall May 11 '11 at 08:03
  • 1
    Another method you could use for this particular example: just create an `NSAttribtuedString`, and put the two lines of text in, with a line break in between and with attributes to set the text size. Then just pass that in as the object value for a text cell and it should just work, no subclassing a cell needed. – Amy Worrall May 11 '11 at 08:09
  • The aim was to being able to layout the table cell (view) in the InterfaceBuilder, visually. That's why I have chosen the overhead you have seen in the example implementation. - Too bad, that Apple makes it so difficult for MacOS. - I think, I will give up on this and "draw" the content by hand. Thank you very much for you valuable tipps! – JJD May 11 '11 at 08:20
  • No problem. You may also like something that is coming in Lion! (BTW, did you try my code? I don't know how bad the slowdown would be, it seems fine on my machine but I do have a quad core iMac. If you tested it on some slower Macs and it was OK, then it does do what you wanted and allow you to lay things out in IB.) – Amy Worrall May 11 '11 at 08:22
  • I did for sure. Since the app will be used on a daily basis I will have to evalute the pros and cons. Cheers! – JJD May 11 '11 at 08:41
0

EDIT: See my other answer for explanation

Have you implemented copyWithZone:? You'll need to ensure you either copy or recreate your view in that method, otherwise different cells will end up sharing a view (because NSTableView copies its cells).

Amy Worrall
  • 16,250
  • 3
  • 42
  • 65
  • I implemented `copyWithZone` in [NSObjectWrapper.m](https://github.com/johnjohndoe/NSTableViewCustomCell/blob/master/NSObjectWrapper.m) which is given to the table view (as described in the code comments above). – JJD May 10 '11 at 10:35
  • I meant implementing it in CustomCell.m. NSCell uses the NSCopying protocol, and according to the documentation "If a subclass inherits NSCopying from its superclass and declares additional instance variables, the subclass has to override copyWithZone: to properly handle its own instance variables, invoking the superclass’s implementation first." – Amy Worrall May 10 '11 at 11:32
  • No, I did not. I use the value transformer [NSObjectTransformer](https://github.com/johnjohndoe/NSTableViewCustomCell/blob/master/NSObjectTransformer.m) in the InterfaceBuilder to only manage [NSObjectWrapper](https://github.com/johnjohndoe/NSTableViewCustomCell/blob/master/NSObjectWrapper.m) objects in the table view. I tried to make the [example on github](https://github.com/johnjohndoe/NSTableViewCustomCell) to be very comprehensible. It is maybe better to understand if you clone the project. – JJD May 10 '11 at 12:17