UITableViewCell subclass with UIImageView proper way to reuse - ios

I have a UITableViewCell subclass. I want to add a UIImageView as a subview, but make sure to do proper reuse so that I am not adding the subview over and over. I also want to make sure to nil out the image in prepareForReuse.
What is the proper method for doing this?

In your custom cell subclass you should add any required views in the initWithStyle:reuseIdentifier: method. As long as you have registered your cell class against the reuse identifier in your table view then this initialiser will be called by dequeueReusableCellWithIdentifier:forIndexPath: whenever a new cell is required. This method will not be called when a cell is reused, so your image view won't be added more than once.
You can clear the image view's current image in the cell class's prepareForReuse method.

For proper reuse of cell, register cell xib in viewDidLoad in UIViewController class and then write
CustomCell *cell = [tableView dequeueReusableCellWithIdentifier:#"cell_ID"];
and then load image in imageView inn cell.This guarantees reuse of cell.

You need to add the UIImageView as a property of the UITableviewCell subclass. That way in cellForRowAtIndexPath you just say myCellInstance.profilePicView.image = ...
Have a look at my answer here on how I added textfields as cell subviews. Look specifically at the override for initWithStyle:reusableIdentifier: in the PersonCell class. No use of prototype cells in storyboard, no use of viewWithTag, just as you want.
Here is what the cellForRowAtIndexPath would look like if the Person class had a profile pic :
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
PersonCell *cell = nil;
cell = [tableView dequeueReusableCellWithIdentifier: #"CellWithNameAndSurname"];
if(!cell)
{
cell = [[PersonCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"CellWithNameAndSurname"];
cell.contentView.backgroundColor = [[UIColor blueColor] colorWithAlphaComponent: 0.08f];
cell.delegate = self;
}
//this should be outside the above if statement!
Person *respectivePerson = _peopleArray[indexPath.row];
cell.profilePicView.image = respectivePerson.profilePic;
cell.nameTextField.text = respectivePerson.name;
cell.surnameTextField.text = respectivePerson.surname;
cell.positionLabel.text = [NSString stringWithFormat:#"%i", (int)indexPath.row];
return cell;
}

Proper way to clean cell before reuse is
-(void)prepareForReuse;
just set self.imageView.image = nil;
And to create UIImageView , I'd do something like that:
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
[self setup];
return self;
}
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
[self setup];
return self;
}
- (void)setup {
/// Create your UIImageView and set layout
}

Related

Why did the image in my custom TableViewCell disappear?

I want to customize my TableViewCell, it just fill with one image. And I tried to resize the frame of both the cell and my imageview, but when I run this program, I can't see my image on the screen.
Here is my code about the customer cell:
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
[self initLayout];
}
return self;
}
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
[super setSelected:selected animated:animated];
// Configure the view for the selected state
}
- (void)initLayout {
self.imageView.frame = self.contentView.frame;
[self addSubview:_image];
}
- (void)resizeWithWidth:(NSInteger)width {
// I want the picture's width equal to the screen's, and the picture's height is 1/3 of its width
CGRect frame = [self frame];
frame.size.width = width;
frame.size.height = width / 3;
self.frame = frame;
self.imageView.frame = frame;
}
And in my TableViewController, I get the cell in this way:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
IntroductViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"introductcell" forIndexPath:indexPath];
// Configure the cell...
if (cell) {
cell = [[IntroductViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"introductcell"];
}
cell.imageView.contentMode = UIViewContentModeScaleAspectFill;
if (indexPath.row == 0) {
cell.imageView.image = [UIImage imageNamed:#"1.jpeg"];
} else if (indexPath.row == 1) {
cell.imageView.image = [UIImage imageNamed:#"2.jpeg"];
}
cell.selectionStyle = UITableViewCellSelectionStyleNone;
[cell resizeWithWidth:[UIScreen mainScreen].bounds.size.width];
return cell;
}
And, this is what I see when I run my program:
I noticed that for each cell, there is some space. How can I remove them?
I tried to remove the line: cell.selectionStyle = UITableViewCellSelectionStyleNone; in cellForIndex, and I found when click the second cell, it shows the image, but I still can not see the first image in the first cell, is there some relationship?
I see a couple of possible problems with the code, but I can't guarantee that this will solve the problem without more context.
1) You are using
- dequeueReusableCellWithIdentifier:forIndexPath: which requires that you have called either registerNib:forCellReuseIdentifier: or registerClass:forCellReuseIdentifier: (the first if you are using the interface builder, the second if you defined the cell in code only). I'm guessing you'll need the second one in your case. Example (this would be appropriate in your TableViewController's viewDidLoad):
[tableView registerClass: [IntroductViewCell class] forCellReuseIdentifier: #"introductcell"];
Sorry if my Objective-C is off a bit, I've been using Swift exclusively for the last year or so. If you already had this but left that part out of this question, then that's fine.
2) Get rid of this bit:
if (cell) {
cell = [[IntroductViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"introductcell"];
}
First of all, you probably wanted !cell there (or something like that). You probably didn't want to scrap the cell already there (you wanted a new one if there was none allocated, right? Not to re-allocate if the cell retrieval was successful). Second, it's no longer needed once you do the registerClass /
dequeueReusableCellWithIdentifier:forIndexPath: combo (in particular, the latter method will always return a valid cell).
The problem is with this line:
IntroductViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"introductcell" forIndexPath:indexPath];
Change it to:
IntroductViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"introductcell”];
To remove the separator line, use:
tableView.separatorStyle = UITableViewCellSeparatorStyleNone;

What is UITableViewCell identifier's role when I create UITableView programmatically?

Previously when I create a UITableView using storyboard, I set the cell identifier in property inspector and paste it into method cellForRowAtIndexPath, then use method dequeueReusableCellWithIdentifier to create a new UITableViewCell. However, when I create a UITableView programmatically, I have nowhere to set up cell identifier. I mean, I can arbitrarily appoint an identifier without having to match that in the property inspector. Am I right? And how should I create a UITableViewCell under this situation?
You can create UITableview and UITableviewCell both programmatically as below.
Create Tableview Programmatically,
#interface ViewController () <UITableViewDataSource>{
//create tableview instance
UITableView *tableViewPro;
}
Alloc it in viewDidLoad
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// init table view
tableViewPro = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
// must set delegate & dataSource, otherwise the the table will be empty and not responsive
tableViewPro.dataSource = self;
tableViewPro.backgroundColor = [UIColor cyanColor];
// add to canvas
[self.view addSubview:tableViewPro];
}
Assign number of rows and UITableview cell in Tableview datasource methods
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return 10;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString *cellIdentifier = #"Tableviewcell";
// Similar to UITableViewCell, but
TableViewCell *cell = (TableViewCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (cell == nil) {
//initialize uitableviewcell programmatically.
cell = [[TableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
}
// Just want to test, so I hardcode the data
cell.descriptionLabel.text = [NSString stringWithFormat:#"Testing index %ld",(long)indexPath.row];
return cell;
}
Now, create your UITableviewCell programmatically. First Add New->file->Cocoa Touch class->Enter tableviewcellname with subclass-of UITableviewCell and UNCHECK the 'Also create XIB file', because cell will be created programmatically.
Two files will be created .h and .m of UITableviewCell, I've create UITableviewCell with TableViewCell name. below is the code to achieve it programmatically.
#interface TableViewCell : UITableViewCell
#property (nonatomic,retain) UILabel *descriptionLabel;
#end
Synthesize descriptionLabel in #implementation file of TableViewCell
#synthesize descriptionLabel = _descriptionLabel;
Below is the initialization function function of creating a UITableviewCell Programmatically. ADD THIS FUNCTION ON #implementation file of TableViewCell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
//it will create UITableiviewCell programmatically and assign that instance on self.
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
// configure control(s)
self.descriptionLabel = [[UILabel alloc] initWithFrame:CGRectMake(5, 10, 300, 30)];
self.descriptionLabel.textColor = [UIColor blackColor];
self.descriptionLabel.font = [UIFont fontWithName:#"Arial" size:12.0f];
[self addSubview:self.descriptionLabel];
}
return self;
}
Below is the resulted output.

UILabel appears multiple times in UITableViewCell

I'm currently creating my UITableViewCells programmatically like so:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Home-Cell" forIndexPath:indexPath];
UILabel *newLabel = [[UILabel alloc] initwithframe:cell.frame];
[newLabel setText:self.data[indexPath.row]];
[cell addSubview:newLabel];
return cell;
}
This seems to create a new UILabel each time the cell is reused though, which I definitely don't want. I tried doing the following:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Home-Cell" forIndexPath:indexPath];
if (cell == nil) {
UILabel *newLabel = [[UILabel alloc] initwithframe:cell.frame];
[cell addSubview:newLabel];
}
[newLabel setText:self.data[indexPath.row]];
return cell;
}
but then the UILabel seems to never be created. Perhaps this is because I'm using prototype cells with Storyboard and thus the cells are never nil?
You have two solutions.
Create a custom table view cell that already has the label.
If you want to add the label in code, don't register a class for the cell. Then the dequeued cell can be nil and you can add the label at that time (like in your 2nd set of code). This also requires using the dequeueReusableCellWithIdentifier method that doesn't also take an indexPath.
You should create a UITableViewCell subclass and add "newLabel" as a property.
The cell is never nil because the method you use to dequeue the table view cell always returns a cell, creating one if it doesn't already exist in the reuse queue.
A better solution would be to create the label in the cell prototype in the storyboard.
This implementation is against MVC architecture where controller managers stuff and do not deal with view. Here, you are trying to add stuff in view from controller. It is suggested to subclass UITableViewCell as below and add your custom UI controls in there
MyTableViewCell.h
#interface MyTableViewCell : UITableViewCell {
}
#property (nonatomic, strong) UILabel *myLabel;
#end
Then you can implement layoutSubviews in your MyTableViewCell.m file to define the look and feel of your cell.
MyTableViewCell.m
- (id)initWithStyle:(UITableViewCellStyle)iStyle reuseIdentifier:(NSString *)iReuseIdentifier {
if ((self = [super initWithStyle:iStyle reuseIdentifier:iReuseIdentifier])) {
self.myLabel = [[UILabel alloc] initWithFrame:<Your_Frame>];
// Set more Label Properties
[self.contentView addSubview:self.myLabel];
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
// Override run time properties
}
Finally use your custom cell like this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MyTableViewCell *cell = (MyTableViewCell *)[tableView dequeueReusableCellWithIdentifier:#"Home-Cell" forIndexPath:indexPath];
if (cell == nil) {
cell = [[MyTableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#"Home-Cell"];
}
cell.myLabel.text = self.data[indexPath.row];
return cell;
}
As a side note, I hope you know that you get textLabel and detailTextLabel free from default UITableViewCell implementation.

What is the correct way to modify the look of a UIView within a UITableViewCell?

Say that you have a custom UITableViewCell and within the cell you have a UIView in which you wanted to have a bottom right and top right radius. Where would be the correct place to make these changes?
I'm assuming that it is not within the drawRect of the UITableViewCell because that would be a huge performance hit. Is there some other function within UITableViewCell to do this? Or should I be doing this in my UITableViewController within the function
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
I would do it once you instantiate your custom cell within the cellForRowAtIndexPath:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
NSString *cellIdentifier = #"cell";
UICustomCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
cell.customview.layer.cornerRadius = 5.0f;
//add other custom stuff here as well
}
return cell;
}
Since this view is owned by the cell, I would do it in the cell's init method -- I think it's best to keep any view setup or modifications that doesn't depend on the index path (i.e. aren't dynamic) out of the table view's data source methods. Which init method depends on how you make your cell. If the cell is made in the storyboard, then use initWithCoder:, if in a xib, then initWithNibName:bundle:. The other alternative is to subclass that view itself, and do the modification in its init method.
I suggest to use the UITableViewCell subclass and add the custom view with the custom radius into the contentView.
Here is my simple implementation.
#import "TableViewCell.h"
#import "RoundedView.h"
#implementation TableViewCell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
RoundedView *roundedView = [[RoundedView alloc] initWithFrame:CGRectInset(self.contentView.bounds, 2, 2)];
[self.contentView addSubview:roundedView];
}
return self;
}
#end
And the rounded my simply set the rounded corner to the required edges.
#import "RoundedView.h"
#implementation RoundedView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor clearColor];
}
return self;
}
- (void)drawRect:(CGRect)rect{
CGContextRef context = UIGraphicsGetCurrentContext();
UIBezierPath *bp = [UIBezierPath bezierPathWithRoundedRect:self.bounds byRoundingCorners:UIRectCornerTopRight | UIRectCornerBottomLeft cornerRadii:CGSizeMake(10, 10)];
CGContextAddPath(context, bp.CGPath);
CGContextClip(context);
[[UIColor brownColor] setFill];
UIRectFill(rect);
}
#end
And inside the cellForRowAtIndexPath:,
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
TableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"CELL"];
if(!cell){
cell = [[TableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"CELL"];
}
return cell;
}
Note since the view is added to cell only inside initWithStyle:reuseIdentifier: method, the view will only be draw when the a new cell is allocated. Most of the time, UITableView dequeues the cell and so this is only drawn few time.
And the final result looks like this,

