Is calling cellForRowAtIndexPath: ever practical? - ios

I've seen many many developers when implementing a UITableViewDelegate and UITableViewDataSource directly call cellForRowAtIndexPath: to either:
1) Retrieve the cell to grab a model element that they stored within
the cell:
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
MyCell *cell = (MyCell *)[tableView cellForRowAtIndexPath:indexPath];
int secretCode = cell.secretCode;
LaunchMissileViewController *vc = [[LaunchMissileViewController alloc] initWithSecretCode:secretCode];
[self.navigationController pushViewController:vc];
}
2) Attempt to style the cell (this has clear problems, but seems very common):
MyCell *cell = (MyCell *)[self cellForRowAtIndexPath:indexPath];
// or [tableView cellForRowAtIndexPat:indexPath];
cell.backgroundColor = [UIColor greenColor];
Is it safe to make the blanket statement that "only the framework should ever call cellForRowAtIndexPath:"? Or is there a practical reason one might ever call it themselves?

Personally I don't think there are ever good cases to directly call tableView:cellForRowAtIndexPath: directly on the data source.
For case #1 it is simply bad coding. The cell should never contain the data. The data source should have the data so get the data from the actual data source, not the cell.
For case #2 you would only ever need to update a visible cell. And for this you can use the UITableView cellForRowAtIndexPath: method. No need to call the data source for the cell.
I try never to say "never" but it should be an extremely rare case where you have a real need to get the cell by calling the data source method.

I will say rmaddy is correct, though I have one case where it is arguably practical to use:
If you ever require a copy of a UITableViewCell to apply to another view.
(derived from https://github.com/Ice3SteveFortune/i3-dragndrop)
UIView* cellCopy;
UITableViewCell* cell = [self.tableView cellForRowAtIndexPath:indexPath];
NSData* viewCopyData = [NSKeyedArchiver archivedDataWithRootObject:cell];
cellCopy = [NSKeyedUnarchiver unarchiveObjectWithData:viewCopyData];
// Maybe to drag-and-drop outside of the UITableViewCell.
[self.otherView addSubview:cellCopy];
I want to leave this here as an example of one of the few cases where it is remotely practical to call cellForRowAtIndexPath:

Retrieving data from the cell makes no sense as the data being inserted into the cell would be known to the developer. The developer can straight away get the data. Another problem, (big one) with this approach is that if that cell is not visible, it would be first generated and then the data retrieved.

Related

How tableView:cellForRowAtIndexPath: is supposed to be used?

The master-detail project created by XCode wizard contains implementation of tableView:cellForRowAtIndexPath: that calls dequeueReusableCellWithIdentifier:forIndexPath:, then calls own custom method configureCell to fill the cell controls with valid data, and then returns the cell to caller. As I understand, this is how table knows how to draw its cells content.
What I don't understand is how to use it when I want to get cell from my code. And is it even supposed to be called by my code, or is it only a callback used by table framework itself?
For example, if I just want to change text of one label in cell, I thought I can just call tableView:cellForRowAtIndexPath:, and change what I need in the resulting cell object controls. But calling tableView:cellForRowAtIndexPath: actually makes a new cell object (probably reusing one from pool of unused cell objects) and fill ALL controls like imageviews and labels in it with data. This does not look good to me regarding to performance, when I want change just one label.
So maybe I could remove configureCell out of tableView:cellForRowAtIndexPath:? But then how to ensure all cell contents will be redrawn by system when [table reloadData] is called?
P.S.
XCode 7 wizard created this kind of code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
NSManagedObject *object = [[self fetchedResultsController] objectAtIndexPath:indexPath];
[self configureCell:cell withObject:object];
return cell;
}
// Then later in controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] withObject:anObject];
break;
configureCell is called twice here. Seems like non-sense to me. I hoped at least people who write code for wizards understand how this is supposed to be working.
There's two different methods being called here that have similar names.
tableView:cellForRowAtIndexPath: is a UITableViewDataSource method that you implement to create the cell for the table to show. The tableview calls this method when a new cell is going to come on screen.
controller:didChange:... is calling a different method called cellForRowAtIndexPath: which is a UITableView method. This method queries the table for a cell that is already being displayed after previously being created using the tableView:cellForRowAtIndexPath: datasource method and doesn't result in it being called again. It's calling this method to access the already displayed cell and update it with new info when the object changes.
Both calls to configureCell are necessary.

