Table View Cell With Image - iphone

I have a table view cell with two image views, one image view is a placeholder, and on top of the other image is the actual image I load from the documents directory. I am having issues with the image being incorrectly displayed when the table view resuses the cell.
I solved my problem by using the below method in the cell class, but I have read that this can cause performance issues, any ideas on a better solution?
- (void)prepareForReuse
{
[[self imageView] setImage:nil];
}

First of all, you are not calling [super prepareForReuse] like you should.
And the documentation is pretty clear, you should be setting the image in tableView:cellForRowAtIndexPath: because it's content.
If you set the image in tableView:cellForRowAtIndexPath: there is no point to set it to nil in prepareForReuse.
Imagine the following flow.
You scroll down, cell 0 is put onto the queue.
prepareForReuse sets imageView.image to nil
tableView:cellForRowAtIndexPath: dequeues the cell and sets imageView.image to image1
You are setting imageView.image twice.
If you use nil there might be no measurable performance impact at all. But you might come to the idea to actually set a placeHolder image. So the cell is queued and prepared for reuse, you set the placeholder image; later the cell is dequeued and the placeholder image is replaced by a real image before the placeholder image was even visible.
I would remove the prepareForReuse method completely. You don't need it if you set the image in tableView:cellForRowAtIndexPath:
from the documentation:
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.

Since you have a placeholder image behind this image view, There is nothing wrong if you set the top imageview's image to nil.
To enhance the image generating process, you can use a NSCache object like this,
1)
#property(nonatomic, strong) NSCache *imageCache;
2) Call this methods inside tableView cellForRowAtIndexPath: method, the image generating process can be moved into this,
-(UIImage *) imageForIndexPathRow:(int) row{
id image = [self.imageCache objectForKey:[NSNumber numberWithInt:row]];
if(!image){//if the image is not in the cache,
UIImage *newImage; //create image here
[self.imageCache setObject:newImage forKey:[NSNumber numberWithInt:row]];
return newImage;
}
return (UIImage *) image;
}

Related

Does CALayer derive its contents?

After investigating Custom UITableViewCell backgroundView & selectedBackgroundView, I noticed that each layer of backgroundView & selectedBackgroundView (of each cell of a grouped table view) has a CGImageRef as its contents.
Must Apple be creating each CGImageRef and explicitly setting the contents of each layer?
Yes, a UIGroupTableViewCellBackground is its layer's delegate and sets its layer's contents in displayLayer:. I figured this out by creating MyLayer : CALayer with a setContents: method, in which I put a breakpoint, and implementing +[UIGroupTableViewCellBackground layerClass] to return [MyLayer class]. Then, I inspected the backtrace when the breakpoint was hit.

Replacing the content of UIImage(s) loaded from XIB at runtime

