Hello I'm trying to use an NSTableView in my program and I'm having a problem setting the values for the NSTableCellView and getting them to display in the NSTableView. When I run my program, only blank cells show up. Using NSLog's, I can see that the cell imageView gets set, but doesn't display. When I go to set stringValues for the NSTableCellViews however, I only get null from my NSLog's despite the string containing data. Here's the delegate method I'm having a problem with:
-(NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
NSString *cellIdentifier;
NSImageView *pageImageView;
NSString *pageString;
int pageVotes;
if (_connectionArray.count == 0) {
return nil;
}
NSTableCellView *cellView = [[NSTableCellView alloc] init];
if (tableColumn == tableView.tableColumns[0]) {
cellIdentifier = #"firstColumn";
pageImageView = [[_connectionArray objectAtIndex:row] getImage]; //Gets imageView from Page Object
cellView.imageView = pageImageView; //Set image view for cell
NSLog(#"%#", cellView.imageView); //This works
}
if (tableColumn == tableView.tableColumns[1]) {
cellIdentifier = #"secondColumn";
pageString = [[_connectionArray objectAtIndex:row] getTitle];
cellView.textField.stringValue = pageString; //Set text for cell
NSLog(#"%#", cellView.textField.stringValue); //Does not work, returns null
}
if (tableColumn == tableView.tableColumns[2]) {
cellIdentifier = #"thirdColumn";
pageVotes = [[_connectionArray objectAtIndex:row] getVotes];
pageString = [NSString stringWithFormat:#"%i", pageVotes]; //Convert int to string
cellView.textField.stringValue = pageString; //Set text for cell.
NSLog(#"%#", cellView.textField.stringValue); //Does not work, returns null
}
[_tableView makeViewWithIdentifier:cellIdentifier owner:self];
return cellView;
}
I think everything set-up correctly between the Storyboard and the ViewController as well, but I could very well be wrong since this is my first time working with NSTableViews. I've also tried using:
[cellView setImage:pageImageView];
[cellView setTextField:[NSTextField textFieldWithString:pageString]];
but I run into the same issue. If anyone can help I greatly appreciate it! I feel like I'm missing something simple...
Setting the textField and imageView properties of NSTableCellView does not add a text field or an image view to the cell view. Those outlets are just intended to inform the cell view about which of its subviews are the primary text field and/or primary image view. You are still responsible for adding those views to the cell view as subviews or, possibly, as deeper descendant views.
Also, it's a bad idea for your model to vend views. That's not how it should work. Among other things, that will specifically interfere with adding those views to the cell view's subview hierarchy.
It's also strange that you're both creating the cell view and asking the table view to make it (by calling -makeViewWithIdentifier:owner:). Normally, you'd do one or the other, or first try -makeViewWithIdentifier:owner: and only create a view if that fails. And, of course, you wouldn't ignore the return value.
Frankly, the best thing to do is set this all up in Interface Builder. If you do it right, there's no need to implement -tableView:viewForTableColumn:row: at all. Is there a reason you didn't go that route?
Related
I have a simple view based NSTableView with one column which is populated with Core Data entities, and the row's view contains just a NSTextField. I need ONLY the first row to be non editable, and to be displayed in red. I tried to to this somewhere in applicationDidFinishLaunching :
NSView *myView = [myPlaylistsTableView viewAtColumn:0 row:0 makeIfNecessary:NO];
NSArray *mySubviews = [myView subviews];
NSTextField *myTextField = [mySubviews firstObject];
[myTextField setEditable:NO];
NSDictionary *myDictionary = [NSDictionary dictionaryWithObjectsAndKeys:[NSColor redColor],NSForegroundColorAttributeName, nil];
NSAttributedString *myRedAttributedString = [[NSAttributedString alloc] initWithString:#"All Songs" attributes:myDictionary];
[myTextField setAttributedStringValue:myRedAttributedString];
But as soon as a new element is added in the table, or a drag in performed, the first row gets editable again. I have tried to write a value transformer, binding the NSTextField editable binding to the array controller selectionIndex, and return NO if selectionIndex is 0, yes in all other cases. This does not work, with various configurations of the value binding's conditionally set editable checkbox. I guess also a subclass of NSTableView should do the trick but I am a bit lost on this side.
Any help is as always greatly appreciated. Thanks.
As suggested by #Joshua Nozzi, I am implementing this delegate method for the table view :
-(NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
NSView *myView = [tableView makeViewWithIdentifier:[tableColumn identifier] owner:self];
if (tableView == myPlaylistsTableView)
{
if (row == 0)
{
NSArray *mySubviews = [myView subviews];
NSTextField *myTextField = [mySubviews firstObject];
[myTextField setEditable:NO];
NSDictionary *myDictionary = [NSDictionary dictionaryWithObjectsAndKeys:[NSColor redColor],NSForegroundColorAttributeName, nil];
NSAttributedString *myBoldAllSongsString = [[NSAttributedString alloc] initWithString:#"All Songs" attributes:myDictionary];
[myTextField setAttributedStringValue:myBoldAllSongsString];
}
}
return myView;
}
But I must be missing something because the method is called for the correct NSTableView and row = 0, code gets executed but I still don't have a red string and the textfield is editable.
Thanks again for any help.
You're trying to target a cell view and set it "forever." NSTableView doesn't quite work that way. Each time a row or the entire table is reloaded the views of the affected row(s) are "refreshed".
The appropriate place to customize them is in the NSTableViewDelegate protocol method -tableView:viewForTableColumn:row: (remember to set the controller in which you implement this method as the table view's delegate).
Update Based On OP's Update
Some points:
It's a little weird to use the table column's identifier as your cell view's identifier since the two could be different. Especially if you have more than one possible cell type in the same column (a common scenario). Best to get into the practice of using unique identifiers for each cell view and using the column identifier only to identify the column if you have more than one.
You make your view outside your if() conditional that checks which table is asking. If you have only one table, don't bother checking; if you might have more than one that is using that controller, make sure everything (including -makeViewWithIdentifier:) pertaining to each table is inside its related conditional. Same for returning myView - that should move into your conditional and if no table matches, your last return should be nil for best practice.
You appear to configure your cell view only if row == 0 but you fail to account for a reused view that's already been configured as a row 0 cell but is being reused for a row >0. You may see "interesting" behavior for all rows >0 in a large table that requires much scrolling.
Are you sure the first subview of your cell view is your NSTextField? Do you see any messages in the debugger console (like "...does not respond to selector...")? Or, if you set a breakpoint on your [myTextField setEditable:NO] line, is myTextField nil when the debugger pauses there? Best practice here is to make your own view subclass with IBOutlets to any of its subviews you want to interact with, then update them that way. Relying on its list of subviews is a recipe for confusion and future bugs as you edit your cell view, even if it was working at first.
Related to 4, if all you need is a text field as your cell, why not use the stock NSTableCellView class (which has its own -textField outlet) so you can tell it myView.textField.attributedStringValue = someAttributedString?
NSTextField has individual methods for setting the font and color of its string value. Why not take advantage of those instead of creating and setting an attributed string?
Adding: Forgot you mentioned bindings. Even if you're properly obtaining a reference to your text field, setting its properties here won't help you since the bindings are probably overriding them. Easier to ditch bindings and implement NSTableDataSource protocol alongside NSTableViewDelegate and handle refreshing the table view yourself when your data changes. You'll save yourself a LOT of headaches and confusion regarding the timing of UI updates.
A rewritten example based on the above:
-(NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
// Is it the right table view?
if (tableView == myPlaylistsTableView)
{
// Determine properties based on row
BOOL firstRow = (row == 0);
NSFont * font = (firstRow) ? [NSFont systemFontOfSize:[NSFont systemFontSize]] : [NSFont boldSystemFontOfSize:[NSFont systemFontSize]];
NSString * text = (firstRow) ? #"All Songs" : [self.songsList[ row ] songTitle]; // or whatever other row titles will be
NSColor * color = (firstRow) ? [NSColor redColor] : [NSColor labelColor];
// Make an configure a cell view
NSTableCellView * myView = [tableView makeViewWithIdentifier:#"TextCell" owner:nil];
NSTextField * textField = myView.textField;
textField.editable = !firstRow;
textField.stringValue = text;
textField.font = font;
textField.textColor = color;
return myView;
}
return nil;
}
I have a NSTableView, its data source and delegate have been set. And I want to customize the cell, so I dragged a view-based cell view from the library. Then I created a class ServiceCell which inherits from NSTableCellView in order to fully control my special cell. After that, I control-drag from the nib file to the cell class to create the IBOutlet properties of the image and text field in the cell.
In the NSTableView's delegate methods, I wrote this:
- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
// Get a new ViewCell
ServiceCell *cellView = [tableView makeViewWithIdentifier:#"ServiceCell" owner:self];
NSLog(#"Field = %#", cellView.textField); //which turns out to be null!!!
if( [tableColumn.identifier isEqualToString:#"ServiceColumn"] )
{
cellView.serviceImage.image = nil;
cellView.nameLabel.stringValue = #"Hello";
return cellView;
}
return cellView;
}
As you can see, the text field is null! But makeViewWithIdentifier: has found the cell in Interface Builder and displayed the cell in the app window. I just cannot set it's value, Why?
The problem is you are accessing your textfield but not accessing its textvalue. Try your log statement like this below:-
NSLog(#"Field = %#", cellView.textField.stringValue);
in Table view select cell and give identifier name (same as you are using in code, for your snipped it will be #"ServiceCell"), your code part is right it will work.
I'm developing an application in iPad 6.0 using Storyboards.
Let me first explain my goal. I'm trying to achieve a Master-Detail (SplitViewController-like) View Controller using 2 UITableViewControllers.
The first UITableView("Master"), let's call this HeaderTableView, as the name implies, lists down the Headers for the...
...Second UITableView("Detail"), let's call this the EncodingTableView, which contains a programmatically changing CustomTableViewCell (subviews contained within each cell may be a UITextField, UIButton or UISwitch).
See EncodingTableView.m
- (void)updateEncodingFields:(NSArray *)uiViewList
{
// Add logic for determining the kind of UIView to display in self.tableView
// Finally, notify that a change in data has been made (not working)
[self.tableView reloadData];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *encodingFieldsTableId = #"encodingFieldsTableId";
CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:encodingFieldsTableId];
if (cell == nil) {
cell = [[CustomTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:encodingFieldsTableId];
}
// Change text in textView property of CustomTableViewCell
cell.encodingFieldTitle.text = uiViewList.title;
// added methods for determining what are to be added to [cell.contentView addSubView:]
// data used here is from the array in updateEncodingFields:
}
My HeaderTableView.m, contains the didSelectRowAtIndexPath to update the EncodingTableView
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (![selectedIndexPath isEqual:indexPath]) {
selectedIndexPath = indexPath;
[self updateDataFieldTableViewForIndexPath:indexPath];
}
}
- (void)updateDataFieldTableViewForIndexPath:(NSIndexPath *)indexPath {
[self.encodingTableView updateEncodingFields:self.uiViewList];
}
Question
- Data is all ok but why doesn't EncodingTableView "redraw"ing the fields? My
suspicion is that reusing cells has something to do with this but I just can't figure out why.
Screenshots on the result:
Initial Selection in HeaderTableView
Second Selection in HeaderTableView
What I've tried :
I kept seeing suggestions such as [UITableView setNeedsDisplay],
[UITableView reloadData] and [UITableView setNeedsLayout] but none of
them worked.
Removing the reuse of tableViewCells works fine but this causes parts of my
CustomTableView.encodingFieldTitle to disappear. Not to mention that this might cause performance issues if I were to drop reusing cells.
Restrictions:
I know that a good idea is to use a SplitViewController but this is just a subpart of my app (hence not the RootViewController).
Finally, thanks for reading such a long post. ;)
It looks like you are most likely adding subviews inside tableView:cellForRowAtIndexPath:.
The issue is that if you use cell reuse then are not always starting from a blank slate inside tableView:cellForRowAtIndexPath: instead you can possibly be given a cell back that has already been configured once. This is what you are seeing, a cell that has previously had labels added to it is handed back to you and then you add some more labels over the top.
There are a few way to deal with this:
(My preferred option) Create a subview of UITableViewCell with these extra sub views available as properties.
Ensure the cell setup is only done once
A great place to do this is when you actually create a cell when one does not already exist e.g. inside the if (cell) check
if (cell == nil) {
cell = [[CustomTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:encodingFieldsTableId];
// add subview's here and give them some way to be referenced later
// one way of doing it is with the tag property (YUK)
UILabel *subView = [[UILabel alloc] initWithframe:someFrame];
subView.tag = 1;
[cell.contentView addSubview:subView];
}
UILabel *label = (id)[cell.contentView viewWithTag:1];
label.text = #"some value";
One problem i can see in your code is that the cell identifiers used are different in tableView cellForRowAtIndxPath function.
While dequeueing you are using this identifier - > "encodingFieldsTableId"
&
while creating a cell you are using this identifier - > "dataFieldUiGroupTableId".
Ideally these two identifiers should be same !!!
Try adding,
cell.encodingFieldTitle.text = nil;
Before if(cell == nil)
So that whenever your cellForRowAtIndexPath method is called, the string already present in the cell you are going to reuse will get deleted and the new text in uiViewList.title will be displayed.
I got a really strange problem.
My tableView has all the delegate and datasource set up.
Everything is fine.
However, clicking the rows do not activate:
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
I used custom cells.
After I click and click and click and click and click, sometimes it goes through.
I wonder what can possibly cause that? It's as if the customCell is "absorbing" the touch event or something or what?
Could that be why? If so, if we want to implement customCell and we want the tableView to handle the touch up event, what should we do?
Additional symptom:
If I remove user interaction enabled from the custom cell then the problem is solved with a catch.
However, clicking the button will somehow erase all the label texts in the customCell.
The implementation of the custom Cell is the following:
- (BGUIBusinessCellForDisplay *) initWithBiz: (Business *) biz
{
if (self.biz == nil) //First time set up
{
self = [super init]; //If use dequeueReusableCellWithIdentifier then I shouldn't change the address self points to right
NSString * className = NSStringFromClass([self class]);
//PO (className);
[[NSBundle mainBundle] loadNibNamed:className owner:self options:nil];
self.frame =self.view.frame;
[self addSubview:self.view]; //What is this for? self.view is of type BGCRBusinessForDisplay2. That view should be self, not one of it's subview Things don't work without it though
}
if (biz==nil)
{
return self;
}
_biz = biz;
self.prominentLabel.text = [NSString stringWithFormat:#"Isi: %#", biz.isiString];
self.Title.text = biz.Title; //Let's set this one thing first
self.Address.text=biz.ShortenedAddress;
//if([self.distance isNotEmpty]){
self.DistanceLabel.text=[NSString stringWithFormat:#"%dm",[biz.Distance intValue]];
self.PinNumber.text =biz.StringPinLineAndNumber;
NSString * URLString=nil;
if(biz.Images.allObjects.count!=0){
//self.boolImage=[NSNumber numberWithBool:true];
Image * image=(biz.Images.allObjects)[0];
URLString = image.URL;
URLString = [NSString stringWithFormat:#"http://54.251.34.144/thumbnailer/Thumbnailer.ashx?imageURL=%#",URLString.UTF8Encode];
//url=[NSURL URLWithString:image.URL];
}else{
float latitude = biz.getCllLocation.coordinate.latitude;
float longitude = biz.getCllLocation.coordinate.longitude;
URLString = [NSString stringWithFormat:#"http://maps.googleapis.com/maps/api/staticmap?&zoom=16&size=160x160&maptype=roadmap&sensor=true¢er=%f,%f&markers=size:small|color:blue|%f,%f",latitude,longitude,latitude,longitude];
URLString = URLString.UTF8Encode;
}
//Should add code and add loading indicator here
[BGHPTools doBackground:^{
UIImage * imageBiz = [BGMDImageCacherAndDownloader getImageFromURL:URLString];
[BGHPTools doForeGround:^{
self.Image.image=imageBiz;
[self.Image makeRound];
}];
}];
//self.view=self.view;
/*if (self.tableViewCell == Nil)//Instantiate that tableviewCell
{
PO(self.tableViewCell);
}
self.tableViewCell.business = bis;
self.pinLbl.text = bis.StringPinLineAndNumber;
self.lblTitle.text=bis.Title;
//self.pinLbl.text=bis.pinNumber;*/
//}
/*self.name=[dict objectForKey:#"Title"];
self.address=[dict objectForKey:#"Street"];
CLLocation * cll=[[CLLocation alloc]initWithLatitude:[[dict objectForKey:#"Latitude"] doubleValue] longitude:[[dict objectForKey:#"Longitude"] doubleValue]];
self.distance=[NSNumber numberWithDouble:[cll distanceFromLocation:[cachedProperties currentLocation]]];*/
return self;
Update: I already figure out why the texts are gone. Turns out my background is white. When a row got selected, the text suddenly turn into white. So by setting selected style to blue I sort of get that "fixed".
However, I still do not see where in my code I specify that all label texts should be white if the underlying tableViewCell is selected.
After all, what's selected is the cell, not the label. How the hell the label knows that it has to turn white is beyond me.
If you are using a Storyboard to handle the interface, instead of using:
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
Try using
#pragma mark --- Push selectedObject to the detailView ---
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
RRAppDelegate *myAppDelegate = [[UIApplication sharedApplication]delegate];
if ([[segue identifier] isEqualToString:#"PushObjectSegue"]) {
NSIndexPath *selectedRowIndex = [self.tableView indexPathForSelectedRow];
RRObjectViewController *detailViewController = [segue destinationViewController];
detailViewController.selectedObject = [myAppDelegate.goals objectAtIndex:selectedRowIndex.row];
}
}
I was having the same problem with the method you used and instead used this, it started working perfectly. Of course you'd have to adapt the code to your app's viewControllers and data source because I used my AppDelegate as the datasource, and I wasn't using a custom cell.
The most likely thing is that a view in your custom cell is absorbing the touch. Sometimes this is what you want, e.g. a button that does something, rather than selecting the entire cell. Assuming you don't want this, then just set those views' userInteractionEnabled property to NO.
--Additional code for custom NIB loading.
All you have to do is register the NIB in your viewDidLoad routine:
[tableView registerNib: [UINib nibWithNibName:#"yourCellNibName" bundle:nil] forCellReuseIdentifier:#"yourCellTypeID"]
and then in your cellForRowAtIndexPath just call:
newCell = [tableView dequeueReusableCellWithIdentifier #"yourCellTypeID"];
...
return newCell;
And it will load a cell from your XIB (or give you one from the previously used queue).
I just want to update that I think I have figured out what the problem is but still can't solve that quite right yet. And well the update is comprehensive so I think it should be an answer though I hope it's not the answer because some puzzle is still missing.
All the problem is interrelated.
The problem is in this line:
[self addSubview:self.view];
I basically turn that into:
Basically the my custom view cell has a view whose type is also tableViewCell. That view cover the real tableViewCell.
That's why when user interaction is enabled, that view will absorb the user's interaction.
That's also why the label "disappear". What happen is the label doesn't disappear. The label got highlighted and become white. However, what's highlighted is the tableViewCell not the opague view. The white opague self.view is still white while the tableCell itself is tinted with blue. So the label becomes white in the middle of white background and is gone.
I think I should replace [self addSubview:self.view] into self= self.view
However, that would mean changing the value of self. Yes it's in init. But it's still awkward. If anyone has the WAY to implement custom subclass of UI with XIB it'll be great because I haven't found one till now.
Awkward.
I wonder if we can draw a pointer to an XIB and specify that the outlet is self itself.
If that fail, I'll set background of self to white and background of self.view to transparent.
After tons of error and trying I did this:
self.contentView.backgroundColor = [UIColor whiteColor];
//self.view.backgroundColor = [UIColor redColor];
self.frame =self.view.frame;
/*PO(self.view.subviews);
PO(self.subviews);
PO(self.Title.superview);
PO(self.Title);
PO(self.view);
PO(self.Title.superview);
PO(self.view.contentView);*/
//Suck all the subviews from my minions
for (UIView* aSubView in self.view.contentView.subviews) {
[self.contentView addSubview: aSubView];
//[self.contentView add]
}
Basically I "move" all the subViews of my view object to my self object. There is a catch though that when subclassing tableViewCell I should move the subviews of the contentView. Who knows why.
At the end I just set self.view to nil for it's no longer needed and my program works as expected.
Also to set background of your tableViewCell, you need also to set the background of self.contentView rather than self.view.
Another approach you can try is to use story board. Alternatively you can just move the contentView of the self.view to self.
Make sure you'r implementing that method and not
deselectRowAtIndexPath:animated
UPDATE AT BOTTOM
I branched off of the MKNetworkKit Flickr demo for this. I have several images on a webserver I want to display in a table. I have a UITableViewCell subclass, ImageCell.
Here is the custom method for retrieving remote images:
-(void)setImageForCell:(NSString *)remoteFileName {
self.loadingImageURLString =
[NSString stringWithFormat:#"http://myserver.com/%#.png",remoteFileName];
self.imageLoadingOperation = [ApplicationDelegate.imageDownloader imageAtURL:[NSURL URLWithString:self.loadingImageURLString]
onCompletion:^(UIImage *fetchedImage, NSURL *url, BOOL isInCache) {
if([self.loadingImageURLString isEqualToString:[url absoluteString]]) {
if (isInCache) {
self.imageView.image = fetchedImage;
[self.contentView drawRect:self.contentView.frame];
NSLog(#"Image is in cache");
} else {
self.imageView.image = fetchedImage;
[self.contentView drawRect:self.contentView.frame];
NSLog(#"Image is not in cache");
}
}
}];
}
//TableView.h
//it is called like this
//in cellForRowAtIndexPath...
ImageCell *cell = (ImageCell *)[tableView dequeue...etc];
MyObject *obj = [dataSource objectAtIndex:indexPath.row];
cell.textLabel.text = obj.name;
[cell setImageForCell:obj.name];
return cell;
I have inspected the contents of my default cache directory, and there are now items inside. Scrolling the table constantly now prints "Image is in cache". The problem is, the cell's imageView never updates. Mugunth has a class method automaticallyNotifiesObserversForKey: but I don't ever see it implemented anywhere. I'm guessing that there's another step involved to tell the tableView instance to update that row with the new contents.
Thanks for your input.
UPDATE
I got this to work by using a custom Interface Builder xib file with a Cell and a UIImageView IBOutlet. Not sure why it wasn't working with self.imageView.image, and would be interested to find out why, exactly. I still consider this an open question because I'd like to just use the standard UITableViewCell class.
I the call to prepareForReuse. Are you setting the self.imageView.image to nil?
If yes, dont do it because it looks like it's never re-initialized.
Why do you call "drawRect" from your code. That's blasphemy!
Inspect your imageView and check if the IB connections are good.