Configuring custom UITableViewCell

In my project I'm creating custom cells by subclassing UITableViewCell. When cellForRowAtIndexPath: is fired I do a pretty basic stuff like:
MyCustomCell *cell = [self.tableView dequeueReusableCellWithIdentifier:[MyCustomCell identifier]];
I don't want to manually configure cell properties in cellForRowAtIndexPath: so I thought I'd create a method inside MyCustomCell called configureWithModel: which is filling MyCustomCell with proper data. So far, so good! Now inside cellForRowAtIndexPath: I have something like:
MyCustomCell *cell = [self.tableView dequeueReusableCellWithIdentifier:[MyCustomCell identifier]];
[cell configureWithModel:model];
In configureWithModel: I assign some data (image also) to cell so as you'd guess it could be slow'n'heavy so I wonder if this is a good solution to have a method like this in subclass of MyCustomCell? What is more, how it's related to prepareForReuse?
Doing this [cell configureWithModel:model]; is the best approach because take for a case when you want to use configureWithModel: in more than 2 tableViews you can avoid code redundancy and cell level control would be there with cell itself.
Use of [cell configureWithModel:model]; will make your code look like more structured, but for image use the following delegate
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
Example :
- (void)tableView:(UITableView *)tableView willDisplayCell:(AlbumCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
AlbumBO *album = [self.arrAlbums objectAtIndex:indexPath.row];
dispatch_async(imageQueue_, ^{
UIImage *image = [self retrieveImageWithImageUrl:album.coverPhoto];
dispatch_async(dispatch_get_main_queue(), ^{
[cell.imgVwAlbum setImage:image];
});
});
}
Here
AlbumCell is my Custom table cell
AlbumBO is the object for containing image object
And
[self retrieveImageWithImageUrl:album.coverPhoto]
is the user defined method to download image.
This sounds like a fairly decent usage of the singular responsibility principle. Where this might bite you is if your cells need to be binded with images that must be downloaded from a server. In this instance you don't want your cell responsible for triggering a download since the cell will then also be responsible for monitoring the progress of the download. Since these cells are reusable this becomes more problematic as the cell becomes reused.
So yes, in a simple case where you need to bind data to a cell it makes sense for the cell to be responsible for configuring its subviews with the relevant data.
Regarding prepareForReuse a casual glance at the documentation details
Discussion If a UITableViewCell object is reusable—that is, it has a
reuse identifier—this method is invoked just before the object is
returned from the UITableView method
dequeueReusableCellWithIdentifier:. For performance reasons, you
should only reset attributes of the cell that are not related to
content, for example, alpha, editing, and selection state. The table
view's delegate in tableView:cellForRowAtIndexPath: should always
reset all content when reusing a cell. If the cell object does not
have an associated reuse identifier, this method is not called. If you
override this method, you must be sure to invoke the superclass
implementation.

no visible #interface for "UITableView" declares the selector "cellForRowAtIndexPath:"