For a concept I'm developing, I need to load XIB files manually and by using class and instance method swizzling I have been able to intercept calls to imageCustomNamed, imageCustomWithContentsOfFile and imageCustomWithCGImage for the UIImage class and initCustomWithImage for UIImageView. What I want to to is detect the image name and replace it with some content rendered on the fly in place of the one set in IB at design time.
for example:
when XIB has an UIImageView object whose Image property is set to "About.png", I need to intercept the loading of that image and replace with another one depending on certain condition. It would be ok even to replace the image after the UIImageView object has loaded the image set at design time, but looks like the original name of the UIImage used to set the content of UIImageView is not stored anywhere.
I cannot use IBOutlets as I don't know the content of XIB file before hand; this is a class that should work on any XIB not just a particular one.
The custom methods are in fact being called in placed of the default ones, but looks like that when loading XIb the system uses imageCustomWithCGImage which accept a CGImageRef as argument; from it there is no way to know the origin (i.e: the image file name)
Any idea on how I can intercept the loading of images?
In OS 3, at least, you can override UIImageNibPlaceholder's initWithCoder:. Replace it with something like this:
-(id)hack_UIImageNibPlaceholder_initWithCoder:(NSCoder*)coder
{
NSString * name = [coder decodeObjectForKey:#"UIResourceName"];
[self release];
return [[UIImage imageNamed:name] retain];
}
I'm not sure what happens if you load nibs from other bundles (e.g. frameworks).
Perhaps you can set another property of UIImageView in xib file, and then reference that property? For example label, or tag...
It's not a good idea to swizzle a private class(UIImageNibPlaceholder)'s method.
But you can overload its superclass(UIImage)'s method. Seems like UIImageNibPlaceholder did't implement 'awakeAfterUsingCoder', so you can safely do this until Apple did it one day.
#import "UIImage+xib.h"
#implementation UIImage (xib)
- (id)awakeAfterUsingCoder:(NSCoder *)coder{
NSString* imageName = [coder decodeObjectForKey:#"UIResourceName"];
if(imageName){
return [UIImage imageNamed:imageName];
}
return self;
}
#end

Items mixed up after scrolling in UITableView

When I scroll in my UITableView, the cells become mixed up.
What am I doing wrong?
This is my method:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
[cell insertSubview:[itemArray objectAtIndex:indexPath.row] atIndex:indexPath.row];
return cell;
}
Update
It now works by using cell.contentView, but now when I select an item, the selected one is overlayed with the content of a different cell...
TechZen's advice here is correct. It's clear from your code that you've misunderstood insertSubview:atIndex. I suspect that you probably also need a better understanding of when tableView:cellForRowAtIndexPath: does and doesn't called.
Unfortunately you've gotten some bad advice from sagar here, which may only confuse you further, especially because it may appear to work at first, but it will kill your scrolling performance and memory usage. For his benefit and yours, let me try to clarify tableView:cellForRowAtIndexPath: and the reuse identifier concept.
The key to understanding tableView:cellForRowAtIndexPath: and the reuse identifier is to understand that building a UITableViewCell is expensive. Consider all the things you need to do:
Allocate a cell
Allocate the cell's subviews.
Define the layout of the subviews within the cell.
Add the subviews to the cell.
Configure properties of the subviews such as font sizes, colors, text wrapping, resizing behaviors, etc.
Configure properties of the cell, such as accessory images, etc.
Define the specific text and/or images that you want the cell to display.
When we create a table, we usually want the cells to have the same basic configuration. They'll typically have the same number of subviews, in the same positions, using the same fonts, etc. In fact, the only thing that usually needs to vary from one cell to the next is item 7 in the list above, the text and images displayed by the cell.
Steps one through six are quite expensive (especially the memory allocation), so it would kill our scrolling performance if we were to go through those steps for every cell we created, only to throw that cell away when it scrolls off the screen. It would be better if we could save the cell when it scrolls off the screen, and then just tweak its contents and reuse it for the next cell that we need to display.
Apple recognized the need for this cell reuse optimization, so they built a mechanism for it right into UITableView. When a cell scrolls off the screen, UITableView doesn't throw it away. Instead it looks at the cell's reuse identifier string, and puts the cell into a special buffer associated with that identifier. The next time you call dequeueReusableCellWithIdentifier: with that same identifier, UITableView will pull the cell out of its buffer and hand it back to you for reuse. This cell still has all the same subviews, in the same configuration as before, so all you need to do is step 7 in our list. Simply update the cell's text and/or images, and it's ready to go.
When you use this mechanism correctly, you'll only allocate one cell for each visible row, plus one for the buffer. No matter how many rows you have in your table, your memory usage will stay low, and your scrolling will be as smooth as butter.
Sagar recommended that you use a different reuse identifier for each row. Hopefully you can see why this is a bad idea. When each cell scrolls off the screen, the table view will look at the cell's identifier, see that it's unique, and create a new buffer for that specific row. If you scroll through 10,000 rows, your table view will end up with 10,000 buffers, each dedicated to a single cell. Your scrolling will be unnecessarily slow while you create 10,000 cell objects, and your app will probably run out of memory before you get to the bottom of the table.
So go ahead and keep your common cell identifier. Inside the if (cell == nil) { } block, put all the setup code that would be common for all cells. Beneath that block, put only the code that populates the contents that are unique to each row. To access custom subviews whose contents you want to change per row, you can use -[UIView viewWithTag:], or better yet, create a subclass of UITableViewCell, and expose your custom subviews as properties of your subclass.
I think your problem here is that you are applying your row logic to the view hierarchy inside a cell instead of to the cells themselves.
This line:
[cell insertSubview:[itemArray objectAtIndex:indexPath.row] atIndex:indexPath.row];
Takes a view from an array and adds it to the cell's subviews at a particular index.row of the cell's existing subview stack. It does nothing to make sure the proper view is inserted in the proper cell itself. If you never remove the views from the previous iteration you will just see all these views stacking up within the individual reused cells.
At the very least, you need to remove all the previously added cell subviews before adding the most one. You should also only add subviews to the cell's contentView view and not to the cell itself.
So:
[[cell.contentView.subviews objectAtIndex:0] removeFromSuperview];
[cell.contentView addSubview:[itemArray objectAtIndex:indexPath.row]];

UITableViewCell doesn't clear context before drawing

I have a subclass of UITableViewCell which contains several elements - UIImageViews, Labels, etc.
Since this cell is intended to be reusable, I want to be able to change it's appearance a bit depending on what data it is currently displaying.
So as an example - I have this view in my custom UITableViewCell:
UIImageView* delimeterView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"cellDelimiter.png"]];
Which I want to be able to hide sometimes like this:
- (void) setRecord:(id)record__ {
if (record__.type == NO_DELIMETER_VIEW)
delimeterView.hidden = YES;
else
delimeterView.hidden = NO;
[self setNeedsLayout];
}
But the problem is that delimeterView will always be displayed on the cell, just like if it was drawn once in the init method and then drawing context was never changed or cleared. I've tried setting clearsContextBeforeDrawing property to YES for both cell and its contentView, I've also tried setting opaque for cell and its contentView to NO since I've read there might be some problems with that aswell in case you're using transparent views.
Nothing helps.
It looks like UITableViewCell never clears its graphic context and just paints over old elements.
Any tips on what am I doing wrong?
I know I can probably fix this by doing custom drawing but I'd rather not.
First, are you sure that delimeterView in setRecord: is actually pointing to your delimeterView? In the code example you give, you assign it to a local. Do you later assign this to an ivar? (You should always use accessors to access ivars: self.delimeterView).
Next, calling -setNeedsLayout just schedules a call to -layoutIfNeeded, which walks the hierarchy calling -layoutSubviews. The default implementation of -layoutSubviews does nothing. You probably meant to call -setNeedsDisplay here, or you need to implement -layoutSubviews to do what you want.

