How to reload a UITableView for more data? - iphone

I have UITableView, which I fill from a JSON Query.
I fill it with Private Messages and it works.
The JSON Query has the properties of "site" in which I get for every site 20 PMs.
So at the beginning I got Site=0 and got 20PMS which are loaded.
Now I wanna have the features like in the Email-app (i believe I saw it there): When you scroll down and reach the end (in my app reached the 20. PM), the application should load the next 20 and so on and so on.
Any ideas how to realize?

Try
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return ([array count] + 1);
}
In
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
Do
if([indexpath row] == [array count]) {
[self loadPMForSite:([array count]/20-1)];
}
In
- (void)loadPMForSite:(int)siteNumber
load the new pms and then reload the tableview data.
Not tested but supposed to work. May include typos.

Sideswipe's answer is a good start, but you don't want to freeze the UI while you get the next twenty items. In the last-row case, you should return a cell with "Loading..." and a UIActivityIndicator while launching a thread to actually get the data. When that thread completes, then it signals the main thread, which can then reload the data. As an aside, be careful not to change the size of array (in background thread) until you actually putting in the data, as you may run into a problem with the user scrolling beyond the last line if [array count] is too large.
See apple's sample: LazyTableImages
For the UIActivityIndicator:
UIActivityIndicatorView * loading = [[UIActivityIndicatorView alloc] initWithFrame:CGRectMake(200, 20, 30, 30)];
[date setActivityIndicatorViewStyle:UIActivityIndicatorViewStyleGray];
[self.view addsubview:loading];
[loading startAnimating];

Related

Displaying a "No rows found" message in UITableView with Core Data