Custom UITableViewCell views are blank after searching

I believe this question is my exact problem, but I was not able to fix the issue looking at the accepted answer.
UISearchBar: FilterContentForSearchText not working on a UITableView (results not shown on table)
I have a UITableViewController that allows searching. It was working perfectly via the default UITableViewCellStyle, then I decided to implement my own custom layout for the cells. I did not subclass UITableViewCell, but instead added two UILabels to the cell via Interface Builder and set up Auto Layout. I assigned them unique tags so that I can reference those labels in cellForRowAtIndexPath:. It works great - everything appears as expected, until you search. The search does work and it displays the sections and rows, but the labels on the rows have no text so the cell appears completely blank.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"List View Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
UILabel *leftLabel = (UILabel *)[cell viewWithTag:100];
UILabel *rightLabel = (UILabel *)[cell viewWithTag:101];
if (tableView == self.searchDisplayController.searchResultsTableView) {
leftLabel.text = ...;
rightLabel.text = ...;
}
else {
leftLabel.text = ...;
rightLabel.text = ...;
}
return cell;
}
I do know why the labels are blank. When you search, dequeueReusableCellWithIdentifier: returns nil so it inits a new cell with the reuse identifier, but that cell does not have a viewWithTag: and this results in left and right label becoming nil. So it obviously cannot add text to nil.
The answer provided in the link stated you should create the label in the if (!cell) statement and then call [cell.contentView addSubview:left{right}Label];. I did that, and then moved my label configuration code into that if statement as well. But when I do that, the main table's rows only has the default values of my left and right labels from Storyboard - it doesn't set the text of the labels. This is because dequeueReusableCellWithIdentifier: doesn't return nil and instead creates a new cell, so it doesn't ever set the text because that's in the if (!cell) statement.
I could not figure out how to take care of both situations: when cell is nil and when it is not. What do I need to do to fix this?
More comments: I've never used xib files before and I'd prefer to keep it that way. :) I wouldn't mind subclassing UITableViewCell if that's a solution. Of course, I would like to implement this the "proper" way - only create a cell when one is needed etc.
I think the easiest way to do this is to make your cell in a xib file if you want to use the same cell type for both the main table and the search results table. You can make a subclass if you want (you only need to put in IBOutlets to your two labels in the .h file), or do it the same way you already did using tags. In viewDidLoad of the table view controller, register the nib for both tables,
[self.searchDisplayController.searchResultsTableView registerNib:[UINib nibWithNibName:#"CommonCell" bundle:nil] forCellReuseIdentifier:#"CommonCell"];
[self.tableView registerNib:[UINib nibWithNibName:#"CommonCell" bundle:nil] forCellReuseIdentifier:#"CommonCell"];
Then in cellForRowAtIndexPath:, you only need to dequeue the cell with that same identifier, and populate the labels. There's no need to check for cell equals nil, because it never will be.
I modified one of my apps to show how you can implement cellForRowAtIndexPath. I subclassed the cell (CommonCell is the class), only adding IBOutlets to the leftLabel, and rightLabel,
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
CommonCell *cell = [tableView dequeueReusableCellWithIdentifier:#"CommonCell" forIndexPath:indexPath];
cell.leftLabel.text = ([tableView isEqual:self.tableView])? self.theData[indexPath.row] : self.filteredData[indexPath.row];
cell.rightLabel.text = ([tableView isEqual:self.tableView])? self.theData[indexPath.row] : self.filteredData[indexPath.row];
return cell;
}
This is conceptually wrong: you are instantiate a NEW CELL from code that is not the cell from interface builder. If you want use that on interface builder you need to register the nib for your tableView, and associate it to an identifier (se the same identifier also in the cell on interface builder):
[self.searchDisplayController.searchResultsTableView registerNib:[UINib nibWithNibName:#"NameNib" bundle:[NSBundle mainBundle]] forCellReuseIdentifier:#"Identifier"];
But the question is: why? The best thing to do is create a subclass and add your labels. It is really simple:
1) Create a new file CustomSearchCell object that extends UITableViewCell:
CustomSearchCell.h
#import <UIKit/UIKit.h>
#interface CustomSearchCell : UITableViewCell
#property (strong, nonatomic) UILabel *leftLabel;
#property (strong, nonatomic) UILabel *rightLabel;
#end
CustomSearchCell.m
#import "CustomSearchCell.h"
#implementation CustomSearchCell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
// Initialization code
_leftLabel = [[UILabel alloc] initWithFrame:CGRectMake(100, 10, 200, 45)];
[_leftLabel setFont:[UIFont systemFontOfSize:20.0]];
[_leftLabel setTextColor:[UIColor blackColor]];
_rightLabel = [[UILabel alloc] initWithFrame:CGRectMake(100, 45, 200, 25)];
[_rightLabel setFont:[UIFont systemFontOfSize:15.0]];
[_rightLabel setTextColor:[UIColor grayColor]];
[self.contentView addSubview: _leftLabel];
[self.contentView addSubview: _rightLabel];
}
return self;
}
#end
2) In your view controller:
#import "CustomSearchCell.h"
and:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"ListCellIdentifier";
CustomSearchCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (!cell) {
cell = [[CustomSearchCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
if (tableView == self.searchDisplayController.searchResultsTableView) {
cell.leftLabel = ...;
cell.rightLabel = ...;
}
else {
cell.leftLabel = ...;
cell.rightLabel = ...;
}
return cell;
}

Resources