UITableView flexible/dynamic heightForRowAtIndexPath

Case
Normally you would use the cellForRowAtIndexPath delegate method to setup your cell. The information set for the cell is important for how the cell is drawn and what the size will be.
Unfortunatly the heightForRowAtIndexPath delegate method is called before the cellForRowAtIndexPath delegate method so we can't simply tell the delegate to return the height of the cell, since this will be zero at that time.
So we need to calculate the size before the cell is drawn in the table. Luckily there is a method that does just that, sizeWithFont, which belongs to the NSString class. However there is problem, in order to calculate the correct size dynamically it needs to know how the elements in the cell will be presented. I will make this clear in an example:
Imagine a UITableViewCell, which contains a label named textLabel. Within the cellForRowAtIndexPath delegate method we place textLabel.numberOfLines = 0, which basically tells the label it can have as many lines as it needs to present the text for a specific width. The problem occurs if we give textLabel a text larger then the width originally given to textLabel. The second line will appear, but the height of the cell will not be automatically adjusted and so we get a messed up looking table view.
As said earlier, we can use sizeWithFont to calculate the height, but it needs to know which Font is used, for what width, etc. If, for simplicity reasons, we just care about the width, we could hardcode that the width would be around 320.0 (not taking padding in consideration). But what would happen if we used UITableViewStyleGrouped instead of plain the width would then be around 300.0 and the cell would again be messed up. Or what happends if we swap from portrait to landscape, we have much more space, yet it won't be used since we hardcoded 300.0.
This is the case in which at some point you have to ask yourself the question how much can you avoid hardcoding.
My Own Thoughts
You could call the cellForRowAtIndexPath method that belongs to the UITableView class to get the cell for a certain section and row. I read a couple of posts that said you don't want to do that, but I don't really understand that. Yes, I agree it will already allocate the cell, but the heightForRowAtIndexPath delegate method is only called for the cells that will be visible so the cell will be allocated anyway. If you properly use the dequeueReusableCellWithIdentifier the cell will not be allocated again in the cellForRowAtIndexPath method, instead a pointer is used and the properties are just adjusted. Then what's the problem?
Note that the cell is NOT drawn within the cellForRowAtIndexPath delegate method, when the table view cell becomes visible the script will call the setNeedDisplay method on the UITableVieCell which triggers the drawRect method to draw the cell. So calling the cellForRowAtIndexPath delegate directly will not lose performance because it needs to be drawn twice.
Okay so by calling the cellForRowAtIndexPath delegate method within the heightForRowAtIndexPath delegate method we receive all the information we need about the cell to determine it's size.
Perhaps you can create your own sizeForCell method that runs through all the options, what if the cell is in Value1 style, or Value2, etc.
Conclusion/Question
It's just a theory I described in my thoughts, I would like to know if what I wrote is correct. Or that maybe there is another way to accomplish the same thing. Note that I want to be able to do things as flexible as possible.
Yes, I agree it will already allocate the cell, but the heightForRowAtIndexPath delegate method is only called for the cells that will be visible so the cell will be allocated anyway.
This is incorrect. The table view needs to call heightForRowAtIndexPath (if it's implemented) for all rows that are in the table view, not just the ones currently being displayed. The reason is that it needs to figure out its total height to display the correct scroll indicators.
I used to do this by:
Creating a collection objects (array of size information (dictionary, NSNumber of row heights, etc.) based on the collection objects that will be used for the table view.
This is done when we're processing the data either from a local or remote source.
I predetermine the type and size of the font that will be used, when I'm creating this collection objects. You can even store the UIFont objects or whatever custom objects used to represent the content.
These collection objects will be used every time I implement UITableViewDataSource or UITableViewDelegate protocols to determine the sizes of the UITableViewCell instances and its subviews, etc.
By doing it this way you can avoid having to subclass UITableViewCell just to get the various size properties of its content.
Don't use an absolute value for initializing the frames. Use a relative value based on the current orientation and bounds.
If we rotate it to any orientation, just do a resizing mechanism at runtime. Make sure the autoresizingMask is set correctly.
You only need the heights, you don't need all of that unnecessary things inside a UITableViewCell to determine the row height. You may not even need the width, because as I said the width value should be relative to the view bounds.
Here is my approach for solving this
I assume in this solution that only one Label has a "dynamic" height
I also assume if we make the label auto size to stretch the height as the cell grows only the cell height is needed to change
I assume that the nib has the appropriate spacing for where the label will be and how much space is above and bellow it
We dont want to change the code every time we change the font or position of the label in the nib
How to update the height:
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
// We want the UIFont to be the same as what is in the nib,
// but we dont want to call tableView dequeue a bunch because its slow.
// If we make the font static and only load it once we can reuse it every
// time we get into this method
static UIFont* dynamicTextFont;
static CGRect textFrame;
static CGFloat extraHeight;
if( !dynamicTextFont ) {
DetailCell *cell = [tableView dequeueReusableCellWithIdentifier:#"cell"];
dynamicTextFont = cell.resizeLabel.font;
CGRect cellFrame = cell.frame;
textFrame = cell.resizeLabel.frame;
extraHeight = cellFrame.size.height-textFrame.size.height; // The space above and below the growing field
}
NSString* text = .... // Get this from the some object using indexPath
CGSize size = [text sizeWithFont:dynamicTextFont constrainedToSize:CGSizeMake(textFrame.size.width, 200000.f) lineBreakMode:UILineBreakModeWordWrap];
return size.height+extraHeight;
}
Issues:
If you are not using a prototype cell you will need to check if the cell is nil and init it
Your nib / storyboard must have the UILabel autosize and have multi line set to 0
You should have a look at TTTableItemCell.m in the Three20 framework. It follows a different approach, basically by having each cell class (with some predefined settings like font, layout etc.) implement a shared method + tableView: sizeForItem: (or something like that), where it gets passed the text in the item object. When you look up the text for a specific cell, you can as well look up the appropriate font, too.
Regarding the cell height: You can check your tableView's width and, if necessary, subtract the margins by UITableViewStyleGrouped and the width an eventual index bar and disclosure item (which you look for in the data storage for your cells' data). When the width of the tableView changes, e.g. by interface rotation, you have to call [tableView reloadData].
To answer the question the original poster asked which was 'is it ok to call cellForRowAtIndexPath?', it's not. That will give you a cell but it will NOT allocate it to that indexPath internally nor will it be re-queued (no method to put it back), so you'll just lose it. I suppose it will be in an autorelease pool and will be deallocated eventually, but you'll still be creating loads of cells over and over again and that is really pretty wasteful.
You can do dynamic cell heights, you can even make them look quite nice, but it's a lot of work to really make them look seamless, even more if you want to support multiple orientations etc.
I have an idea about dynamic cell height.
Just create one instance of your custom cell as member variable of UITableViewController. In the tableView:heightForRowAtIndexPath: method set the cell's content and return the cell's height.
This way you won't be creating/autoreleasing cell multiple times as you will if you call cellForRowAtIndexPath inside the heightForRowAtIndexPath method.
UPD: For convenience, you can also create a static method in your custom cell class that will create a singleton cell instance for height calculation, set the cell's content and then return it's height.
tableView:heightForRowAtIndexPath: function body will now look like this:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return [MyCell cellHeightForContent:yourContent];
}
Here's my solution which I've used to implement some rather slick cells for a chatting app.
Up to this point I've always been really really irritated with heightForCellAtIndexPath: because it leads to violating the DRY principle. With this solution my heightForRowAtIndexPath: costs 1.5ms per cell which I could shave down to ~1ms.
Basically, you want each subview inside your cell to implement sizeThatFits: Create an offscreen cell which you configure then query the root view with sizeThatFits:CGSizeMake(tableViewWidth, CGFLOAT_MAX).
There are a few gotchas along the way. Some UIKit views have expensive setter operations. For example -[UITextView setText] does a lot of work. The trick here is to create a subclass, buffer the variable, then override setNeedsDisplay to call -[super setText:] when the view is about to be rendered. Of course, you'll have to implement your own sizeThatFits: using the UIKit extensions.