How to load UITableview part by part - ios

I have an UITableView its loading number ofsongs from the back end (actually those are song's urls). Those are getting in JSON format. Now my problem is when number of songs are increases the table getting stuck. It hard to scroll. So how I can avoid this?
I'm loading views top of a main view. In main view viewDidLoad
arrSongList=[[NSMutableArray alloc]init];
arrSongList=[ws GetSongs];`
Then there is a menu to select song option. When I click on that menu cell it loads the songs view. for loading song view
-(void)getAllSongsAndReloadTable :(id)sender//1
{
[viewDisplayArea addSubview:viewSongs];
[self flipView : viewDisplayArea: viewDisplayArea : UIViewAnimationTransitionFlipFromLeft];
[progressAlert dismissWithClickedButtonIndex:0 animated:YES];
}
ViewSongs view has the song table. It loads in this way
This is for numberOfRowsInSection
else if (tableView.tag==4)
{
return [arrSongList count];
}
This is for cellForRowAtIndexPath
else if (tableView.tag==4)//All Songs
{
cell4 =nil;
if (cell4 == nil) {
NSArray *nib =[[NSBundle mainBundle]loadNibNamed:#"SongCell" owner:self options:nil];
cell4=[nib objectAtIndex:0];
}
[cell4 setSelectionStyle:UITableViewCellSelectionStyleNone];
cell4.lblSong.text=[[arrSongList objectAtIndex:indexPath.row] valueForKey:#"SONGTITLE"];
cell4.lblArtist.text=[[arrSongList objectAtIndex:indexPath.row] valueForKey:#"ARTISTNAME"];
NSString *imgUrl=[[arrSongList objectAtIndex:indexPath.row]valueForKey:#"SONGIMG"];
NSData *data=[NSData dataWithContentsOfURL:[NSURL URLWithString: imgUrl]];
UIImage *img = [[UIImage alloc] initWithData:data];
cell4.imgSong.image=img;
NSString *imgUrlLarge=[[arrSongList objectAtIndex:indexPath.row]valueForKey:#"SONGIMGLARGE"];
NSData *dataLarge=[NSData dataWithContentsOfURL:[NSURL URLWithString: imgUrl]];
UIImage *imgLarge = [[UIImage alloc] initWithData:dataLarge];
cell4.imgSongLarge=[[UIImageView alloc]init];
cell4.imgSongLarge.image=imgLarge;
[cell4.btnTick addTarget:self action:#selector(tickButtonTapped:) forControlEvents:UIControlEventTouchUpInside] ;
[cell4.btnAddtoMyList addTarget:self action:#selector(topLeft:) forControlEvents:UIControlEventTouchUpInside];
return cell4;
}

You will be having a array of songs urls. And you would have given number of rows in table as arrayOfUrlSongs.count. In stead of that make
Make a global count
int count = 10;
if(arrayOfUrlSongs.count < count)
return arrayOfUrlSongs.count;
else
{
return count;
}
Keep a reload button in footer view of table. On click of that increment count by 10 and reload table.
I hope this helps you.

You should also consider loading the cells in batches. I'd recommend checking out the Sensible TableView framework. The framework will automatically fetch all the songs from your web service and divide them in batches if you wish. Should save you a ton of work.

Related

Multiple NSURLSessions Causing UITableView Problems

I'm running into a bit of a strange problem here. One of my NSURLSessions is in charge of getting information for restaurant information that I have stored (restaurant name, restaurant's logo URL, etc), and then the second NSURLSession is in charge of using the restaurant's logo URL to retrieve the specific image and set it for each UITableView's cell.
The problem, however, is that my UITableView does not load anything at all sometimes so the cells are empty, but at other times when I add an extra [_tableView reload] in the NSURLSessions' completion block in the fetchPosts method, it'll work, but then the cells will stop displaying anything again if I re-run it. Something is definitely wrong. Have a look at my code below:
#import "MainViewController.h"
#import "SWRevealViewController.h"
#import "RestaurantNameViewCell.h"
#import "RestaurantList.h"
#interface MainViewController ()
#end
#implementation MainViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//List of restaurants needed to load home page
_restaurantInformationArray = [[NSMutableArray alloc] init];
self.tableView.dataSource = self;
self.tableView.delegate = self;
//setup for sidebar
SWRevealViewController *revealViewController = self.revealViewController;
if ( revealViewController )
{
[self.sidebarButton setTarget: self.revealViewController];
[self.sidebarButton setAction: #selector( revealToggle: )];
[self.view addGestureRecognizer:self.revealViewController.panGestureRecognizer];
}
//Get list of restaurants and their image URLs
[self fetchPosts];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [_restaurantInformationArray count];
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
RestaurantNameViewCell *cell = (RestaurantNameViewCell *)[_tableView dequeueReusableCellWithIdentifier:#"restaurantName" forIndexPath:indexPath];
RestaurantList *currentRestaurant = [_restaurantInformationArray objectAtIndex:indexPath.row];
cell.restaurantName.text = currentRestaurant.name;
cell.imageAddress = currentRestaurant.imageURL;
cell.restaurantClicked = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapDetected:)];
cell.restaurantClicked.numberOfTapsRequired = 1;
cell.restaurantLogo.userInteractionEnabled = YES;
[cell.restaurantLogo addGestureRecognizer:cell.restaurantClicked];
cell.restaurantLogo.tag = indexPath.row;
//Add restaurant logo image:
NSString *URL = [NSString stringWithFormat:#"http://private.com/images/%#.png",cell.imageAddress];
NSURL *url = [NSURL URLWithString:URL];
NSURLSessionDownloadTask *downloadLogo = [[NSURLSession sharedSession]downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
UIImage *downloadedImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:location]];
cell.restaurantLogo.image = downloadedImage;
}];
[downloadLogo resume];
return cell;
}
-(void)fetchPosts {
NSString *address = #"http://localhost/xampp/restaurants.php";
NSURL *url = [NSURL URLWithString:address];
NSURLSessionDataTask *downloadRestaurants = [[NSURLSession sharedSession]dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSError *someError;
NSArray *restaurantInfo = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&someError];
for(NSDictionary *dict in restaurantInfo) {
RestaurantList *newRestaurant = [[RestaurantList alloc]init];
newRestaurant.name = [dict valueForKey:#"name"];
newRestaurant.imageURL = [dict valueForKey:#"image"];
[_restaurantInformationArray addObject:newRestaurant];
//Refresh table view to make sure the cells have info AFTER the above stuff is done
[_tableView reloadData];
}
}];
[downloadRestaurants resume];
}
#end
It's probably a very stupid mistake that I'm making, but I'm not certain how I should correct this. I'm new to iOS development, so would greatly appreciate some guidance :)
Besides assuming that your network requests aren't erroring (you should at least log if there are network errors), there are threading issues.
Your NSURLSession callback probably runs on a background thread. This makes it unsafe to call UIKit (aka - [_tableView reloadData]). UIKit isn't thread safe. This means invoking any of UIKit's APIs from another thread creates non-deterministic behavior. You'll want to run that piece of code on the main thread:
dispatch_async(dispatch_get_main_queue(), ^{
[_tableView reloadData];
});
Likewise for fetching the images. It's slightly more complicated because of table view cell reuse which could cause the wrong image to display when scrolling. This is because the same cell instance is used for multiple values in your array as the user scrolls. When any of those callbacks trigger, it'll replace whatever image happens to be in that cell. The generally steps to reproduce this is as follows:
TableView requests 5 cells
MainViewController requests 5 images (one for each cell)
User scrolls down one cell
The first cell gets reused as the 6th cell.
MainViewController requests another image for the 6th cell.
The 6th image is retrieved, the callback is triggered, image of the first cell is set to image #6.
The 1st image is retrieved, the callback is triggered, image of the first cell is set to image #1 (incorrect).
You'll need to make sure the cell is displaying the correct cell before attempting to assign the image to it. If you rather not implement that logic for image fetching in cells, you could use SDWebImage instead. Using SDWebImage's [UIImageView sd_setImageWithURL:] is thread safe (it will set the image on the main thread).
Side notes:
You only need to reload data once all your changes are in _restaurantInformationArray, and not every time in the for loop.

showing progress on imageview on customcell on uitableview

I followed the codes here https://stackoverflow.com/a/7212943/711837 to get me started to show an indicator my app trying to download images from a particular website. The scenario is:
i have a tableview with many custom cells, the custom cells has 2 labels and a imageview.
i have a NSURL to download the contents that will fill up the labels and then a separate class that will download the images to be filled into the UIImageView. The code for the spinner and the downloading of images are:
resultArray = [[NSArray alloc] initWithArray:response];
[self downloadImageFromInternet:#"http://static.colourlovers.com/images/shapes/0/7/7090/50x50.png"];
//spinner for the download
spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
spinner.frame = CGRectMake(0, 0, 24, 24);
custcell.accessoryView = spinner;
[spinner startAnimating];
///[spinner release];
[self.thetableView reloadData];
and then i call the [spinner stopAnimating] at the finish downloading method of the class but somehow, the spinners just don't animate, or appear for the matter! am i missing something? or is there somewhere i can refer to? my aim is to show the UIIndicatorView at the place of the UIImageView then after loading, the imageview takes over the same position and this is on every cell.
UPDATED added the methods
-(void) downloadImageFromInternet:(NSString*)urlToImage{
// Create a instance of InternetImage
asynchImage = [[DownloadThumb alloc] initWithUrl:urlToImage];
// Start downloading the image with self as delegate receiver
[asynchImage downloadImage:self];
}
-(void) internetImageReady:(DownloadThumb*)downloadedImage{
// The image has been downloaded. Put the image into the UIImageView
[arrayImg addObject:downloadedImage.Image];
[spinner stopAnimating];
[self.thetableView reloadData];
}
-(void)downloadImage:(id)delegate{
m_Delegate = delegate;
NSURLRequest *imageRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:ImageUrl]
cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:60.0];
imageConnection = [[NSURLConnection alloc] initWithRequest:imageRequest delegate:self];
if(imageConnection)
{
workInProgress = YES;
m_ImageRequestData = [[NSMutableData data] retain];
}
}
You should really initiate the spinner when you create the cell.
Then you should check in cellForRowAtIndexPath if it should be visible or not.
Finally, when you need to display it, you can use [tableView reloadData] to display or remove it.

A follow-up Q for avoid UI getting lagged (IOS)

Now I'm using this code, with a little modification:
if (self._photoPath && !self._photo) {
dispatch_queue_t bg_thread = dispatch_queue_create("com.yourcompany.bg_thread", NULL);
dispatch_queue_t main_queue = dispatch_get_main_queue();
dispatch_async(bg_thread,^{
NSData *data = [NSData dataWithContentsOfFile:self._photoPath];
if(data != nil) {
dispatch_async(main_queue,^{
self._photo = [UIImage imageWithData:data];
[self.photoButton setImage:[UIImage imageNamed:#"photoButton.png"] forState:UIControlStateNormal];
});
}
});
}
As you can see, in fact after I get that photo I want set it immediately to my "photoButton",
but now, UI got smoothed, but my photoButton's appearance is always black...
What should I do next?
Thanks for your patience.
_______________Updated________
I have 2 viewControllers, A and B.
A is the root viewController, and B is A's child viewController.
In B, there is a button for calling the camera to take a photo.
After user toke a photo, the photo's apperance becomes that photo.
When I push a new B (with no photo) from A,
things goes smoothly.
But when there is an old B with a photo in it.
the animation gets a little stucked, since the following code I guess:
- (void)viewWillAppear:(BOOL) animated {
if (self._photoPath && !self._photo) {
NSData *data = [NSData dataWithContentsOfFile:self._photoPath];
if(data != nil)
self._photo = [UIImage imageWithData:data];
}
[super viewWillApear];
}
But I do need to get that photo before the view is displayed since I need to set that photo to my photoButton's background.
So, is there a way to avoid stucking the view's animation? Cause it really result in bad user experience.
Thanks a lot!
I found the answer.
Just add a line of code to refresh the button.
For me, it is
[self.photoButton setImage:[UIImage imageNamed:#"photoButton.png"] forState:UIControlStateNormal];
NSUInteger p[2] = {1,4};
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:
[NSIndexPath indexPathWithIndexes:p length:2]]
withRowAnimation:UITableViewRowAnimationNone];

Progress Bar In AQGridView (Similar to TableView)

I have an odd problem. I am using an AQGridView which has a method similar to table view controller which I have defined as follows:
- (AQGridViewCell *)gridView:(AQGridView *)aGridView cellForItemAtIndex:(NSUInteger)index
{
static NSString *CellIdentifier = #"IssueCell";
AQGridViewCell *cell = (AQGridViewCell *)[gridView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
[[NSBundle mainBundle] loadNibNamed:#"IssueCell" owner:self options:nil];
cell = [[AQGridViewCell alloc] initWithFrame:self.gridViewCellContent.frame
reuseIdentifier:CellIdentifier];
[cell.contentView addSubview:self.gridViewCellContent];
cell.selectionStyle = AQGridViewCellSelectionStyleNone;
}
IssueCell *content = (IssueCell *)[cell.contentView viewWithTag:1];
//This model object contains the title, picture, and date information
IssueModel *m = (IssueModel *)[self.issues objectAtIndex:index];
//If we have already downloaded the file, set the alpha to 1
if ([m hasPdfBeenDownloaded])
{
content.downloadIcon.hidden = YES;
content.imageView.alpha = 1;
content.progressView.hidden = YES;
}
else
{
if (m.pdfDownloadRequest && m.pdfDownloadRequest.isExecuting)
{
content.downloadIcon.hidden = YES;
content.imageView.alpha = .2;
content.progressView.hidden = NO;
}
else
{
content.downloadIcon.hidden = NO;
content.imageView.alpha = .2;
content.progressView.hidden = YES;
}
}
content.title.text = m.title;
// Only load cached images; defer new downloads until scrolling ends
if (!m.coverImageIcon)
{
if (self.gridView.dragging == NO && self.gridView.decelerating == NO)
{
[self startIconDownload:m forIndex:index];
}
content.imageView.image = [UIImage imageNamed:#"grid_cell_loading.png"];
}
else
{
content.imageView.image = m.coverImageIcon;
}
return cell;
}
My problem is since cells are reused, I lose the correct progress indication and updating of it. I am using ASIHTTP as follows:
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:model.issuePdfUrl];
request.userInfo = [NSDictionary dictionaryWithObjectsAndKeys:ic, #"cell", model, #"model", nil];
model.pdfDownloadRequest = request;
[request setShouldContinueWhenAppEntersBackground:YES];
[request setDelegate:self];
[request setDownloadDestinationPath:mediaPath];
[request setDownloadProgressDelegate:ic.progressView];
[request startAsynchronous];
The problem I am having is when I scroll down and then scroll backup, I lose the progressView I used to have as a reusable cell is used.
What is the correct way to do this so I don't lose the progress view?
As far as I understand, you bind progressView to a cell (first one, for example), then you scroll to a second cell, it is being created. Then third. This third possibly reuses first cell. But progressBar is not recreated, you reuse it.
So you have one progressBar but two ASIHTTPRequests that point to it. That's not very good.
What can I suggest? Well. You can update link of downloadProgressDelegate with the progressBar during gridView:cellForItemAtIndex: call. That is more ok path. You also can remove reusability of cells. That might help but is less ok and can cause problems (for example memory leaks) in the future.
Another way is to make some single method that gets all the progress messages. And uses these messages to map progress data to a grid model.

using NSInvocationOperation for UITableViewCell causes some UIImageViews to display the wrong image? Possible re-entrant issue?

I'm trying to keep my UITableView scrolling smoothly while going through about 700 pictures that are downloaded from the internet, cached (to internal storage) and displayed on each cell of the table. My code so far seems fine as far as scrolling performance. However, I noticed that sometimes, if the connection is being crappy or If I scroll really fast, a cell will display the wrong picture (that of another cell) for maybe about 1/2 a sec and then update to the image it is supposed to display.
I suspect 2 things so far:
A-
I might have a reentrancy issue from the point where my NSInvocationOperation calls back into the main thread with [self performSelectorOnMainThread:] to the point where the selector in the main thread gets executed. Though I don't really spot any shared variables.
B-
Some sort of race between the main thread and the NSInvocationOperation? Like:
1 main thread calls cacheImageFromURL
2 inside this call, UIImage spans the worker thread
3 worker thread is almost done and gets to call performSelectorOnMainThread
4 the cell in question is dequeued to be reused at this point, so main thread calls cahceImageFromURL again for a new image.
5 inside this call, UIImage stops the NSOPerationQueue which causes the previous NSInvocationOperation thread to die.
6 BUT, the thread had already called performSelectorOnMainThread
7 so the selector gets excited causing the old image to load.
8 immediately after this, the recently spawned thread is done fetching the new image and calls performSelectorOnMainThread again, causing the update to the right image.
If this is the case, I guess I'd need to set a flag on entry to the cacheImageFromURL method so that the worker thread code doesn't call performSelectorOnMainThread if there's another thread (the main one) already inside cacheImageFromURL?
Here's my code for my UIImageView subclass, which each cell in the table uses:
#implementation UIImageSmartView
//----------------------------------------------------------------------------------------------------------------------
#synthesize defaultNotFoundImagePath;
//----------------------------------------------------------------------------------------------------------------------
#pragma mark - init
//----------------------------------------------------------------------------------------------------------------------
- (void)dealloc
{
if(!opQueue)
{
[opQueue cancelAllOperations];
[opQueue release];
}
[super dealloc];
}
//----------------------------------------------------------------------------------------------------------------------
#pragma mark - functionality
//----------------------------------------------------------------------------------------------------------------------
- (bool)cacheImageFromURL:(NSString*)imageURL
{
/* If using for the first time, create the thread queue and keep it
around until the object goes out of scope*/
if(!opQueue)
opQueue = [[NSOperationQueue alloc] init];
else
[opQueue cancelAllOperations];
NSString *imageName = [[imageURL pathComponents] lastObject];
NSString* cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
NSString *cachedImagePath = [cachePath stringByAppendingPathComponent:imageName];
/* If the image is already cached, load it from the local cache dir.
Else span a thread and go get it from the internets.*/
if([[NSFileManager defaultManager] fileExistsAtPath:cachedImagePath])
[self setImage:[UIImage imageWithContentsOfFile:cachedImagePath]];
else
{
[self setImage:[UIImage imageWithContentsOfFile:self.defaultNotFoundImagePath]];
NSMutableArray *payload = [NSMutableArray arrayWithObjects:imageURL, cachedImagePath, nil];
/* Dispatch thread*/
concurrentOp = [[NSInvocationOperation alloc] initWithTarget:self selector:#selector(loadURI:) object:payload];
[opQueue addOperation: concurrentOp];
[concurrentOp release];
}
return YES;
}
//----------------------------------------------------------------------------------------------------------------------
/* Thread code*/
-(void)loadURI:(id)package
{
NSArray *payload = (NSArray*)package;
NSString *imageURL = [payload objectAtIndex:0];
NSString *cachedImagePath = [payload objectAtIndex:2];
/* Try fetching the image from the internets.
If we got it, write it to disk. If fail, set the path to the not found again.*/
UIImage *newThumbnail = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:imageURL]]];
if(!newThumbnail)
cachedImagePath = defaultNotFoundImagePath;
else
[UIImagePNGRepresentation(newThumbnail) writeToFile:cachedImagePath atomically:YES];
/* Call to the main thread - load the image from the cache directory
at this point it'll be the recently downloaded one or the NOT FOUND one.*/
[self performSelectorOnMainThread:#selector(updateImage:) withObject:cachedImagePath waitUntilDone:NO];
}
//----------------------------------------------------------------------------------------------------------------------
- (void)updateImage:(NSString*)cachedImagePath
{
[self setImage:[UIImage imageWithContentsOfFile:cachedImagePath]];
}
//----------------------------------------------------------------------------------------------------------------------
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
// Return YES for supported orientations
return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
#end
And the way this UIImage is used is in the context of cellForRowAtIndexPath, like so:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UIImageSmartView *cachedImage;
// and some other stuff...
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:CellIdentifier] autorelease];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.selectionStyle = UITableViewCellSelectionStyleGray;
// some labels and tags stuff..
cachedImage = [[UIImageSmartView alloc] initWithFrame:CGRectMake(5, 5, 57, 80)];
cachedImage.contentMode = UIViewContentModeCenter;
cachedImage.defaultNotFoundImagePath = [[NSBundle mainBundle] pathForResource:#"DefaultNotFound" ofType:#"png"];
cachedImage.tag = PHOTO_TAG;
[cell.contentView addSubview:cachedImage];
[cell.contentView addSubview:mainLabel];
[cell.contentView addSubview:secondLabel];
}
else
{
cachedImage = (UIImageSmartView*)[cell.contentView viewWithTag:PHOTO_TAG];
mainLabel = (UILabel*)[cell.contentView viewWithTag:MAINLABEL_TAG];
}
// Configure the cell...
NSString *ImageName = [[[self.dbData objectAtIndex:indexPath.row] objectAtIndex:2]
stringByReplacingOccurrencesOfString:#".jpg"
withString:#"#57X80.png"];
NSString *imageURL = [NSString stringWithFormat:#"www.aServerAddress.com/%#/thumbnail5780/%#",
self.referencingTable,
ImageName];
[cachedImage cacheImageFromURL:imageURL];
mainLabel.text = [[self.dbData objectAtIndex:indexPath.row] objectAtIndex:0];
return cell;
}
The problem is reuse of cell, one cell makes the request of various images at same time, and is displaying when each one is downloaded, i know that you are canceling operations queue but as the processing caller is synchronous the operation continues the execution. I suggest try to save the indexPath of the request, and match it with the index path of the cell before set the UIImage.
D33pN16h7 is right in that the problem was cell reuse. However, instead of trying to make the indexPath thread-safe through an NSURLConnection, I decided to reimplement the whole thing by moving the NSOperationQueue into the UITableViewController code and having the concurrent imageView class be actually a proper subclass of NSOperation (since I was using NSOperationInvocation in the first place to try and avoid the full-fledged NSOperation subclass).
So now, the table controller manages it's own NSOperationQueue, the operations are subclasses of NSOperation and I can cancel them from the table controller code as the table view scrolls past them. And everything works fast and nice.

Resources