How to setup a NSTableView with a custom cell using a subview
Asked Answered
S

2

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?

Submaxillary answered 4/5, 2011 at 13:36 Comment(0)
L
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

Lightproof answered 10/5, 2011 at 13:39 Comment(6)
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.Submaxillary
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.Lightproof
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.Lightproof
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!Submaxillary
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.)Lightproof
I did for sure. Since the app will be used on a daily basis I will have to evalute the pros and cons. Cheers!Submaxillary
L
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).

Lightproof answered 10/5, 2011 at 9:2 Comment(3)
I implemented copyWithZone in NSObjectWrapper.m which is given to the table view (as described in the code comments above).Submaxillary
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."Lightproof
No, I did not. I use the value transformer NSObjectTransformer in the InterfaceBuilder to only manage NSObjectWrapper objects in the table view. I tried to make the example on github to be very comprehensible. It is maybe better to understand if you clone the project.Submaxillary

© 2022 - 2024 — McMap. All rights reserved.