I am new in IOS6 dev. I got a problem with UITableView. My code below is to display Check Mark at the end of the row selected. But I received an error like "no visible #interface for UITableView declares the selector cellForRowAtIndexPath:". UITableView has cellForRowAtIndexPath method, but tableView cannot
show it. I don't know why. Please help.
Below is the code:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; -----error line
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}
The problem is "tableView" cannot recognize all the methods under UITableView. Some it knows such as "numberOfRowsInSection". I cannot figure out why.
The TableView itself does not implement this selector.
The full selector for this method is
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
and is from the protocol for the delegate. Your delegate (e.g. the viewController) has to implement this method. It is not recommended (and not easily possible) to get the cell object from the table.
Instead, change the underlying data and redraw your table with
[tableView reloadData];
Your problem is not in the code sample you include. Your problem rests elsewhere. We can't diagnose the problem on the basis of this one snippet. You'll have to share a more complete code sample with us.
Unrelated to your question, there is a subtle issue in your didSelectRowAtIndexPath. You should not just be updating the cellAccessoryType here. You really should be updating your model that backs your UI. This would be critical if the table had more rows than were visible at any given moment in time.
To illustrate the idea, let's assume your model was an array of objects with two properties, the title of the cell and whether the cell was selected or not.
Thus, your cellForRowAtIndexPath might look like:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
RowData *rowObject = self.objects[indexPath.row];
cell.textLabel.text = rowObject.title;
if (rowObject.isSelected)
cell.accessoryType = UITableViewCellAccessoryCheckmark;
else
cell.accessoryType = UITableViewCellAccessoryNone;
return cell;
}
And your didSelectRowAtIndexPath might look like:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
RowData *rowObject = self.objects[indexPath.row];
rowObject.selected = !rowObject.isSelected;
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
Again, your compiler warning/error is undoubtedly stems from some other problem in your source code, as your original code snippet is syntactically correct. I'm just trying to correct a different flaw in your didSelectRowAtIndexPath. In MVC programming, you really want to make sure you're updating your model (and then updating your view), not just updating the view.
But, to be clear, if you don't correct the error that is causing your current compiler warning/error, you'll probably just get another warning regardless of what you put into didSelectRowAtIndexPath. You have to identify why the compiler is balking at your current code.

UIView & UITableViewCell not being deallocated when UIView becomes the delegate of a UITableViewCell