I have implemented an iPhone app that uses UITableViewController/UITableView and Core Data. Further, I use a NSFetchedResultsController to manage the table data. This was all very straight forward and works great. I then decided that I should display a message in the UITableView when no rows where found/retrieved. After researching this, it appeared that the best way (perhaps the only way) to do this was to return a "dummy" cell that contains the message. However, when I do this, I get a nastygram from the runtime system that complains (and rightfully so) about data inconsistencies: "Invalid update: invalid number of sections. The number of sections contained in the table view ...". Here is the relevant code:
- (NSInteger) numberOfSectionsInTableView: (UITableView *)tableView
{
if ([[self.fetchedResultsController fetchedObjects] count] == 0) return 1;
return [[self.fetchedResultsController sections] count];
}
- (NSInteger) tableView: (UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if ([[self.fetchedResultsController fetchedObjects] count] == 0) return 1;
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex: section];
return [sectionInfo numberOfObjects];
}
- (UITableViewCell *) tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath
{
if ([[self.fetchedResultsController fetchedObjects] count] == 0) {
UITableViewCell *cell = [[UITableViewCell alloc] init];
cell.textLabel.text = #"No widgets found.";
return cell;
}
STCellView *cell = (STCellView *)[tableView dequeueReusableCellWithIdentifier: #"ShieldCell"];
[self configureCell: cell atIndexPath: indexPath];
return cell;
}
I have read responses from similar questions and it appears that I should use
insertRowsAtIndexPaths: withRowAnimation:
to insert the "dummy" message row into my table. However, this also means removing the "dummy" row when a real row is inserted. I can do this, but it seems like there should be an easier way to accomplish this. All I want to do, is to display a message indicating that there are no rows in the table (simple enough?). So, my question is this: Is there a way to display a message in an UITableView without using the "dummy" cell approach OR is there a way to convince UITableViewController/NSFetchResulsController that this is only a "dummy" row and they should not get so upset about it because it is not a real row (from my point of view) in the table?
Any help you can provide would be very appreciated (I am a struggling newbie to iPhone development and I want to learn the best practices). Thanks.
Rather than hack with the tableview datasource to get the intended UI you should add the "No rows found" message to the tableview header instead.
I did as follows in viewDidLoad.
UILabel *label = [[UILabel alloc] init];
[label setTextColor:[UIColor lightGrayColor]];
[label setText:#"No widgets found."];
[label sizeToFit];
label.frame = CGRectMake((self.tableView.bounds.size.width - label.bounds.size.width) / 2.0f,
(self.tableView.rowHeight - label.bounds.size.height) / 2.0f,
label.bounds.size.width,
label.bounds.size.height);
[self.tableView insertSubview:label atIndex:0];
In this case, each TableViewCells must be opaque to hide the label. or need to toggle the hidden property of the label according to the row count.
An alternative approach, which I have used before is to use Core Data to manage the update for you by inserting a 'no rows' entity for the section where no rows have been detected in your model class, which handles the data update.
There are a number of ways to implement this e.g. set the name/title field to a known status message or a flag within the entity. Once inserted you can detect the 'no rows' entity in the cellForRowAtIndexPath delegate method and insert an alternative table cell to show the message.
Just remove the 'no rows' entity before refreshing the data for that section.
My simple suggestion to display an empty message is to rearrange your controller to be a simple UIViewController (not a UITableViewController).
This UIViewController is composed by a UITableView (the controller is the data source and the delegate for your table) and by a UILabel (or a UIView that contains a UILabel) that displays the empty row message.
In this manner you can control the visibility of the table and the label based on the retrieved rows.
This approach could be laborious but I think it's good to avoid hacking NSFetchResultsController and data source. Furthermore you could have a complete control on arranging the position for your empty message.
As #Rog suggested you could also use the table view header to display that message. As you prefer.
Hope it helps.

Asynchronous image loading error

I am trying to loading images in tableViewCell asynchronously.
When I move the table up and down then images appear otherwise not.
AsyncImageView contains NSUrlConnection. It doesn't go inside the delegate "connectionDidFinishLoading". but when I move the table up or down then it goes inside this delegate function
Below is the code
CGRect frame;
frame.size.width=115; frame.size.height=100;
frame.origin.x=0; frame.origin.y=0;
AsyncImageView* asyncImage = [[[AsyncImageView alloc]
initWithFrame:frame] autorelease];
loadingTable=[[UIActivityIndicatorView alloc] initWithFrame:frame];
[cell.contentView addSubview:loadingTable];
[loadingTable startAnimating];
asyncImage.tag = 999;
NSURL* url = [NSURL URLWithString:[[videoCollection objectAtIndex:indexPath.row] videoImageUrl] ];
[asyncImage loadImageFromURL:url activity:loadingTable];
[cell.contentView addSubview:asyncImage];
First load all images and add them some data structure like array and after that show it to the table. Don't overlap your UI with network calls.
I'm sure as you know, all UI related business must be done on the main thread. However, downloading big amounts of data can and should be done in the background. And this tutorial with Grand Central Dispatch may be just what you need.
Download the images in a background thread and when its complete, let the main thread know of this and do the updating of the UI on the main thread.
I've done this yesterday.
The best way is to call the download in background and in the - (UITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath method.
This method will load the cell's image when the user would want to see it.
But warning ! To prevent downloading many time the same image, you should use a data structure (like dictionary) to check if the image has been downloaded.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
...
if([myPictures objectForKey:indexPath.row] == nil) {
[self performSelectorInBackground:#selector(downloadPhotoInBackground:) withObject:indexPath];
}
return cell
}
and in downloadPhotoInBackground:, don't forget to add your downloaded image to "myPictures" property.

animating a large number of rows/sections in UITableView poor performance

we're not talking thousands of rows or anything, although if there was a way to make things scale up that high, I'd love it.
I have a table with 27 sections and 180 rows spread across all sections, and the scenario I'm currently getting stuck in is when I animate things to a model state with only 3 sections and 5 rows, and (even worse) back again.
I'm batching all the animations with beginUpdates/endUpdates. My app pretty well locks up for 1-2 seconds on an iphone4 while it figures things out, then the animations start.
I've tried both animating the removal/addition of each row, keeping the sections around (and dropping their row counts to 0 in the removal case), and also animating just the removal/insertions of the sections themselves (when the row count would have dropped to 0). I would have assumed the latter would give better performance but it didn't change things at all.
Is there anything that can be done on the app end to speed this up? Right now I have a rather gross bit of code to bail out of the individual animations if there are more than 20 of them, opting to just reloadData instead.
edit here's code that exhibits the problem. The performance of this code is slightly better than the equivalent monotouch code (which is what I was using before), but it's still pretty bad.
#import "TableViewController.h"
#interface MyTableViewDataSource : NSObject<UITableViewDataSource> {
int rows;
};
#end
#implementation MyTableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (void)setRowCount:(int)r
{
rows = r;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return rows;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (!cell)
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
cell.textLabel.text = [NSString stringWithFormat:#"row %d", indexPath.row];
return cell;
}
#end
#implementation MyTableViewController {
UIBarButtonItem *populateButtonItem;
};
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
populateButtonItem = [[UIBarButtonItem alloc] initWithTitle:#"Populate" style:UIBarButtonItemStylePlain target:self action:#selector(populateDataSource)];
}
return self;
}
- (void)populateDataSource
{
NSMutableArray* new_rows = [[NSMutableArray alloc] init];
[((MyTableViewDataSource*)self.tableView.dataSource) setRowCount:200];
for (int i = 0; i < 200; i ++)
[new_rows addObject:[NSIndexPath indexPathForRow:i inSection:0]];
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:new_rows withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView endUpdates];
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.tableView.dataSource = [[MyTableViewDataSource alloc] init];
self.navigationItem.rightBarButtonItem = populateButtonItem;
}
#end
It only makes sense to animate the rows that are visible. Instead of doing the animation for all the rows that you're inserting, consider just animating the insertion of those rows that will be visible.
Also, are you sure that it's the animation that's causing the delay? Do you get the same delay if you pass UITableViewRowAnimationNone for the animation, or is it faster? If it's faster, then again, avoid animating those insertions that won't be visible. (You can figure out which rows are currently visible using -indexPathsForVisibleRows.) If it's not faster, then the problem is probably not related to the animation at all, but rather the overhead of inserting a few hundred rows all at once. Reloading the entire table as you're now doing is one option; inserting the rows in smaller batches is another.
Finally, it'd be a good idea to profile your app with Instruments while you do the insertion. You'll get a better idea of what the app is doing during that delay, and that's the first step toward eliminating the delay.
You're using UITableViewRowAnimationAutomatic (Reference: The table view chooses an appropriate animation style for you. (Introduced in iOS 5.0.)), and for some reason, the table view chooses a really bad one, which fades in all rows while expanding them. Changing opacity on 200 UIViews while resizing their frame and moving them around HAS to be slow. :)
You can just do this:
[self.tableView insertRowsAtIndexPaths:new_rows withRowAnimation:UITableViewRowAnimationNone];
This will reduce animations to UITableView's very basic animation when inserting rows, which, in my opinion, is absolutely sufficient when inserting that many.
Try to test the same functionality with some hardcoded array of strings and see issue is gone. If issue is still there, it means the problem is with rendering not with data.
For different kind of animation there are some custom controls already made where you can have the faster animation with some nice one liner implementation: Here have a look.

