I have slightly different cells and for such purpose I need to pass parameter from my UIViewController to my subclass of UITableViewCell. But it does not work. The scenario is written as below:
MessagesViewController.m :
#import "MessagesViewController.h"
#import "MessageTableViewCell.h"
- (void)viewDidLoad
{
[super viewDidLoad];
[self.tableView registerClass:[MessageTableViewCell class] forCellReuseIdentifier:MessengerCellIdentifier];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
MessageTableViewCell *cell = (MessageTableViewCell *)[self.tableView dequeueReusableCellWithIdentifier:MessengerCellIdentifier];
if (cell == nil) {
cell = [[MessageTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:MessengerCellIdentifier customParam:YES];
}
return cell;
}
MessageTableViewCell.m :
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier customParam:(BOOL)customParam
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
// **** GET Custom Parameter (customParam) HERE ??? ****/
}
return self;
}
At this scenario customParam is my parameter. Everything seems as OK, but cell is not nil and so the procedure fails.
Option 1: Remove the dequeueReusableCellWithIdentifier line so that you create a new custom cell every time. Otherwise you are using a pre-existing cell that already has the previous customParameter set to whatever the last cell displayed was set to.
-Note to option 1 (added as explanation of why it is a very, VERY, VERY bad idea (#Duncan C). Since you are setting up your cells with a ReuseIdentifier in creating your cells iOS will hold on to them for you once they scroll offscreen so that you can reuse them when your code asks for it. But then your code never asks for reusable cells because it makes a completely new one each time the table asks for the next cell. This causes high load times (to create a new cell every time) and high memory use (since the OS is saving the cells for you to use later and not deallocating them immediately). The reusability was built for a reason, so don't use Option 1 unless you have very specific need to do so (and even then, you are probably wrong, don't do it).
Option 2: Change the custom parameter to a separate method call. Instead of in the initializer create a new method that clears the cell and rebuilds it the way your new custom parameter requires. Then you can re-use cells and modify their looks using the new setCustomParameter: method.
Edit: Code example of option 2, as simple as possible:
In table controller
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
MessageTableViewCell *cell = (MessageTableViewCell *)[self.tableView dequeueReusableCellWithIdentifier:MessengerCellIdentifier];
if (cell == nil) {
cell = [[MessageTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:MessengerCellIdentifier];
}
[cell setCustomParam:customParam];
return cell;
}
In your cell .m
-(void)setCustomParam:(ParamType)type
{
//Do whatever you would like right here to clear the previous
//cell's custom information and add the new custom information
//to this new cell.
}
Then you have to try some another method to add parameters in the MessageTableViewCell. The parameter is nil because the cell aren't nil everytime they are reusing the table view cell from the line
MessageTableViewCell *cell = (MessageTableViewCell *)[self.tableView dequeueReusableCellWithIdentifier:MessengerCellIdentifier];
You have to call another method like
MessageTableViewCell *cell = (MessageTableViewCell *)[self.tableView dequeueReusableCellWithIdentifier:MessengerCellIdentifier];
if (customParam)
{
// IF custom parameter is your labal
cell.yourCustomParameter.text = #"Add your content here"
}
Related
I am using the QuickBlox framework to build a chatting app. Below is the cellForRowAtIndexPath code.
There are some things I'd like to do with each cell only once (like download images), so as I understood, I should add the if (!cell) block to do this.
However, that block never actually fires, even when the tableview is loading for the first time. Why would that be?
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
QBChatMessage *message = [[ChatService shared] messagsForDialogId:self.dialog.ID][indexPath.row];
ChatMessageTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ChatMessageCellIdentifier];
if (!cell) {
// some things I only want to do once here, such as download images. but it never fires
}
[cell configureCellWithMessage:message];
return cell;
}
The call to dequeueReusableCellWithIdentifier: will only return nil if you didn't register a cell using registerClass:forCellReuseIdentifier:.
Remove your use of registerClass:forCellReuseIdentifier: then dequeueReusableCellWithIdentifier: can return nil and you can create a new cell and initialize it properly in your if statement:
if (!cell) {
cell = [[ChatMessageTableViewCell alloc] init...]; // use proper init method
// setup cell as needed for first time
}
Instead you could perform the one time actions inside the initialiser of the ChatMessageTableViewCell class. That way you could keep the behaviour you are used to with registering.
I would like to customize a simple UITableViewCell so that I run the customization only once and add values (e.g., cell title) later. My app's cell is more complex - it has subviews and uses auto layout; however, a simple example, I believe, will help in focusing on the objective.
I am using iOS 8, Xcode 6.X, Objective-C and Nibs (no storyboard) to keep it simple. I have not created a custom class for UITableViewCell. Instead, I have the following code:
- (void)viewDidLoad {
[super viewDidLoad];
//[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:#"Cell"];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1; //FIXED VALUE FOR EXAMPLE'S SAKE
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 3; //FIXED VALUE FOR EXAMPLE'S SAKE
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSLog(#"tableView:cellForRowAtIndexPath:");
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
//UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
if (cell == nil) {
NSLog(#"cell == nil");
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
//CUSTOMIZING CELL THAT I WANT TO RUN ONLY ONCE
cell.backgroundColor = [UIColor redColor];
}
NSArray *numbersArray = #[#1,#2,#3];
cell.textLabel.text = [NSString stringWithFormat:#"%#", numbersArray[indexPath.row]];
return cell;
}
Which outputs:
tableView:cellForRowAtIndexPath:
cell == nil
tableView:cellForRowAtIndexPath:
cell == nil
tableView:cellForRowAtIndexPath:
cell == nil
FIRST QUESTION: Why is cell == nil run 3 times? It seems wasteful to run the customization code cell.backgroundColor = [UIColor redColor]; 3 times.
Now, when I enable:
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:#"Cell"];
And use:
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
Instead of:
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
I get the output:
tableView:cellForRowAtIndexPath:
tableView:cellForRowAtIndexPath:
tableView:cellForRowAtIndexPath:
SECOND QUESTION: Why isn't cell == nil run at all?
FINAL QUESTIONS: How can I make cell == nil run only once so that I format the UITableViewCell only once? Is there a better way to customize a simple cell, running the customization code only once?
Why is cell == nil run 3 times? It seems wasteful to run the customization code cell.backgroundColor = [UIColor redColor]; 3 times.
The table view most likely displays three cells at once, hence requiring three distinct cell objects.
Why isn't cell == nil run at all?
The documentation states that -dequeueReusableCellWithIdentifier:forIndexPath: always returns a valid cell if you registered the identifier previously. It basically takes care of checking if a new cell is required for you.
How can I make cell == nil run only once so that I format the UITableViewCell only once?
You don't. You will have to customize every single instance. I would recommend to use a custom subclass though, rather then messing with UITableViewCell from the outside.
The best way to do this, is to create a custom class for your cell, and do any customization that isn't dependent on the indexPath there. Usually, I do this in initWithCoder or awakeFromNib. You should register the nib in viewDidLoad; I don't see anything wrong with the code you mention in your comment to Christian's answer, unless the name of the file is wrong. It really isn't the view controller's business to be adding subviews or customizing your cell; that code belongs in the cell's class.
BTW, this doesn't keep the customization code from running multiple times. It needs to run once for each cell instance that you create, just like it does in your original code. The number of cells created will be equal to the number that fit on the screen at one time (plus one maybe).
I want to alter the font size and color etc. for my UITableView cells. I've designed the cells custom in Xcode and got everything working.
First of I'll post my code here:
UITableViewController:
- (void)viewDidLoad
{
[super viewDidLoad];
[self.tableView registerClass:MainCategoryTableViewCell.class forCellReuseIdentifier:#"MainCategoryCell"];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"MainCategoryCell";
MainCategoryTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
return cell;
}
And my custom cell:
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.title.font = [Theme tableCellTitleFont];
self.title.textColor = [Theme tableCellTitleColor];
self.subcategories.font = [Theme tableCellSubTitleFont];
self.subcategories.textColor = [Theme tableCellSubTitleColor];
self.costs.font = [Theme tableCellValueFont];
self.costs.textColor = [Theme tableCellValueColor];
}
return self;
}
I'm confused now how this dequeue works:
As far as I understood if I register the class in the viewDidLoad, the initWithStyle method of the cell gets ONLY called, when theres no cell for reuse. If theres a cell for reuse it will be used. I've seen a lot of if(cell == nil) calls in other code snippets but is that really necessary? I thought the registerClass method takes care of that anyway?
And at the moment my cells will be displayed completely empty. Before I registered the class everything worked, however the initWithStyle didn't get called..
Complete cellForRowAtIndexPathMethod:
#pragma mark Delegate methods
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"MainCategoryCell";
MainCategoryTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
// Configure the cell...
MainCategory *mainCategory = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.title.text = mainCategory.name;
cell.subcategories.text = [NSString stringWithFormat:#"%i subcategories", [[mainCategory getNumberOfSpendingCategories] integerValue]];
cell.costs.text = [[mainCategory getMonthlyCostsOfAllSpendingCategories] getLocalizedCurrencyString];
if(!mainCategory.icon){
cell.icon.image = [UIImage imageNamed:#"DefaultIcon.png"];
} else {
cell.icon.image = [UIImage imageNamed:mainCategory.icon];
}
if(!mainCategory.color){
cell.backgroundColor = [PresetColor colorForPresetColor:PresetColorsWhite];
} else {
cell.backgroundColor = [PresetColor colorForPresetColor:(PresetColors)[mainCategory.color intValue]];
}
cell.cellBackground.image = [[UIImage imageNamed:#"content-bkg"] resizableImageWithCapInsets:UIEdgeInsetsMake(10, 10, 10, 10)];
return cell;
}
If you have defined the cell as "prototype cell" for the table view in the xib/storyboard file, then you don't have to register it at all. If the custom cell is in a separate nib file, you register the custom cell with registerNib, not registerClass. For example:
[self.tableView registerNib:[UINib nibWithNibName:#"MainCategoryTableViewCell" bundle:nil]
forCellReuseIdentifier:#"MainCategoryCell"];
For cells instantiated from a nib file, initWithCoder is called, not initWithStyle.
To configure any outlets of your custom cell, override awakeFromNib. The connections are
not yet established in initWithCoder.
For best understanding see the below image for just a deque reference.
Deque means you can add and delete cells from both the ends.
By ends I mean up and down.
Lets say you have 4 cell containg Acell,Bcell,Ccell and Dcell and height for row is for three cells.
so at a time only 3 cells would be visible.
when you scroll to see the Dcell , Acell would become as invisible row and memory for it will be reused for Dcell.
In the same way when you scroll to see the Acell , Dcell would become as invisible row and memory for it will be reused for Acell.
It says clearly in documentation
dequeueReusableCellWithIdentifier:forIndexPath:
For performance reasons, a table view's data source should generally
reuse UITableViewCell objects when it assigns cells to rows in its
tableView:cellForRowAtIndexPath: method. A table view maintains a
queue or list of UITableViewCell objects that the data source has
marked for reuse. Call this method from your data source object when
asked to provide a new cell for the table view. This method dequeues
an existing cell if one is available or creates a new one based on the
class or nib file you previously registered.
.
dequeueReusableCellWithIdentifier:
Return Value : A UITableViewCell object with the associated identifier
or nil if no such object exists in the reusable-cell queue.
Discussion : For performance reasons, a table view's data source
should generally reuse UITableViewCell objects when it assigns cells
to rows in its tableView:cellForRowAtIndexPath: method. A table view
maintains a queue or list of UITableViewCell objects that the data
source has marked for reuse. Call this method from your data source
object when asked to provide a new cell for the table view. This
method dequeues an existing cell if one is available or creates a new
one using the class or nib file you previously registered. If no cell
is available for reuse and you did not register a class or nib file,
this method returns nil.
If you registered a class for the specified identifier and a new cell
must be created, this method initializes the cell by calling its
initWithStyle:reuseIdentifier: method. For nib-based cells, this
method loads the cell object from the provided nib file. If an
existing cell was available for reuse, this method calls the cell’s
prepareForReuse method instead.
Before introducing storyboard.The tableview checks the returned cell which can be nil .So if nil we must reallocate the cell and tehn initialize and provide the cell in the datasource method
I get the following error:
* Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'unable to dequeue a cell
with identifier FontCell - must register a nib or a class for the
identifier or connect a prototype cell in a storyboard'
I'm not sure exactly what I'm doing wrong. I set the cell identifier (programmatically, as it's not created through Interface Builder) and do everything I thought I was supposed to do in the delegate methods, but I'm still getting that error when I try to get the UITableView to load.
Here's the relevant code (it's worth noting I've subclassed UITableViewCell for customization options):
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.fonts.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifier = #"FontCell";
FontCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];
if (!cell) {
cell = [[FontCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"FontCell"];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
int row = indexPath.row;
cell.fontFamilyLabel.text = self.fonts[row];
return cell;
}
And here's the only method I changed in my subclassed UITableViewCell (FontCell):
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.fontFamilyLabel = [[UILabel alloc] initWithFrame:CGRectMake(5, 5, 200, 20)];
self.fontFamilyLabel.textAlignment = NSTextAlignmentCenter;
[self.contentView addSubview:self.fontFamilyLabel];
}
return self;
}
What exactly am I doing wrong?
Easiest fix is to just change it to FontCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; As with your current code, you'll have to check to be sure cell is not nil if you do this method.
Alternately, you can register a UINib or Class at the table level that is tied to #"FontCell"
For example (up in viewDidLoad):
[self.tableView registerClass: [FontCell class] forCellReuseIdentifier:#"FontCell"];
Then you can do
FontCell *cell = [tableView dequeueReusableCellWithIdentifier:#"FontCell" forIndexPath:indexPath];
The nice thing with this method is that you know that your cell will never be nil, so you can just immediately begin modifying it.
You're using the dequeueReusableCellWithIdentifier:forIndexPath: method. The documentation for that method says this:
You must register a class or nib file using the registerNib:forCellReuseIdentifier: or registerClass:forCellReuseIdentifier: method before calling this method.
So here
[self.tableView registerClass: [FontCell class] forReuseIdentifier: #"FontCell"];
I also had such a problem, and the solution I found is:
Go to the Project Navigator and select “ViewController.h”. Append
<UITableViewDelegate, UITableViewDataSource>
after “UIViewController”.
If using a tableview (not a table view controller) as I was, there are not cells that appear by default.
In the storyboard select the tableview
Open the Attributes inspector
Change prototype cells from 0 to 1
Select the newly displayed table cell
In the Attributes inspector set the Identitifier to "FontCell"
I am trying to create a "settings" table view for my app. I am trying to mimic it to be the same style as the gneral setting on an Iphone. I have created my own custom cell class by inheriting from UITableCell. I gave it the appropriate IBOulets and i have hooked them up in the storyboard. I also hooked up the switch to my tableViewControler, but for some reason my code is only returning me one empty cell (it being only one cell is not an issue atm for that's all i have in my setting). I triple checked and made sure that I'm using the same cell identifier in my code and in storyboard. Anyone know why I'm getting a blank cell back?
Here is my .h file for my custom cell.
#interface NHPSettingsCell : UITableViewCell
#property (nonatomic,weak) IBOutlet UILabel *settingLabel;
#property (nonatomic,strong) IBOutlet UISwitch *settingSwitch;
#end
MY Problem code is here, my .h file for the custom cell:
#import "NHPSettingsCell.h"
#implementation NHPSettingsCell
#synthesize settingLabel, settingSwitch;
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
// Initialization code
}
return self;
}
- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
[super setSelected:selected animated:animated];
// Configure the view for the selected state
}
#end
My method for drawing the cell in my custom view controller:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"SettingsCell";
NHPSettingsCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[NHPSettingsCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
cell.selectionStyle = UITableViewCellStyleDefault;
}
cell.settingLabel.text = #"firstSetting";
//check the if user wants promt of disclaimer each time or not.
if([prefs boolForKey:#"firstSetting"] == YES){
cell.settingSwitch.on = YES;
}else{
cell.settingSwitch.on = NO;
}
return cell;
}
Now the thing that annoys me is i have successfully managed to implement the cellForRowAtIndexPath method for a dynamic table that uses custom cells. I have also implements the code for a static table using the default cell, but for a static table with custom cells it just doesn't seem to work. Here is the code on how I implemented my custom cells on a dynamic table (note how i didn't have to init the cells but it works).
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"InteractionResultCell";
NHPResultCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
// Configure & Fill the cell
cell.leftLabel.text = [[resultsList objectAtIndex:indexPath.row] substanceName];
cell.rightLabel.text = [[resultsList objectAtIndex:indexPath.row] substanceName2];
NSString *color = [NSString stringWithFormat:#"%#", [[resultsList objectAtIndex:indexPath.row] color]];
//Change a hex value to a readable 0x number to pass ot hte macro so we can go from a hex color to a RGB system.
NSScanner *scanner;
unsigned int tempint=0;
scanner = [NSScanner scannerWithString:color];
[scanner scanHexInt:&tempint];
cell.severityButton.backgroundColor = UIColorFromRGB(tempint);
return cell;
}
Two problems:
If you are using static cells, do not implement any datasource methods in your view controller (numberOfRows, numberOfSections, cellForRow...) as this will override what you have built in the storyboard. The table has the sections, rows and content you give it in the storyboard.
Cells loaded from the storyboard (either dynamic prototypes, or static cells) are initialised using initWithCoder:, not initWithStyle:. awakeFromNib: is a better place to put your set up code.
dequeueReusableCellWithIdentifier: works only if a cell has already been created to prevent repeated memory allocations. You cannot reuse a cell without creating it first. The static cells created in the xib are the default type. That's why it doesn't work for static table with custom cells. Add the cell creation code after reuse as you've done in your custom view controller's cellForRowAtIndexPath: method:
if (cell == nil) {
cell = [[NHPSettingsCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
cell.selectionStyle = UITableViewCellStyleDefault;
}
EDIT- To init your custom cell, you'll have to load from xib. Add the following class method to your NHPSettingsCell.m:
+(NHPSettingsCell*) createTextRowWithOwner:(NSObject*)owner{
NSArray* wired = [[NSBundle mainBundle] loadNibNamed:#"NHPSettingsCell" owner:owner options:nil];
NHPSettingsCell* cell = (NHPSettingsCell*)[wired firstObjectWithClass:[NHPSettingsCell class]];
return cell;
}
and then call it from your custom view controller as:
cell = (NHPSettingsCell*)[tableView dequeueReusableCellWithIdentifier: CellIdentifier];
if (Nil == cell) {
cell = [NHPSettingsCell createTextRowWithOwner:self];
}