I have a view controller (EmbeddedMenuView) that uses a custom view (HorizontalMenuView). The Embedded menu view uses multible HorizontalMenuViews. The HorizontalMenuView contains a UITableView. Each cell in the table view uses quite a bit of memory (high quality images.).
Now, I need to execute a task every time a section of the table view cells in the HorizontalMenuView is touched. I did this by creating a protocol in the table view cell and assigning the HorizontalMenuView its delegate. Then I created a protocol in the HorizontalMenuView and assigned the EmbeddedMenuView its delegate. So I pass the touch event up to the EmbeddedMenuView.
The problem is, when I assign the cell's delegate, the HorizontalMenuView does not get deallocated. Since this view refreshes itself every time the view appears, the memory footprint gets out of control fast.
If I comment out the part where the cell is assigned a delegate, everything works fine.
My question is: How can I properly release a UITableViewCell's delegate?
This is the code snippet from the HorizontalMenuView:
-(UITableViewCell*) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
//Custom Logic
HorizontalMenuItemTableViewCell *cell = (HorizontalMenuItemTableViewCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (cell == nil) {
cell = [[[NSClassFromString([[AMPUIManager sharedManager] classNameForName:cellIdentifier]) alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier] autorelease];
cell.shouldAlwaysTransform = shouldAlwaysTransform;
cell.selectionStyle = UITableViewCellSelectionStyleNone;
cell.colorsDict = colorsDict;
if ([cell isKindOfClass:[ATCustomTableViewCell class]]) {
((ATCustomTableViewCell *)cell).delegate = self; //Commenting this out solves my problem.
}
}
//More Custom Logic
return cell;
}
PS I am using manual reference counting. ARC is not an option for this project.
It sounds like you may have a circular reference. You almost always want to use 'assign' convention with delegates.
See: Why are Objective-C delegates usually given the property assign instead of retain?

Copying UITableViewCell

I'm reading a custom table cell in tableView:cellForRowAtIndexPath: from a nib file. This works great for my purposes, except it's quite slow.
Now, I know the right thing to do in the long term is to create the cell entirely in code, and to use a single view, and so on. But this is a prototype, and I don't want to put that much effort into it.
For now, I'd be happy if I was reading the nib only once in the UIViewController subclass, then tableView:cellForRowAtIndexPath: made copies of it. My assumption here is that copying would be faster than reading the nib.
Here's what I use to load the nib, which I call from viewDidLoad: (and retain after)
-(id)loadFromNamed:(NSString*)name {
NSArray *objectsInNib = [[NSBundle mainBundle] loadNibNamed:name
owner:self
options:nil];
assert( objectsInNib.count == 1 );
return [objectsInNib objectAtIndex:0];
}
All is good so far. But the question is: How do I copy this over and over? Is it even possible?
I tried [_cachedObject copy] and [_cachedObject mutableCopy] but UITableViewCell doesn't support either copy protocol.
If I have to, I can just tell them to ignore the speed until I'm prepared to remove the nib entirely, but I'd rather get it going a little faster if there's a low-hanging fruit here.
Any ideas?
I think coping of table cell can be used together with dequeuing mechanism, which will allow to create cell one time (from nib or programmatically or getting it loaded automatically from other nib and linking as an outlet in IB) and then clone it or dequeue it when needed.
UITableViewCell doesn't conform to NSCopying protocol, but it supports keyed archiving/unarchiving mechanism, so it can be used for cloning.
Based on answer "
How to duplicate a UIButton in Objective C? " my data source delegate method looks like:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellID = #"CellIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellID];
if (!cell) {
NSData *archivedData = [NSKeyedArchiver archivedDataWithRootObject:self.tableViewCell];
cell = [NSKeyedUnarchiver unarchiveObjectWithData:archivedData];
}
// ... config ...
return cell;
}
And in my case self.tableViewCell is a cell that was loaded one time from view's nib file.
I don't tested what will be faster: "archive + unarchive" to clone or "load nib file + unarchive" which framework will do in case of -loadNibNamed:owner:options:, I used this method only with convenience considerations, but good chances that memory operation vs file operation will be faster.
EDIT: It appears not as easy as it seemed at first. As UIImage doesn't conforms to NSCoding, cells with configured UIImageViews can't be just copied without additional code. Yep, copying whole image is definitely not a good practice, cheers to Apple for pointing this.
Use the cell cloning built into the table view. Apple knew generating a lot of table cells was slow. Check out the docs for this method:
- (UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier
You create the cell once, then as new cells are requested, use that method to clone the existing cells. Then you just change what needs to be changed about the new cell and return the cell object.
Also check out the table view realted sample code provided by Apple that uses this method and show you the right way. The fact your cell was loaded from a nib shouldn't matter at all.
Minor clarification: I dont think the above method clone cells for you. Instead it takes cell object that have scrolled off the screen and simply moves them to a new spot. So it's literally reusing a cell. So be sure your custom table view can be set to all the new values it needs outside of the intialization.
Not proud of this solution, but it works with the maximum number of possible IB bindings:
Interface (AlbumTableViewCell is a subclass of UITableViewCell of which an instance is defined in AlbumViewController's XIB file):
#interface AlbumsViewController : UITableViewController {
IBOutlet AlbumTableViewCell *tableViewCellTrack;
}
#property (nonatomic, retain) AlbumTableViewCell *tableViewCellTrack;
Implementation (unarchive / archive makes a copy / clones the table view cell):
#implementation AlbumsViewController
#synthesize tableViewCellTrack;
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
AlbumTableViewCell *cell = (AlbumTableViewCell *)[tableView dequeueReusableCellWithIdentifier: #"AlbumCell"];
if (cell == nil) {
AlbumsViewController *albumsViewController = [[[AlbumsViewController alloc] init] autorelease];
[[NSBundle mainBundle] loadNibNamed: #"AlbumsViewController" owner: albumsViewController options: nil];
cell = albumsViewController.tableViewCellTrack;
}
cell.labelTitle.text = ...;
cell.labelArtist.text = ...;
return cell;
}
Well, I'm not sure why all the tutorials out there doesn't specify this step.
When using your own custom UITableViewCell from Nib, calling dequeueReusableCellWithIdentifier is not enough. You have to specify the "Identifier" in the IB, just for for it in the Table View Cell tab section.
Then make sure the identifier you put in IB is the same as the identifier you use for the dequeueReusableCellWithIdentifier.
Here it is in Swift
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell : UITableViewCell?
let cellId = String(format: "Cell%d", indexPath.row)
cell = alertTable!.dequeueReusableCellWithIdentifier(cellId) as! UITableViewCell?
if cell == nil {
let archivedData = NSKeyedArchiver.archivedDataWithRootObject(masterTableCell!)
cell = NSKeyedUnarchiver.unarchiveObjectWithData(archivedData) as! UITableViewCell?
}
// do some stuff
return cell!
}

Resources