I have a table view where the cells have a variable height. This causes problems with having a reuse identifier but I would really like the cache for UITableViewCells that Apple gave me. Therefore I tried making a variable reuse identifier and it seems but I'm not sure if it's the right way.
Can anyone tell me if I'm handling multiple reuse identifiers right?
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
float height = [self calcCellHeight:indexPath];
NSString *CellIdentifier = [NSString stringWithFormat:#"TextCell_%f", height];
TextCell *textCell = (TextCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (textCell == nil) {
textCell = [[TextCell alloc] initWithHeight:height reuseIdentifier:CellIdentifier];
}
return textCell;
}
The best way to do this is probably to set the height every time you get the cell, and then recalculate all the internal cell frames in that setter. Here's an example:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
float height = [self calcCellHeight:indexPath];
TextCell *textCell = (TextCell *)[tableView dequeueReusableCellWithIdentifier: #"AlwaysTheSame"];
if (textCell == nil) {
textCell = [[[TextCell alloc] init] autorelease];
}
[textCell setHeight: height];
return textCell;
}
Also, note you forgot your autorelease the first time through.
// TextCell
- (id) init {
if ([super initWithStyle: UITableViewCellStyleDefault
reuseIdentifier: #"AlwaysTheSame"]) {
self.myInternalStuff = [[[MyInternalStuff alloc] initWithFrame: CGRectZero] autorelease];
// I don't know what size I am yet!
}
return self;
}
- (void) setHeight: (CGFloat) height {
self.myInternalStuff.frame = CGRectMake(0, 0, 100, height);
// I know what height I am now, so I can lay myself out!
}
You can do that, but if there's much variability in the height of the cells it's going to pretty much defeat the purpose of caching cells. After all, you won't save much time or memory if you have fifty different sizes of cell in your cache.
If you're going to do that, I'd suggest using an int rather than a float to construct the identifier.
Related
I have a custom/sub-classed UITableViewCell using NSLayoutConstraints. The left most UIImageView may or may not be populated with an image, and if not, the objects to right of this field will naturally shift to the left. Works well within the custom UITableViewCell, however I'll be populating the content with my custom UITableViewController. Unfortunately, following code will populate every row cell.dispatchedView with an image -- though I've trapped to not populate rows with anything.
This has something to do with the dequeueReusableCellWithIdentifier. Ideas?
static NSString *CellIdentifier = #"CellIdentifier";
- (void)viewDidLoad {
[super viewDidLoad];
[self.tableView registerClass:[NXGActiveUnitsCell class] forCellReuseIdentifier:CellIdentifier];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath {
NSDictionary *item = [self.model.dataSource objectAtIndex:indexPath.row];
NXGActiveUnitsCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
if([[[item objectForKey:#"cfs_no"] clean] length] > 0) {
cell.dispatchedView.image = [UIImage imageNamed:#"p.png"];
NSLog(#">>>%#",[item objectForKey:#"cfs_no"] );
} else {
NSLog(#">>> i got nothing so I should not touch this row...");
}
cell.departmentIconView.image = [UIImage imageNamed:#"f.png"];
cell.unitLabel.text = [item valueForKey:#"unit_id"];
return cell;
}
I don't know if this already solves your problem, but you should
definitely set
cell.dispatchedView.image = nil;
in the else-case, because cells are reused.
I have a UITableView loading a custom UITableViewCell from a XIB file. Everything is working fine, but my layout requires that the cells (all inside one single section) have spacing between them.
Any chance this can be done without having to raise the ROW height?
how it is now
how it's supossed to be
EDIT:
this is how the code is today
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return [[self.cards valueForKeyPath:#"cards"] count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
[ccTableView setBackgroundColor:[UIColor clearColor]];
cardsCell *cell = [tableView dequeueReusableCellWithIdentifier:#"cardsCell"];
if(cell == nil){
NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:#"cardsCell" owner:self options:nil];
cell = [topLevelObjects objectAtIndex:0];
}
NSString *nmCard = [[self.cards valueForKeyPath:#"cards.name"] objectAtIndex:indexPath.row];
cell.descCardLabel.text = nmCard;
return cell;
}
There's actually a pretty easy solution to it that I found, if you're using custom uitableviewcell classes.
In the cell class, add this code:
- (void)setFrame:(CGRect)frame {
frame.origin.y += 4;
frame.size.height -= 2 * 4;
[super setFrame:frame];
}
This will give you a 4pt buffer within the cell height you put into the uitablview class you are calling this cell in. Obviously, you now have to compensate for that less space in the cell when putting in labels and images, but it works. You can also do the same thing on the x-axis to make the width of the cell smaller. I've done this in my app to show background images behind my cells.
If you can't change the cell's height, the solution is to use invisible intermediate
cells of the required height. You'll need to recalculate indexes at table view delegate and datasource in that case.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString *CELL_ID2 = #"SOME_STUPID_ID2";
// even rows will be invisible
if (indexPath.row % 2 == 1)
{
UITableViewCell * cell2 = [tableView dequeueReusableCellWithIdentifier:CELL_ID2];
if (cell2 == nil)
{
cell2 = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:CELL_ID2];
[cell2.contentView setAlpha:0];
[cell2 setUserInteractionEnabled:NO]; // prevent selection and other stuff
}
return cell2;
}
[ccTableView setBackgroundColor:[UIColor clearColor]];
cardsCell *cell = [tableView dequeueReusableCellWithIdentifier:#"cardsCell"];
if(cell == nil){
NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:#"cardsCell" owner:self options:nil];
cell = [topLevelObjects objectAtIndex:0];
}
// Use indexPath.row/2 instead of indexPath.row for the visible section to get the correct datasource index (number of rows is increased to add the invisible rows)
NSString *nmCard = [[self.cards valueForKeyPath:#"cards.name"] objectAtIndex:(indexPath.row/2)];
cell.descCardLabel.text = nmCard;
return cell;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
// two times minus one (invisible at even rows => visibleCount == invisibleCount+1)
return [[self.cards valueForKeyPath:#"cards"] count] * 2 - 1;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row % 2 == 1)
return 40;
return 162;
}
You will also need to recalculate the indexPath.row for :didSelectRowAtIndexPath: and other methods where it is used.
Although #A-Live is technically correct, in order to prevent other issues, ex: you forget to put a indexPath.row/2 somewhere, you can do the following (which involves no programming):
Say for example your UITableViewCell height normally is 90 points and you want a 10 point spacing in between each cell. Make your UITableViewCell 100 points high and just make the bottom 10 points of the UITableViewCell blank (no objects of any sort). Then click the UITableViewCell in interface builder and set the backgroundColor to whatever you want the spacing area's color to be.
Voila! You got spacing in between each cell with a little work that is much easier than programming /2 everywhere!
If you are looking for a solution in swift, this answer from reddit worked perfectly for me.
class CustomTableViewCell: UITableViewCell {
override var frame: CGRect {
get {
return super.frame
}
set (newFrame) {
var frame = newFrame
frame.origin.y += 4
frame.size.height -= 2 * 4
super.frame = frame
}
}
}
Below is code for UITableView, But when i scroll its behaves weirdly (too annoying)... This problem is due to reuseIdentifier.... but dont know how to solve..
- (UITableViewCell *)tableView:(UITableView *)tableView1 cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView1 dequeueReusableCellWithIdentifier:CellIdentifier];
NSInteger imgTag = 1;
NSInteger lblTag = 2;
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
UIImageView *imgView = [[UIImageView alloc] initWithFrame:CGRectMake(2, 2, 52, 52)];
// Image:[UIImage imageNamed:[self.glassType objectAtIndex:indexPath.row]]];
imgView.tag = imgTag;
[cell.contentView addSubview:imgView];
[imgView release];
UILabel *lblName = [[UILabel alloc] initWithFrame:CGRectMake(60, cell.frame.size.height/4, 200, 21)];
// lblName.text = [self.glassName objectAtIndex:indexPath.row];
lblName.tag = lblTag;
[cell addSubview:lblName];
[lblName release];
}
NSInteger imgIndex = 2;
NSInteger lblIndex = 3;
((UIImageView *)[cell viewWithTag:imgTag]).image = [[self.glassType objectAtIndex:indexPath.row] objectAtIndex:imgIndex];
((UILabel *)[cell viewWithTag:lblTag]).text = [[self.glassName objectAtIndex:indexPath.row] objectAtIndex:lblIndex];
return cell;
}
- (void)tableView:(UITableView *)tableView1 didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView1 cellForRowAtIndexPath:indexPath];
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}
How to make Cell for row at index so that it remains constant even when scrolled??? Also how to make single selection in UITableView??
The answer is that you should not add subviews to your table cells outside of the "if (cell == nil) { ..." clause or they get added over and over again to the same cell when it gets re-used.
See my answer to this question for a more detailed explanation, including code for how to fix it:
cellForRowAtIndexPath memory management
You also cannot store state in table cells because as soon as they scroll offscreen they are recycled and re-appear at a different index in your table. You need to set up an array of model objects to store state for your table cells (such as what their accessory type should be). A more detailed explanation can be found in my answer to this question:
Looping through UITableViewCells of a UITableView
If you fix how you are adding subviews to the cells, and store your "ticked" state in an array of model objects as well as setting the cell.accessoryType (so that it can be restored when the cell is dequeued), then your approach to row selection is otherwise correct.
So put this in your tableView:cellForRowAtIndexPath: method, just before the return cell;:
MyModelObject *object = [self.arrayOfModelObjects objectAtIndex:indexPath.row];
BOOL isChecked = object.checked;
cell.accessoryType = isChecked? UITableViewCellAccessoryCheckmark: UITableViewCellAccessoryNone;
And in your tableView: didSelectRowAtIndexPath: method, get rid of the current logic and replace it with:
- (void)tableView:(UITableView *)tableView1 didSelectRowAtIndexPath:(NSIndexPath *)indexPath
for (int i = 0; i < [self.arrayOfModelObjects count]; i++)
{
MyModelObject *object = [self.arrayOfModelObjects objectAtIndex:i];
object.checked = (i == indexPath.row); // only check the one we just tapped
}
//refresh table to update the accessory views for all rows
[tableView1 reloadData];
}
Obviously replace the arrayOfModelObjects with your own model implementation. You could just use an array of NSNumber objects containing bools if you don't want to create a custom class for this purpose.
The recycling queue is like a pool where previously created Cells are stored before to reuse them. For example when you scrolls up, at the moment the cell disappears above, it is stored in the queue and becomes available for the cell that will appear at the bottom. Ok ?
Actually the number of cells really created is exactly the max simultaneous cell you can display in your table (in most cases from 3 to 8). In other words your if (cell == nil) code is executed (more or less from 3 to 8 times) at the first reloadData to create the pool of cells your table needs.
Then all you make on a cell is kept as it and appears again when you dequeue it. It's now easy to understand that, in your code, you have to make all strictly row-dependant settings outside the if (cell == nil) block. The same way, do not add subViews outside the if (cell == nil) block, you can imagine the thousands of subview you will add each time you reset a dequeued cell !
Tip: if you need some custom cleanup before reusing a cell (like to set an image to blank), you can create a custom UITableviewCell class and implements the prepareForReuse method.
Is it clear ?
Always reload your tableView in viewWillAppear method instead of viewDidLoad.
(void) viewWillAppear:(BOOL)animated{
[self.tableView reloadData];
}
This avoids most of all unexpected and annoying problems. :)
I setup my cells like this:
So, a couple of switches, define the cells, because the data is not in the list of objects, but a set of information which should be displayed in a tableView in some different ways.
-(UITableViewCell *)value1CellForTableView:(UITableView *)tableView {
static NSString *CellIdentifierValue1 = #"Value1Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifierValue1];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:CellIdentifierValue1];
cell.detailTextLabel.textAlignment = UITextAlignmentLeft;
cell.textLabel.textAlignment = UITextAlignmentLeft;
}
return cell;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = nil;
switch (indexPath.section) {
case 0:
//Coupon
switch (indexPath.row) {
case 0:
//Couponcode
cell = [self value1CellForTableView:tableView];
cell.textLabel.text = #"Code";
cell.detailTextLabel.text = presentedCoupon.couponNr;
break;
case 1:
//Coupondescription
cell = [self value1CellForTableView:tableView];
cell.detailTextLabel.text = presentedCoupon.couponDescription;
cell.detailTextLabel.numberOfLines = 0;
cell.textLabel.text =#"Ihr Vorteil";
break;
}
break;
case 1:
//Productinfo
switch (indexPath.row) {
case 0:
cell = [self defaultCellForTableView:tableView];
cell.imageView.image = [UIImage imageWithContentsOfFile:[[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent: [presentedCoupon.refProdName stringByAppendingString:#".png"]]];
cell.textLabel.text = presentedCoupon.refProdName;
break;
}
break;
case 2:
//Shopinfo
switch (indexPath.row) {
case 0:
cell = [self defaultCellForTableView:tableView];
cell.textLabel.text = ((Shop*)presentedCoupon.refShop).name;
cell.accessoryType = UITableViewCellAccessoryDetailDisclosureButton;
break;
}
break;
}
if (cell == nil) {
cell = [self defaultCellForTableView:tableView];
cell.textLabel.text = #"Stanni";
}
[cell layoutIfNeeded];
return cell;
}
And i calculate the height like this.
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
NSLog(#"height");
UITableViewCell *cell = [self tableView:tableView cellForRowAtIndexPath:indexPath];
CGFloat height = 24 + [cell.detailTextLabel.text sizeWithFont:cell.detailTextLabel.font constrainedToSize: CGSizeMake(cell.detailTextLabel.frame.size.width, 1000.0f) lineBreakMode:cell.detailTextLabel.lineBreakMode].height;
return MAX(height, 44.0f);
Problem:
The problem is, as mentioned in many threads, and also visible in my log, that the height of each cell, (visible or not) is asked at the initialization of the table view. So in bigger 100+ lists, also 100+ cells are created --> wasted at startup.
Ist there another possibility to get this information when the cell is set up like this? Is it really neccessary to built the switch case sturcture again in heightForRowAtIndexPath to avoid these calls and though get the right heights for each cell?
Would it be better to hold a "datasource-list" with the single information of each cell?
But how to handle the different cell styles, custom cells.
The method
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
is called for each cell before the method
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
So in the first method, cells are not yet created, and you should not try to access them.
When you create a tableView, the data source is first asked for the number of row. Then for each row, the data source is asked for the height of the row, so that the tableView know the total height of it's content, and then finally, the data source is asked for the cell (actual view) to display.
In your case, i would built the switch case structure again. Concerning the "datasource-list", I have never done it before, so maybe it's a better solution.
Two possibilities:
How many cells you have? If you have a small number of cells (which seems to be the case here), you don't need cell reusing! In general, cell reusing is overused.
Just create cells when you are creating your controller or when you have updated your data and put them into a NSArray.
Then you can return them from tableView:cellForRowAtIndexPath: or measure their height.
Create a separate method that returns cell text/font and use it in both delegate methods instead of reading the information from the cell directly.
If you have an array of strings in objects and are using the standard table cell then try this iOS 7 compatible magic:
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
NSString* text = [self.objects objectAtIndex:indexPath.row];
NSAttributedString * attributedString = [[NSAttributedString alloc] initWithString:text attributes:
#{ NSFontAttributeName: [UIFont systemFontOfSize:18]}];
//its not possible to get the cell label width since this method is called before cellForRow so best we can do
//is get the table width and subtract the default extra space on either side of the label.
CGSize constraintSize = CGSizeMake(tableView.frame.size.width - 30, MAXFLOAT);
CGRect rect = [attributedString boundingRectWithSize:constraintSize options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading) context:nil];
//Add back in the extra padding above and below label on table cell.
rect.size.height = rect.size.height + 23;
//if height is smaller than a normal row set it to the normal cell height, otherwise return the bigger dynamic height.
return (rect.size.height < 44 ? 44 : rect.size.height);
}
As I see – you have only two cell types.
So you might have a #property for each type of cell (please take a look to an example below):
static NSString * const kCellIdentifier = #"kCellIdentifier";
#interface ...
#property (nonatomic, retain) UITableViewCell *cell;
#end
#implementation
#synthesize cell = cell_;
...
- (UITableViewCell *)cell {
if (!cell_) {
cell_ = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:kCellIdentifier];
}
return cell_;
}
- (UITableViewCell *)cellForTableView:(UITableView *)tableView {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellIdentifier];
if (!cell) {
//put it to autorelease pool to avoid EXC_BAD_ACCESS
cell = [[self.cell retain] autorelease];
self.cell = nil;
}
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = self.cell;
CGFloat height = 24 + [#"text" sizeWithFont:cell.detailTextLabel.font constrainedToSize: (CGSize){cell.detailTextLabel.frame.size.width, CGFLOAT_MAX} lineBreakMode:cell.detailTextLabel.lineBreakMode].height;
return MAX(height, 44.0f);
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [self cellForTableView:tableView];
cell.detailTextLabel = #"text";
return cell;
}
So the cell would be initialized only once in the beginning.
I have a UITableView with several datasources. This is because, I need to switch the data with a UISegmentedControl, and if I add them as subviews, I cannot use the statusBar to scroll up, etc.
At first, I show a login screen:
self.tableView.dataSource = loginTableView;
self.tableView.delegate = loginTableView;
[self.tableView reloadData];
Then, once the user has logged in, I do the following to change to index:1 of the segmentedControler, which is their profile:
self.tableView.dataSource = profileTableView;
self.tableView.delegate = profileTableView;
[self.tableView reloadData];
However, the table view updates, but is a bit of a mix between the two dataSources. Some of the text has changed, some is overlapping, while some of it is completely missing.
Is there another way I should be changing the dataSource for a UITableView?
Thx
I had the exact same problem. My solution was to make the tableView hidden, change it's source, reload the data and make the tableView visible again.
Example in C# (MonoTouch):
tableView.Hidden = true;
tableView.Source = newTableViewSource;
tableView.ReloadData();
tableView.Hidden = false;
Not sure why this is happening from the code you have posted.
Instead of changing the delegate and datasource, swap out whatever ivar represents the data being displayed:
- (NSArray*)tableData{
if(showingLogin)
return self.loginData;
return self.profileData;
}
Now you only have 1 UITableViewController instance, but a BOOL to tell you which datasource to use.
The table view is caching the cells internally it uses for displaying your data. So if you change the data source you should also check that your is - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath method is updating all the cells to the correct new values.
From your description it sounds like it is using the cached UITableViewCell instances and is not updating it to the correct new data in all cases. Perhaps code like this:
- (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.frame = CGRectZero;
cell.textLabel.font = //Set font;
cell.selectionStyle = UITableViewCellSelectionStyleNone;
cell.textLabel.text = #"My Text for this cell"; // <==== Not good! Move it out of this if block
}
// Set cell text here
}
The simplest solution I found for this sort of problem is to just make the String you use for the cell creation (CellIdentifier) depending of the data Source. In this case you don't mix the cell of the different content types (and this helps you also if the cells need to have a different look depending on the mode).
I had this problem, turned out it was because my CellIdentifiers were the same...
static NSString *CellIdentifier = #"Cell";
Change one of them and the cells layout properly
static NSString *CellIdentifier2 = #"Cell2";
Wow, that's freaky.
The last time I did something like this, I simply used multiple views, hiding one and showing another when the segmented control was tapped. There are other possible solutions, but this is probably easiest and perhaps most efficient.
I have the same issue and what you have to do is use a different cell identifier within - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath.
if (self.segmentedControl.selectedSegmentIndex == 0) {
self.cellIdentifier = #"segmentOne";
} else {
caseAtIndexPath = [self.awaitingReviewCaseList caseAtIndex:indexPath.row];
self.cellIdentifier = #"segmentTwo";
}
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:self.cellIdentifier forIndexPath:indexPath];