I have been wondering why my code works well with cellForItemAtIndexPath: & not with dequeueReusableCellWithReuseIdentifier: while fetching collection view cells.
Here is my code:
This one works Fine:
NSInteger numberOfCells = [self.collectionView numberOfItemsInSection:0];
for (NSInteger i = 0; i < numberOfCells; i++) {
myCustomCollectionCell *cell = (myCustomCollectionCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:i inSection:0]];
//here I use the cell..
}
While this compiles well, but not working (the changes I perform on cell is not depicted)
NSInteger numberOfCells = [self.collectionView numberOfItemsInSection:0];
for (NSInteger i = 0; i < numberOfCells; i++) {
myCustomCollectionCell *cell = [self.collectionView dequeueReusableCellWithReuseIdentifier:#"myCell"forIndexPath:[NSIndexPath indexPathForItem:i inSection:0]];
//here I use the cell..
}
Tried this too, but no use:
NSInteger numberOfCells = [self.collectionView numberOfItemsInSection:0];
for (NSInteger i = 0; i < numberOfCells; i++) {
myCustomCollectionCell *cell = (myCustomCollectionCell *)[self.collectionView dequeueReusableCellWithReuseIdentifier:#"myCell"forIndexPath:[NSIndexPath indexPathForItem:i inSection:0]];
//here I use the cell..
}
Any ideas?
These two are basically two very different methods.
dequeReusableCellWithReuseIdentifier:
Suppose that you have a list of articles to view. Say you have 50 articles. The screen won't show you all 50 articles on screen at once. It will show you limited cells at once, based on the height you given to the rows. Lets say the screen shows only 5 articles at once and now you are at the top of the list. The list will show items 1-5. Now when you scroll, inorder to display the 6th item, the list reuses your 1st cell, configures it for the 6th one and display it. By this time your 1st cell is out of the view.
2.cellForRowAtIndexPath :
On the other hand cellForRowAtIndexPath returns you the cell which is already in the view or from IndexPath you provide. In this case if the cell is already in the memory, it will just return that or it will configure a new cell and return.
The example below is for UITableViews, but UICollectionViews can be handled in the same way.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
/*
* This is an important bit, it asks the table view if it has any available cells
* already created which it is not using (if they are offscreen), so that it can
* reuse them (saving the time of alloc/init/load from xib a new cell ).
* The identifier is there to differentiate between different types of cells
* (you can display different types of cells in the same table view)
*/
UITableViewCell *cell = [tableView dequeueReusableCellWithReuseIdentifier:#"MyIdentifier"];
/*
* If the cell is nil it means no cell was available for reuse and that we should
* create a new one.
*/
if (cell == nil) {
/*
* Actually create a new cell (with an identifier so that it can be dequeued).
*/
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#"MyIdentifier"] autorelease];
}
/*
* Now that we have a cell we can configure it to display the data corresponding to
* this row/section
*/
//Configure the cell here..
/* Now that the cell is configured we return it to the table view so that it can display it */
return cell;
}
Let me know if you were still unclear.
They are different things:
cellForItemAtIndexPath gets an already-populated cell from the collection view.
dequeueReusableCellWithReuseIdentifier will possibly return a cell that can be re-used, when populating a new cell. It's designed to help reduce the number of cell objects in existence. If it fails (and it often will) then a new cell must be created explicitly.
Related
I have created custom cells in my app.I want to get the each cell in HeightForRowAtIndexPath.Please tell me how can i get the custom cell in this method.I have tried this code but this causes infinite loop & finally crash the app.
HomeCell *cell=(HomeCell *)[tableView cellForRowAtIndexPath:indexPath];
EDIT:
I Have tried this but it gives me cell height as zero.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = #"HomeCell";
HomeCell *cell = (HomeCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier];
float tv_view_height=cell.tv_post.frame.size.height;
float like_count_height=cell.label_like_count.frame.size.height;
float first_comment_height=cell.first_comment.frame.size.height;
float second_comment_height=cell.second_cmment.frame.size.height;
float third_comment_height=cell.third_comment.frame.size.height;
Post *user_post=[arr_post objectAtIndex:indexPath.row];
float comment_count=[user_post.comment_count intValue];
if(comment_count<=0)
{
first_comment_height=0;
second_comment_height=0;
third_comment_height=0;
}
else if(comment_count==1)
{
second_comment_height=0;
third_comment_height=0;
}
else if(comment_count==2)
{
third_comment_height=0;
}
float like_count=[user_post.like_count intValue];
if(like_count<=0)
{
like_count_height=0;
}
float total_height=tv_view_height+like_count_height+first_comment_height+second_comment_height+third_comment_height;
NSLog(#"total heigh is %f'",total_height);
return total_height;
}
Please tell which is the best way?
How to get cell in heightForRowAtIndexPath?
It's impossible, because when -heightForRowAtIndexPath is called, no cells are created yet. You need to understand how the UITableView works:
UITableView asks it's datasource how many sections it will have
-numberOfSectionsInTableView
At this point there are no cells created.
UITableView asks it's datasource how many rows each section will have
-numberOfRowsInSection
At this point there are no cells created.
UITableView asks it's delegate height of each visible row, to know where cells will be located
-heightForRowAtIndexPath
At this point there are no cells created.
UITableView asks it's datasource to give it a cell to display at given index path
-cellForRowAtIndexPath
At this point the cell is created.
The height of each cell you can calculate from data model. You don't need the cell – you already know the frame width that will contain a comment, you know it's content, you know it's font, you know linebreak mode, etc. So, you can calculate height. For example:
CGFloat commentsHeight = 0;
Post *user_post = [arr_post objectAtIndex:indexPath.row];
for (NSString *comment in user_post.comments)
{
CGRect commentrect = [comment boundingRectWithSize:CGSizeMake(self.view.bounds.size.width - 18, FLT_MAX)
options:(NSStringDrawingUsesLineFragmentOrigin)
attributes:#{NSFontAttributeName:[UIFont systemFontOfSize:15]}
context:nil];
commentsHeight += commentrect.size.height;
}
And you can calculate height of the other components of cell from its data model.
But now, in 2015, it's not the best way. You really would be better to read the tutorials, which showed #Zil, and do it with Autolayout.
You should declare an array for storing TableView cells in cellForRowAtIndexPath and you can use stored cells in heightForRowAtIndexPath. Lets Try using this.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = #"HomeCellID";
HomeCell *cell = (HomeCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (!cell) {
cell = [[[HomeCell alloc] init] autorelease];
}
// Store table view cells in an array
if (![tableViewCells containsObject:cell]) {
[tableViewCells addObject:cell];
}
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if([tableViewCellsArray objectAtIndex:indexPath.row]) {
HomeCell *cell = (HomeCell *)[tableViewCells objectAtIndex:indexPath.row];
// Process your Code
}
return yourCalculatedCellHeight;
}
I would recommend you to take the height form a configuration collection on your viewController.
Something like this:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
CGFloat height;
CellConfiguration * selectedCellConfiguration =[_cellConfigurations objectAtIndex:indexPath.row];
switch (selectedCellConfiguration.type) {
case TheTypeYouNeed:
return TheValueYouNeed
default:
height = 44.0f;
break;
}
return height;
}
You could create a new cell from scratch, simply by HomeCell *sexyCell = [[HomeCell alloc]init];
or dequeue one like you did in cellForRow (tableView.dequeueWithReuseIdentifier:).
Though I advise creating one from scratch and disposing it after (setting it to nil), because if you dequeue it there they'll go in queue and cause heavy memory leaks and end up with many cells for the same indexPath.
What you COULD do is the following :
Create a cell with alloc init
Fill it with the real data
use .layoutsubviews on its view
calculate it's size and apply it to your real cell
What you SHOULD do :
Use auto layout and add all the constraints that are necessary, all your labels will size dynamically. It takes about 3 or 4 hours to get the basics of Auto layout, and about a month of regular use to really get the hang of it with ease.
I strongly strongly strongly suggest you do NOT resize using the frame of objects, most labels and views will resize like they should without having to write any code if you use constraints properly.
Once you have done that, because you have cells of varying heights, is using the DynamicHeight property of the tableview and the slight adjustements that comes with it. You can find
A great tutorial here
The same updated tutorial for swift (more up to date but you'd need to translate)
This amazing StackOverflow answer which you MUST read
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:indexpath.row inSection:0];
Custom Cell *cell = [tableview cellForRowAtIndexPath:indexPath];
By this , you will get each cell in your method
Here is the cellForItemAtIndexPath code I have:
static NSString *cellIdentifier = #"Cell";
MyCustomCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];
//do stuff, set label text
if (indexPath.section == 0) cell.label.text = [words1 objectAtIndex:indexPath.row];
else if (indexPath.section == 1) cell.label.text = [words2 objectAtIndex:indexPath.row];
else if (indexPath.section == 2) cell.label.text = [words3 objectAtIndex:indexPath.row];
return cell;
I register the cell type in viewDidLoad:
[_collectionView registerClass:[MyCustomCell class] forCellWithReuseIdentifier:#"Cell"];
I have my collection view setup in my storyboard with delegates and datasource linked up as normal.
Now my issue... I have two approaches, both with problems. I have 3 sections, each drawing from a unique array of data.
After I have fetched the required data, I use [_collectionView reloadData]. This loads the correct number of cells, as per the array count, but the cells offscreen are not populated. So when I scroll them onscreen, they have not has their label set.
Instead of the above, I try reloading each section as the data is fetched (a preferred route) using the following code (once per section):
[self.collectionView performBatchUpdates:^{
[self.collectionView reloadSections:[NSIndexSet indexSetWithIndex:0]];
} completion:nil];
However, the problem using this method, is that after it has reloaded, it fills my collection view with the correct number of cells, but the cells themselves are blank, no text set on the labels.
In both scenarios, if I scroll the non-loading cells onscreen, offscreen and back on again, they will load. But the first time you see them, they don't.
Resolved. Placing your UI code, such as creating UILabel (not nil) inside the layoutSubviews method is bad.
Moving this code to the init method on my custom UICollectionViewCell subclass, fixed my problem if cell UI not consistently.
OK, I’ve got a table with 3 different prototype cells (cellVaccination, cellAdmin, cellExpire). In my cellForRowAtIndexPath method, I’m splitting up a Core Data object across those 3 individual cells so that structurally the table will look like the following:
- Drug 1
- Drug 1 Admin
- Drug 1 Expire
- Drug 2
- Drug 2 Admin
- Drug 2 Expire
- Drug 3
- Drug 3 Admin
- Drug 3 Expire
Additionally, I’ve programmatically added a UISwitch into the ‘top level’ cell (i.e. Drug 1) so that the switch might control the secondary cells features (i.e. color, text, etc). Here is what my current cellForRowAtIndexPath looks like:
- (VaccineTableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// we need to adjust the indexPath because we split a single core data object into 3 different rows
NSIndexPath *adjustedIndexPath = [NSIndexPath indexPathForRow:indexPath.row / 3 inSection:indexPath.section];
Vaccine *vaccine = [self.fetchedResultsController objectAtIndexPath:adjustedIndexPath];
// define the switch that will get added to the primary table rows
UISwitch *switchview = [[UISwitch alloc] initWithFrame:CGRectZero];
if (indexPath.row % 3 == 0) {
static NSString *cellIdentifier = #"cellVaccination";
VaccineTableViewCell *cell = [myTableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];
cell.vaccineName.text = vaccine.vaccineName;
// add a switch into that table row
cell.accessoryView = switchview;
[switchview addTarget:self action:#selector(switchChanged:) forControlEvents:UIControlEventValueChanged];
switchview.tag = indexPath.row;
switchview.on = [vaccine.vaccineEnabled boolValue];
// PROBLEM AREA BELOW
if (switchview.on) {
VaccineTableViewCell *cell1 = [myTableView dequeueReusableCellWithIdentifier:#"cellAdmin" forIndexPath:[NSIndexPath indexPathForItem:indexPath.row + 1 inSection:0]];
cell1.vaccineAdmin.textColor = [UIColor redColor];
cell1.vaccineAdminDate.textColor = [UIColor redColor];
NSLog(#"Row %d is %#", indexPath.row, switchview.on ? #"ON" : #"OFF");
} else {
VaccineTableViewCell *cell1 = [myTableView dequeueReusableCellWithIdentifier:#"cellAdmin" forIndexPath:[NSIndexPath indexPathForItem:indexPath.row + 1 inSection:0]];
cell1.vaccineAdmin.textColor = [UIColor lightGrayColor];
cell1.vaccineAdminDate.textColor = [UIColor lightGrayColor];
NSLog(#"Row %d is %#", indexPath.row, switchview.on ? #"ON" : #"OFF");
}
//
return cell;
}
else if (indexPath.row % 3 == 1) {
VaccineTableViewCell *cell = [myTableView dequeueReusableCellWithIdentifier:#"cellAdmin" forIndexPath:indexPath];
cell.vaccineAdminDate.text = vaccine.vaccineAdmin;
return cell;
}
else if (indexPath.row % 3 == 2) {
VaccineTableViewCell *cell = [myTableView dequeueReusableCellWithIdentifier:#"cellExpire" forIndexPath:indexPath];
cell.vaccineExpireDate.text = vaccine.vaccineExpire;
return cell;
}
else {
// do nothing at the moment
}
}
The problem I’m having seems to stem around the area notated within the “Problem Area Below” element, more specifically I’m guessing with the dequeueReusableCellWithIdentifier. In theory, what’s supposed to happen is that when the cells are first populated via the Core Data objects, I want to test whether or not the switch is either “on” or “off” and adjust a parameter (such as color) appropriately so that, without any other interaction, the respective rows are colored appropriately.
What’s happening is this - let’s assume that I’m simulating on an iPhone 4S and that the screen is displaying 4 row sets, or 12 rows total (4 rows of 3 different prototypes). And let’s also assume that the first 2 are switched ON and the second 2 are switched OFF, again driven directly from Core Data. Initially, the screen will look correct, the first two items have been colored red, and the next two items have been colored gray. However when I start scrolling my table up, the NEXT two (that were off the screen) are colored red, and so the pattern continues. Oddly, when NSLog returns the Row identifiers (seen within that “problem area” section) everything looks like it’s identifying the correct rows, but apparently it’s not, i.e.:
vaccinations[10952:1486529] Row 0 is ON
vaccinations[10952:1486529] Row 3 is ON
vaccinations[10952:1486529] Row 6 is OFF
vaccinations[10952:1486529] Row 9 is OFF
vaccinations[10952:1486529] Row 12 is OFF
vaccinations[10952:1486529] Row 15 is OFF
I believe it has something to do with the dequeueReusableCellWithIdentifier method, however why would the NSLog identify the rows correctly, but the changing of the colors not hit the correct rows?
You have references to cell1 in which you dequeue a cell for a different NSIndexPath, configure the color of that cell, and then discard this cell. I'm guessing that you are trying to adjust the visual appearance of a different cell (the next cell).
That is not correct. The cellForRowAtIndexPath should be adjusting the state of the current cell only. If you want to adjust the appearance of cellAdmin cell, you should do that within the if (indexPath.row % 3 == 1) ... block.
So the if (indexPath.row % 3 == 0) block will look up in the model to determine if the switch is on or off. The if (indexPath.row % 3 == 1) block will look up in the model to determine what color the text should be.
But cellForRowAtIndexPath should not be trying to adjust the appearance of another cell. You have no assurances of what order these will be instantiated (and it may vary depending upon whether your scrolling, from which direction, etc.).
If one did want to update another cell that is visible, dequeueReusableCellWithIdentifier is not the correct method, regardless. Instead, one would use [tableView cellForRowAtIndexPath:] which retrieves the cell for a currently visible cell (and must not to be confused with the similarly named UITableViewDataSource method). But you would never do that in this context because you don't know if that other cell had been loaded or not. (I actually think it's a bad practice in general to update another cell in any context, a violation of the separation of responsibilities.)
I want to make cells overlapping.
What I did is
Adjust the tableview's contentSize:
int count = [self.tableView numberOfRowsInSection:0];
self.tableView.contentSize = CGSizeMake(self.view.frame.size.width ,(100 * count - 30 * (count - 1)));
And set the cellForRowAtIndexPath :
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"Cell"];
cell.contentView.backgroundColor = [UIColor orangeColor];
}
if (indexPath.row != 0) {
UITableViewCell *currentCell = [tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row + 1 inSection:0]];
[currentCell setFrame:CGRectMake(0, - 30, self.view.frame.size.width, 100)];
}
return cell;
}
But when I test, this is not working.
Any help? Thanks.
What is happing with your code is that the values that you are manually setting inside tableView:cellForRowAtIndexPath: are getting overwritten by the UITableViewController layout methods.
You are not supposed to edit the frame of a table view cell directly since this is an attribute that is set by UITableViewController internally. You should only interact with this by using methods of the UITableViewDelegate protocol. (tableView:heightForRowAtIndexPath:, ...)
IMHO the best way (cleanest one) to implement what you are describing is to subclass UICollectionView instead of UITableView and define a custom layout.
However if you wanna go the hacky way you can achieve an apparent row overlapping in many different ways.
Just as an example, you could create two different row types, one with height 30 and one with height 70, where the first one represents the overlapping part of the row and the second one represents the not-overlapping part of the row. Then use the first type for even rows and the second one for odd rows.
Hope this helps.
p.s. i'm really sorry if my english is not the best
Then I get json update I reload my tableView, but I have tableView with collectionView inside it and I have little lag in visible cell at the moment of reload. I want to reload only invisible cell of tableView. But there is no 'visibleSections' method like 'visibleCells'.
I get all visible sections
NSArray *visibleSections = [[self.tableView indexPathsForVisibleRows]valueForKey:#"section"];
Not I want get all sections, then remove all visibleSections and reload this new array. But I can not find the way to get all sections. How can I do it?
You could use one of these methods:
- (void)reloadRowsAtIndexPaths:withRowAnimation:
- (void)reloadSections:withRowAnimation:
Edit
When you try to get the indexPaths of all invisible cells, you could try something like this:
NSMutableArray *invisibleCells = [NSMutableArray array];
for (int i = 0; i < [tabsTableView numberOfSections]; i++) {
for (int n = 0; n < [tabsTableView numberOfRowsInSection:i]; n++) {
for (NSIndexPath *indexPath in [tabsTableView indexPathsForVisibleRows]) {
if ([indexPath section] == i && [indexPath row] == n) {
goto nextIteration;
}
}
[invisibleCells addObject:[NSIndexPath indexPathForRow:n inSection:i]];
nextIteration:;
}
}
More important is why you would like to do that?! Due UITableViewCells get reused, they won't be updatable. When the cell is "invisible", it changes its content and is used at a different position within the visible cells. This is how UITableView works.
You will (also currently) use this method to set the content of the cell:
- (void)tableView:tableView cellForRowAtIndexPath:indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:_cellID];
if (!cell) {
cell = (UITableViewCell *)[[[NSBundle mainBundle] loadNibNamed:#"cellTemplate" owner:nil options:nil] objectAtIndex:0];
}
// configure cell content here
}
When the cell get's reused, this method gets called and you create the cell only if UITableView does not reuse an invisible cell. If it does, you change the content.
So there is no need to reload invisible cells and it's impossible. From the docs:
Return Value
An object representing a cell of the table or nil if the cell is not visible or indexPath is out of range.