cellForRowAtIndexPath doesn't get called after calling reloadData in UICollectionView - ios

I have a UICollectionView which uses a subclass of UICollectionViewCell's. Initially, my dataSource contains 5 items, and when the user scrolls down, I fetch more data and add them to my dataSource, then I call reloadData.
But only 3 items are visible. and when I scroll up I can't see the rest of the items, just an empty area. I noticed that cellForRowAtIndexPath only gets called for those 3 items.
When I navigate to my parent view, and back to the view which contains my UICollectionView I can see all the items.
Note: I have implemented layout:sizeForItemAtIndexPath function as each cell has a different size.
Edit:
I partially solved the issue. I had a refreshControl and I called endRefreshing in a background thread.
Am adding images for a better demonstration of what's happening now:
The 1st image is before fetching new data, as u can see the data is displayed perfectly.
The 2nd image is after fetching new data, as u can see the new items take the exact height of the previous ones (the old cells) and there is an empty area, when I scroll down I can see the rest of the data, and when I scroll back up, the top cells get the right height, as shown in the 3rd image.
After I finish loading new items, I call this method
- (void)updateDataSource
{
self.collectionViewDataSource = _manager.messages;
[self.collectionView reloadData];
}
I checked numberOfItemsInSection method, it returns the right number of items.
and here is my layout:sizeForItemAtIndexPath
- (CGSize)collectionView:(UICollectionView *)collectionView layout: (UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath: (NSIndexPath *)indexPath
{
// Here I am calculating the width and height of the textView which will fit the message
SPH_PARAM_List *feed_data=[[SPH_PARAM_List alloc]init];
feed_data=[self.collectionViewDataSource objectAtIndex:indexPath.row];
if ([feed_data.chat_media_type isEqualToString:kSTextByme]||[feed_data.chat_media_type isEqualToString:kSTextByOther])
{
NSAttributedString *aString = [[NSAttributedString alloc] initWithString:feed_data.chat_message];
UITextView *calculationView = [[UITextView alloc] init];
[calculationView setAttributedText:aString];
[calculationView setFont:[UIFont systemFontOfSize:14]];
[calculationView setTextAlignment:NSTextAlignmentJustified];
CGSize sc = [calculationView sizeThatFits:CGSizeMake(TWO_THIRDS_OF_PORTRAIT_WIDTH, CGFLOAT_MAX)];
NSLog(#"IndexPath: %li Height: %f", (long)indexPath.row ,sc.height);
return CGSizeMake(self.view.frame.size.width - (5 * 2), sc.height);
}
return CGSizeMake(self.view.frame.size.width - (5 * 2), 90);
}
Edit 2:
I noticed that layout:collectionViewLayoutsizeForItemAtIndexPath: gets called and it returns the right height, but cellForItemAtIndexPath still deals with an old one.

You likely need to invalidate your layout so that the cell positions and heights get recalculated since simply reloading the data won't properly set the cell heights (since cells are reused).
- (void)updateDataSource
{
self.collectionViewDataSource = _manager.messages;
[self.collectionView reloadData];
// Add this line.
[self.collectionView.collectionViewLayout invalidateLayout];
}

Can't figure it out until you show code. However, I guess your data source is not updated. Just check it out, your DataSource should have fetched the data before reloadData. You can put Some log to check number of items before/after reloadData. This might help.

Related

UITableViewCell needs reloadData() to resize to correct height

I have the a custom tableview cell with applied constraints but the first time the table is displayed the row height is not resized properly unless new cells are created, is there a way to do this without calling reloadData again?
Yes. This is actually an issue with self-sizing that you need to work around until it is fixed.
The problem is that when a cell is instantiated, its initial width is based on the storyboard width. Since this is different from the tableView width, the initial layout incorrectly determines how many lines the content actually would require.
This is why the content isn't sized properly the first time, but appears correctly once you (reload the data, or) scroll the cell off-screen, then on-screen.
You can work around this by ensuring the cell's width matches the tableView width. Your initial layout will then be correct, eliminating the need to reload the tableView:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
TableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
[cell adjustSizeToMatchWidth:CGRectGetWidth(self.tableView.frame)];
[self configureCell:cell forRowAtIndexPath:indexPath];
return cell;
}
In TableViewCell.m:
- (void)adjustSizeToMatchWidth:(CGFloat)width
{
// Workaround for visible cells not laid out properly since their layout was
// based on a different (initial) width from the tableView.
CGRect rect = self.frame;
rect.size.width = width;
self.frame = rect;
// Workaround for initial cell height less than auto layout required height.
rect = self.contentView.bounds;
rect.size.height = 99999.0;
rect.size.width = 99999.0;
self.contentView.bounds = rect;
}
I'd also recommend checking out smileyborg's excellent answer about self-sizing cells, along with his sample code. It's what tipped me off to the solution, when I bumped into the same issue you are having.
Update:
configureCell:forRowAtIndexPath: is an approach Apple uses in its sample code. When you have more than one tableViewController, it is common to subclass it, and break out the controller-specific cellForRowAtIndexPath: code within each view controller. The superclass handles the common code (such as dequeuing cells) then calls the subclass so it can configure the cell's views (which would vary from controller to controller). If you're not using subclassing, just replace that line with the specific code to set your cell's (custom) properties:
cell.textLabel.text = ...;
cell.detailTextLabel.text = ...;

How is the Default UITableView Scrolling Supposed to Work when Items are Deleted and Inserted Programatically?

I have a UITableView with two sections. Items can move from one section to another, as seen in the image below.
The basic interaction is as follows:
you swipe a cell/row
that cell is removed from its section
that cell is added to the bottom of the other section
Note, I'm using a sequence of deleteRowsAtIndexPaths and insertRowsAtIndexPaths with CATransactions. Remove followed by add isn't the only table operation in place; for the general approach I want chained insert/delete/update animations to start when the preceding operation has finished. Here's an example:
[CATransaction begin];
[CATransaction setCompletionBlock:^{
[table beginUpdates];
// update data source...
// insertRows...
[table endUpdates];
}];
[table beginUpdates];
// update data source...
// deleteRows...
[table endUpdates];
[CATransaction commit];
When there are enough items in the table view to take up more than a screen's worth of space, (what I think is) the default UITableView scrolling kicks in when cells are removed from one section and added to another.
My assumption (as I haven't found any reference to scroll behaviour in the docs) is that the UITableView scrolls to place/keep the inserted row in view.
Consider the example of swiping a cell to remove it from one section and have it added to the other:
the cell is currently in view because the user is swiping it (so perhaps there is no change/scroll required for the deleteRows...
(in some circumstances) the table view scrolls for the subsequent insertRows...; the scroll happens before the insert so that the animated insert is observed
For example, in the image below, swipe one of the cells in the bottom section, table view scrolls, then inserts at the bottom of the top section...
I say some circumstances because this behaviour is observed if the insert takes place in the top section (section 0). When the delete / insert is from section 0 to section 1, the table view does not scroll at all.
I'm also seeing some other behaviour I don't understand. In the case where some of the cells have more than one line of text (cells aren't all the same size) - the scroll seems to "not work". At first the scroll amount simply appeared wrong; however, testing with all the cells being single line revealed consistent scrolling when the insert occurred in section 0.
The following image shows the wrong scrolling:
I have two specific questions and one general question.
Specific question #1: is there a hierarchy for UITableViews such that section 0 is preferred, so the table view will scroll to section 0 so that inserts are observed, but not scroll to other sections?
Specific question #2: is it the height of the cells in the above example that is causing the wrong scrolling to be observed?
This is the code I'm using for calculating the height of the table view cells:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSInteger index = indexPath.row;
ParsedItem* item;
if (indexPath.section == 0) {
item = [_parsedItemList.toDoList objectAtIndex:index];
}
else {
item = [_parsedItemList.doneList objectAtIndex:index];
}
NSAttributedString* attrString = [[NSAttributedString alloc] initWithString:item.displayText attributes:_tableViewTextAttrNormal];
CGFloat height = [self textViewHeightForAttributedText:attrString andWidth:300.00];
return fmaxf(50.0, height);
}
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return 50.0;
}
- (CGFloat)textViewHeightForAttributedText:(NSAttributedString*)text andWidth:(CGFloat)width {
UITextView* calculationView = [[UITextView alloc] init];
[calculationView setAttributedText:text];
CGSize size = [calculationView sizeThatFits:CGSizeMake(width, FLT_MAX)];
return size.height;
}
General question: as I mentioned above, I haven't found anything in the docs that describes how the scrolling is supposed to work. Is there a general rule or principal that I've missed? I don't want to add explicit scroll operations if I'm simply doing it wrong...

How do I change the height of a table cell that has already loaded?

Currently I have webviews loading in customized uitableview cells. The problem is the web views have variable sizes. In webViewDidFinishLoad I am able to set the size of the web view based on the actual size of the html document just fine. My problem is the table cells which have already had their height set in heightForRowAtIndexPath before the web views having finished loading. How can I change the height of a table cell after it has already been loaded?
Ideally I feel like I should be able to use some line of code like this.
cellW.frame = cellW.cellWebView.frame;
However I don't seem to have access to cell information in heightForRowAtIndexPath. I've felt like I've explained the situation fairly well, but any code you think I should post I can put up here. I've tried a lot things (so there comments and failed attempts at this everywhere), but the main issue is I can't seem to access cell information in the right places such as heightForRowAtIndexPath. If I could even set cell information somehow in webViewDidFinishLoad, I could simply set the frame of the cell where I am also setting the frame size of the web view.
Below is the setup for my table cell subclass.
#import <UIKit/UIKit.h>
#import "DefinitionsAndConstants.h"
#interface UITableViewCellWebView : UITableViewCell
{
UIWebView *cellWebView;
}
#property (nonatomic,retain) UIWebView *cellWebView;
#end
Here is what I have tried last trying to use part Gavin's code. But of course there is no way to set the table cell now that I've gotten out because cellForRowAtIndexPath is not assignable.
-(void)webViewDidFinishLoad:(UIWebView *)webViews {
[webViews stringByEvaluatingJavaScriptFromString:#"twitterfy()"];
[webViews stringByEvaluatingJavaScriptFromString:#"document.getElementById('tweettext').innerHTML=tweet"];
NSString* h = [webViews stringByEvaluatingJavaScriptFromString:#"getDocHeightMax()"];
int height = [h intValue];
webViews.frame = CGRectMake(0.0f, 0.0f, 320.0f, height);
NSString* i = [webViews stringByEvaluatingJavaScriptFromString:#"return indexPath"];
int ii = [i intValue];
NSIndexPath* ip = [NSIndexPath indexPathForRow:ii inSection:0];
UITableViewCellWebView *tableCell = (UITableViewCellWebView *)[self.tableView cellForRowAtIndexPath:ip];
if (tableCell) {
tableCell.frame = webViews.frame;
}
NSLog(#"Got height from webview %d", height);
}
I would set up a mutable array to read heights from. In viewDidLoad, you'll have to assign some starting values (probably just a bunch of #"44.0", it doesn't really matter, it won't be used for much yet). In your heightForRowAtIndexPath:, just return the number from that array. Then in webViewDidFinishLoad, replace the heights in the array with the height you actually need for that cell (the NSString *h in the code you posted, I believe), and call reloadData on your table view. reloadData will hit heightForRowAtIndexPath:, which will look at the heights array, which now has the actual height needed for that cell, so everything should be shiny.
You should be able to do something like this for your heightForRowAtIndexPath method:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
MyTableViewCell *tableCell = (MyTableViewCell *)[self.tableView cellForRowAtIndexPath:indexPath];
if (tableCell) {
return tableCell.cellWebView.frame.size.height;
}
return 44;
}
This way, if the table cell is visible, it'll set the height according to the height of the embedded web view, otherwise it'll use a default height. In order to make the table view refresh the height of the cell, you'll have to call the following once the web view is sized properly:
[self.tableView beginUpdates];
[self.tableView endUpdates];
That won't make it reload the cells, which could screw things up for you, it'll just make it check the heights again, and the cell height will animate to the new height.

The correct way to calculate the height of cell

In my app, it have about thousand contents to display in tableView. Each of content has different heights since there are one to three lines UILabel in it. Currently, it calculates and returns the heights of each cell in the tableView delegate function:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
And the way it calculate is:
contentCell = (RSContentViewCell *)self.tmpCell;
UIFont *font = contentCell.myTextLabel.font;
width = contentCell.myTextLabel.frame.size.width + 30;
size = [contentStr sizeWithFont:font
constrainedToSize:CGSizeMake(width, CGFLOAT_MAX)
lineBreakMode:contentCell.myTextLabel.lineBreakMode];
height = size.height;
return height;
It works but takes about 0.5 secs to calculate those heights, so the UX is not so good, since the app will be no response during the calculation process.
So what's the correct way and where is the correct place to calculate heights of these cell?
UPDATE
The data is from the server and requested at the time that entering the table view.
As you are laoding your Data from a Server you do have a delay no matter what.
=> I suggest you do the Height calculating in background bevor you reload the table / remove the spinner etc.
// Method you call when your data is fetched
- (void)didReciveMyData:(NSArray *)dataArray {
// start background job
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
self.myCachedHeightArray = [[NSMutableArray alloc] initWithCapacity:[dataArray count]];
int i = 0;
for (id data in dataArray) {
float height;
// do height calculation
self.myCachedHeightArray[i] = [NSNumber numberWithFloat:height];// assign result to height results
i++;
}
// reload your view on mainthread
dispatch_async(dispatch_get_main_queue(), ^{
[self doActualReload];
});
});
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return [[self.myCachedHeightArray objectAtIndex:indexPath.row] floatValue];
}
I put this into my custom cell subclass usually... so the code doesn't clutter my controller and is correct for the cell I use. (+ it is better suited to MVC that way... the height of a cell is a view property IMO)
I DONT measure with each cell, but with a static method - see https://github.com/Daij-Djan/TwitterSearchExampleApp (the ViewController and the DDTweetTableViewCell class for an example)
You still have to do this on the front end but only do it once and cache the results in an Array and use that for future calls.
i.e. The first time the array value is empty so calculate it and store it in the array.
The next time the array has a value so use that without having to calculate.
How do you set/get self.tmpCell ? Do you save the reusable cell in a property?
Instead getting text from cell and calculating the size you can calculate the size of text from data source of the cell. I mean you set the texts in cellForRowAtIndexPath: somehow (eg. from an array) just use the text from that to calculate it.
For the frame : The cells have the same width of tableview
For the font : Just write a method called
- (UIFont *)fontForCellAtIndexPath:(NSIndexPath *)indexpath
and use it also from cellForRowAtIndexPath. It makes your job easier if you change the fonts of texts later.

Why is my UILabel in a UIView in a UITableCellView not updating after a correct database call, (it seems cell reuse related)?

I have a UITableview cell that gets a tally from a core data database. The tallyTable is in a view controller inside a UITab view. I have an NSLog statement that prints out the tally value whenever it gets updated. Another tab has a list to change the source (different day) for the tallies. I am using iOS5 with ARC targeting iOS 4.2.
Here's the problem. When I load the application, the correct tallies for whatever the last selected day show up in the table tab. If I then go to the day tab and change the day and return to the tally tab there is no change in the display. However, the viewWillAppear on the tally tab runs and as the table cycles through cellForIndexPath, my NSLog statement prints out all the correct new values. If I then scroll the top label off the screen and back the label updates to the new value.
I've tried setNeedsLayout and setNeedsDisplay on the UILabel, the UITableViewCell, the UITableView and the view controller loading the table. I tried changing the CellReuse identifier so that it would never reuse a cell.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
CollectionItemTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[CollectionItemTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
NSUInteger row = [indexPath row];
cell.textLabel.text = [[self.collectionKeys objectAtIndex:row] valueForKey:#"collectionTitle"];
NSInteger test1 = indexPath.row + 150;
NSLog(#"tag = %i", test1);
cell.tallyButton.tag = test1;
NSNumber * questionID = [[self.collectionKeys objectAtIndex:row] valueForKey:#"answerID"];
cell.tallyLabel.text = [NSString stringWithFormat:#"%i",[self updatePointTotal:questionID]];
NSLog(#"Collection text should be = %#", [NSString stringWithFormat:#"%i",[self updatePointTotal:questionID]]);
[cell setNeedsLayout];
return cell;
}
I've read over a half dozen other similar questions. Got about three hours invested so far in trying to solve this.
EDIT: I thought I fixed it by using the navigation controller to repush the top level view controller on to the view again. I'll admit now this feels like a classically kludgy hack in every way. When the view is PUSHED everything updates and it is seamless. However, in order to have a fixed footer to make selection settings for the table buttons, I used a UIView with two subviews, a UITableView on top and a simple UIView with four buttons below.
The captions on the buttons need to change with the data source. Now when the view controller is pushed onto the view it obscures my fixed footer view. So, I inserted the fixed footer into the UITableview and everything appeared fine until I scrolled the UITableView and the footer scrolled up with it. The table is basically a tally sheet with buttons next to each item and in the footer is four buttons to note the color of the tallied item. Say the next item was a green lego, you would tap "green" in the footer and the button next to "lego" in the table. When I push the view controller with the two subviews the UITableview labels do not update. Thus the tableview needs to be pushed itself (as far as I can tell).
ANSWER: see comment below but ultimately I needed to reload both the visible UITableView data and the delegate UITableView controller data behind it.
I'll give it a shot. First, are you using ARC? If not, you need to add autorelease when you alloc/init a new cell. Otherwise, it's fine as is.
If I'm understanding your question correctly:
The tableView displays the correct data at app launch
You switch away from the tab with the tableView and change the tableView dataSource
You switch back to the tab with the tableView and you see (via NSLog) that the table cells are reloaded with the correct data yet the old data is still visible in the cells
If you scroll a cell off the display and back forcing it to refresh it contains the correct data
Some thoughts:
the tableView will not reload itself automatically when it's view appears. You need to call [tableView reloadData] whenever the dataSource changes. This is independent of whether the tableView is currently displayed or not. My guess is this alone will solve your problem.
You don't need to call setNeedsLayout on the cell unless you want the cell to relayout its subviews based on the data. You also don't need setNeedsDisplay.
I'm assuming there aren't other complicating factors (such as multiple tableViews displaying the same data) that could confuse things.
If you use prepare for reuse method, remember to over the original method with [super prepareForReuse];
Another method if the above way does not work is re setup cell as describe here.
I use the same method i applied for some of my collection view : we should remove/reset your subview where you create/add it to cell 's content. That mean we need set up data each cell completely for each row.
I move the code reset data value from prepare reuse to where i set value and I worked simply !
In my CustomCell.m :
- (void)configCellWith:(id)item At:(NSUInteger)row {
if (_scrollView) {
[[_scrollView subviews]
makeObjectsPerformSelector:#selector(removeFromSuperview)];
_scrollView = nil;
[_scrollView removeFromSuperview];
}
else {
CGFloat y = labelHeight+15;
float scrollHeight = _imgArray.count*200;
_scrollView=[[UIScrollView alloc]initWithFrame:CGRectMake(10, y,SCREEN_WIDTH-20, scrollHeight)];
_scrollView.scrollEnabled=YES;
_scrollView.userInteractionEnabled=YES;
_scrollView.layer.masksToBounds = YES;
[self.contentView addSubview:_scrollView]; } }
Remember to change your data source appropriately too.

Resources