Get data in batches & store it in NSMutableArray to display it in UITableView in iphone

How can I get the data from address book in batches of say 20 records at a time & display that 20 records in table view. When new 20 records are fetched , I want to add it in existing array & reload the data. How can I do this? Thanks
In the following delegate where you are setting your rows add one extra row
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return yourMutableArray.count+1;
}
The following delegate tells you that currently which cell is being created.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
Under this you can add a check condition like
if(indexPath.row > yourMutableArray.count)
{
// This means you are at the end of your table
// Call your webservice here and append the next set of data into the yourMutableArray
// You can also show an activity indicator over the extra cell here, Indicating the loading of new data
[self performSelector:#selector(getDataFromWebservice) withObject:nil afterDelay:1.0];
}
else
{
// Do your cell settings here
}
and somewhere in your code where you are checking for the success of your webserivce add the following line to reload the table
[yourTable reloadData];
Make sure, that you append the new data into your MutableArray, else table will show only the latest data from the webservice. Hope, this will help you.

iPhone SDK: Inserting and updating a UITableView with a new row

I have a tableView that needs to be updated after information has been inserted from another view. If I perform a
[self.tableView reloadData];
The very next time I insert more information in another view and try to reload the table, all the currently visible rows are duplicated.
In other words, when I start up the app I have:
tableView:
Row 1
Row 2
Then I submit some information that will also show up in the table and suddenly I have:
tableView
Row 1
Row 2
Row 3 <- info I just added
Row 1
Row 2
My numberOfRowsInSection implementation looks like this:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [ItemsController sharedItemsController].count;
}
My cellForRowAtIndexPath implementation looks like this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
ItemsController* controller = [ItemsController sharedItemsController];
NSMutableArray* recentItems = controller.listOfRecentItems;
CustomCell *cell = nil;
NSUInteger row = [indexPath row];
if( row < recentItems.count )
{
Items* item = [recentItems objectAtIndex:row];
if( recentCellData == nil )
recentCellData = [[NSMutableDictionary alloc] initWithCapacity:[indexPath length]];
if( [recentCellData count] > 0 )
cell = [recentCellData objectForKey:[NSString stringWithFormat:#"%d", row]];
if (cell == nil) {
UIViewController * view1 = [[UIViewController alloc] initWithNibName:#"CustomCell" bundle:nil];
cell = (CustomCell*)[view1 view];
[recentCellData setObject:cell forKey:[NSString stringWithFormat:#"%d",row]];
}
// do some other stuff here
}
// Set up the cell
return cell;
}
What's the best way to update the table and avoid duplicating the currently visible rows.
Thank in advance for all the help!
The error isn't in how you're reloading the table, it's in how you're providing data to it. Set a breakpoint in the data source methods and the method that adds new rows to see where you're going wrong.
You'll only end up with five items if tableView:numberOfRowsinSection: returns 5. Thats the simple answer to your question, but I see other problems here. I'm wondering why you have this test: row < recentItems.count. Is that array the same thing as [ItemsController sharedItemsController].count? You really need to be using the same array for both methods.
(Also, it's not a syntax error, but you shouldn't use the property syntax for things that aren't declared as properties. You should write [recentItems count] instead.)
I'm also confused by the code you use to set up the cell. Cells are meant to be reusable. That is, you create one cell, then reconfigure it every time in your implementation of tableView:cellForRowAtIndexPath:. Your code creates a cell for each item in your list. This is very memory-inefficient, and will likely crash your program due to insufficient memory on the iPhone if you keep lots of cells in memory like this.
The recommended approach is to call dequeueReusableCellWithIdentifier:. If that returns nil, then you set up a cell using the initWithFrame:reuseIdentifier: initializer. The table view is very smart, and will only ask you to redraw the cell when it needs you to.
Your recentCellData dictionary looks really shaky to me, too. What if you insert an item after the item with key #"2"? All the items with key #"3" onward will need to be shifted one element to the right to work the way you expect. That's a ton of bookkeeping that seems rather unnecessary to me. If you really needed something like this -- and to be clear, I don't think you do -- why wouldn't you use an NSMutableArray, which is much easier to use?
I added a bit more info above.