I'm having a problem which is similar to others on SE, in that my UITableView controller loads the text label immediately, but only loads my thumbnail image when I scroll the view and move the item offscreen.
I tried adding [self.tableView reloadData] to the AFHTTPRequestOperation setCompletionBlockWithSuccess, which works with one drawback. It obviously runs too often.
Here is the method in which the problem occurs:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *fullPath;
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"TableViewCell" forIndexPath:indexPath];
Child *child = _children[indexPath.row];
if([child.data.thumbnail length] == 0) {
fullPath = #"reddit.png";
} else {
// Get the thumbnail
NSURL *url = [NSURL URLWithString:child.data.thumbnail];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
fullPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[url lastPathComponent]];
[operation setOutputStream:[NSOutputStream outputStreamToFileAtPath:fullPath append:NO]];
[operation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
NSLog(#"bytesRead: %lu, totalBytesRead: %lld, totalBytesExpectedToRead: %lld", (unsigned long)bytesRead, totalBytesRead, totalBytesExpectedToRead);
}];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
// [self.tableView reloadData];
NSLog(#"RES: %#", [[[operation response] allHeaderFields] description]);
NSError *error;
if(error) {
NSLog(#"ERR: %#", [error description]);
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"ERR1: %#", [error description]);
}];
[operation start];
}
cell.textLabel.text = child.data.title;
cell.imageView.image = [UIImage imageNamed:fullPath ];
return cell;
}
Instead of reloading the entire table view every time an image is loaded, you could just set that image directly on the cell inside you completion block.
HOWEVER, if you do that you need to check that the cell is still visible and that it is still on the same index path it was on when you started loading the view, otherwise you might be setting the image on a cell that has been reused and is now in a different position in the table view.
Related
I have a working UICollectionViewCell in UITableViewCell. I'm using HWViewPager, so the collectionview cell moves from left to right. Unfortunately, the data is not updated until the user scrolls to the left or to the right. The content that fills up the cell is fetched from json, so I figure that has something to do but I'm not sure what. I load everything in awakeFromNib.
- (void)awakeFromNib {
] videoArray = [[NSMutableArray alloc] init];
[self getDisco];
}
with getDisco being a void function.
- (void)getDisco
{
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager GET:url parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
videoArray = [NSMutableArray arrayWithArray:[responseObject valueForKey:#"releases"]];
// NSLog(#"JSON: %#", responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
}
Now, where the problem is.
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
if (indexPath.item < [videoArray count]){
DiscoCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"CollectionCell2" forIndexPath:indexPath];
NSDictionary *shot = [videoArray objectAtIndex:[indexPath row]];
cell.label2.text = [shot objectForKey:#"title"];
return cell;
return nil;
}else{
DiscoCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"CollectionCell2" forIndexPath:indexPath];
cell.label2.text = [NSString stringWithFormat:#"Cell %d", indexPath.row];
return cell;
}
}
Without the if/else statements, the viewcontroller crashed completely. With it, nothing updates till the user scrolls. My question is how do I get it all to pre-load, without crashing, for the user? Please keep UICollectionViewCell is in a tableviewcell.
You should reload collectionView after finished the request:
- (void)getDisco
{
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager GET:#"http://musicbrainz.org/ws/2/release/?query=arid:e0140a67-e4d1-4f13-8a01-364355bee46e%20AND%20primarytype:single&fmt=json&limit=100" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
videoArray = [NSMutableArray arrayWithArray:[responseObject valueForKey:#"releases"]];
// NSLog(#"JSON: %#", responseObject);
[collectionView reloadData];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
}
I know that there are a lot of similar questions to this one, but i didn't find any well-explained one yet.
I have a UITableView that gets its content (which is JSON) from a url, i'm using this method for fetching the JSON data:
-(void)getContents
{
NSString *contentStartString = [NSString stringWithFormat:#"%ld",(long)contentStart];
NSString *contentCountString = [NSString stringWithFormat:#"%ld",(long)contentCount];
NSString *contentsUrl = #"http://www.ana.fm/api/index.php?start=";
contentsUrl = [contentsUrl stringByAppendingString:contentStartString];
contentsUrl = [contentsUrl stringByAppendingString:#"&count="];
contentsUrl = [contentsUrl stringByAppendingString:contentCountString];
NSLog(#"%#",contentsUrl);
NSURL *URL = [NSURL URLWithString:contentsUrl];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
//AFNetworking asynchronous url request
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc]
initWithRequest:request];
operation.responseSerializer = [AFJSONResponseSerializer serializer];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"%#", responseObject);
jsonContents = [responseObject objectForKey:#"contents"];
[self.tableView reloadData];
tableLoadMoreCapability = true;
} failure:nil];
[operation start];
}
This works perfectly, but then when i reach the end of the table which i can detect using this method:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
float endScrolling = scrollView.contentOffset.y + scrollView.frame.size.height;
if (endScrolling >= scrollView.contentSize.height)
{
if(tableLoadMoreCapability == true){
contentStart = contentStart + 20;
[self updateContentsTable];
}
}
}
As you see i want to execute this method updateContentsTable (when reaching the end of the table) which is this one:
- (void)updateContentsTable
{
tableLoadMoreCapability = false;
NSLog(#"load more rows");
NSString *contentStartString = [NSString stringWithFormat:#"%ld",(long)contentStart];
NSString *contentCountString = [NSString stringWithFormat:#"%ld",(long)contentCount];
NSString *contentsUrl = #"http://www.ana.fm/api/index.php?start=";
contentsUrl = [contentsUrl stringByAppendingString:contentStartString];
contentsUrl = [contentsUrl stringByAppendingString:#"&count="];
contentsUrl = [contentsUrl stringByAppendingString:contentCountString];
NSLog(#"%#",contentsUrl);
NSURL *URL = [NSURL URLWithString:contentsUrl];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
//AFNetworking asynchronous url request
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc]
initWithRequest:request];
operation.responseSerializer = [AFJSONResponseSerializer serializer];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"%#", responseObject);
jsonContents = [responseObject objectForKey:#"contents"];
[self.tableView reloadData];
} failure:nil];
[operation start];
}
Now i'm using [self.tableView reloadData]; so the code works and the table reloaded with the new data, i don't want that, i need the new rows to be inserted below the old ones, i learned from other answers that i have to use [self.tableView insertRowsAtIndexPaths: .....] but i didn't understand how to do that.
Anyone can clarify how to do that ?
Thanks in advance.
I think you are doing more work than you need. Why not tell the table view that the number of rows you have is larger than the number you get with your first call? Then, as soon as the delegate is asked for a cell at a row beyond the ones you have actually loaded (indicating that the user has scrolled past the data you have available), you then get your next batch of data via JSON. Through the delegate methods, you have precise control over what data is shown in the visible cells. I would avoid the scrolling stuff completely.
The only care you need to take is when (and how) you reload the table: you want to avoid reload calling reload calling reload....
Think of the rows of the table as a window sliding over a (virtual) table of all your rows.
I use Table View and Embed In to Navigation make a custom cell and set a Progress bar and a downloading button on it. And i set multiple URLs and save all URLs in array and set those array on table view. I use AFNetworking to Downloading via URL. I set all methods on NSObject class and call that method on table view cell.
- (void)startDownload{
if(!self.isDownloading){
self.isDownloading = YES;
NSURL *url = [NSURL URLWithString:self.DownloadString];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
NSString *fullPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[url lastPathComponent]];
[operation setOutputStream:[NSOutputStream outputStreamToFileAtPath:fullPath append:NO]];
[operation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
self.Progress = (float)((float)totalBytesRead/(float)totalBytesExpectedToRead);
NSLog(#"progress: %f",self.Progress);
}];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"RES: %#", [[[operation response] allHeaderFields] description]);
NSError *error;
NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:fullPath error:&error];
if (error) {
NSLog(#"ERR: %#", [error description]);
} else {
// NSNumber *fileSizeNumber = [fileAttributes objectForKey:NSFileSize];
// long long fileSize = [fileSizeNumber longLongValue];
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"ERR: %#", [error description]);
}];
[operation start];
}
}
now I have a issue if downloading was start and I Press back button and again come to table view the progress Bar is again "0" Downloading Button is again available.
I want to save the state of my progress bar when ever cell dequeue and I go back to the navigation or came back. I want Preserve my Downloading state and my progress bar state.
Is any One knows how to Save state of my Progress bar when I download a big file the progress bar is running on tableview cell when I came back Progress states preserved.
I'm new in iOS programming. So I've newbie question. I'm getting started with AFNetworking 2 and that is the task:
I've a request. Its response is the part of the second request. It means that I have to wait untill first request ends. They follow step-by-step. When I get the second response I parse it and save 20 URLs in format http://lalala-xx.jpg. After that I want to load images and put them into UICollectionView, and I want to do it not all in scope but in scheme "downloaded->straight to cell". I save URLs and images in singleton class and get access to them just like
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
CustomCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"cell" forIndexPath:indexPath];
cell.imageView.image = [[UserData sharedUser].images objectAtIndex:indexPath.row];
return cell;
}
The chain of methos looks like
- (void)method1
{
NSString *string = [NSString stringWithFormat:firstRequestString];
NSURL *url = [NSURL URLWithString:string];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
operation.responseSerializer = [AFJSONResponseSerializer serializer];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject)
{
// getting needed part for second request
[self method2:(NSString *)part1];
}
failure:^(AFHTTPRequestOperation *operation, NSError *error)
{
// show error
}];
[operation start];
}
Second method:
- (void)method2:(NSString *)part1
{
// lalala making secondRequestString
NSString *string = [NSString stringWithFormat:secondRequestString];
NSURL *url = [NSURL URLWithString:string];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
operation.responseSerializer = [AFJSONResponseSerializer serializer];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject)
{
NSMutableArray *imageURLs = [[NSMutableArray alloc] init];
// getting needed URLs
[self loadAllImages:(NSMutableArray *)imageURLs];
}
failure:^(AFHTTPRequestOperation *operation, NSError *error)
{
// show error
}];
[operation start];
}
Last:
- (void)loadAllImages:(NSMutableArray *)imageURLs
{
// ???
}
I'm stuck. What should I do next? I have 20 URLs, but how should I download images and direct them to ViewController to update image in cells?
I suppouse AFNetworkig can provide me some operation queue.
And I dont like my code now. I use this chain, but I want an independent method2 returning imgURLs. So it should look:
User presses button -> method1 -> method2 -> stop. Wait untill user presses button -> download image1 -> show image1 -> download image2 -> show image2 -> and so on -> download imageN -> show imageN -> stop. I'll repeat, I need to store images in Array, I'll use it after that.
Thx u read that.
///////////////////////////////////// UPDATE /////////////////////////////////////
I found solution. But it does not satisfy me completely. Images come randomly. How to make them load in order?
- (void)loadAllImages:(NSMutableArray *)imageURLs
{
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
for (NSURL *url in imageURLs)
{
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
operation.responseSerializer = [AFImageResponseSerializer serializer];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject)
{
[[UserData sharedUser].images addObject:responseObject];
[[NSNotificationCenter defaultCenter] postNotificationName:#"CollectionViewRealoadData" object:nil];
}
failure:^(AFHTTPRequestOperation *operation, NSError *error)
{
// show error
}];
[manager.operationQueue addOperation:operation];
}
}
You need to get the data from the URLs and then create a UIImage object from that data.
You can get the data from the URL using the NSURL methods
for(NSString *imgString in imageURLs){
NSURL *url = [NSURL URLWithString:imgString];
NSData *imgData = [NSData dataWithContentsOfURL:url];
UIImage *img = [UIImage imageWithData:imgData ];
[imageUrls addObject:img];//this mutable array should be initialized early in view controller life cycle (like ViewDidLoad).
}
Once you have your image object you can add it to your array of images that you are using as a datasource for your collection view.
[_collectionView insertItemsAtIndexPaths:#[[NSIndexPath indexPathForItem:[imageUrls count] - 1 inSection:0]]];
//reload your collection view once you add new data
Using AFNetworking to download files from a server. Here's the code:
self.networkQueue = [[[NSOperationQueue alloc] init] autorelease];
[networkQueue setMaxConcurrentOperationCount:3];
for(NSDictionary* fileDictionary in self.syncArray) {
#autoreleasepool {
if([[fileDictionary allKeys] containsObject:#"downloadZipURL"]) {
NSString* downloadPath = [fileDictionary objectForKey:#"downloadZipURL"];
downloadPath = [downloadPath stringByAddingPercentEscapesUsingEncoding:NSASCIIStringEncoding];
NSURLRequest *requestURL = [NSURLRequest requestWithURL:[NSURL URLWithString:downloadPath]];
NSString* localDestPath = [NSString stringWithFormat:#"%#/%#", [FileUtil userDocumentsDirectory], [downloadPath lastPathComponent]];
NSString* localTempPath = [NSString stringWithFormat:#"%#.tmp", localDestPath];
[(NSMutableDictionary*)fileDictionary setObject:localDestPath forKey:#"downloadDestination"];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:requestURL];
operation.outputStream = [NSOutputStream outputStreamToFileAtPath:localDestPath append:NO];
operation.userInfo = fileDictionary;
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
if (networkQueue.operationCount == 0)
{
if(hasDownloadError || isCancellingSync) {
return ;
}
[self performSelectorInBackground:#selector(processAllFiles) withObject:nil];
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
// [operation setDownloadProgressBlock:^(NSUInteger bytesWritten, long long totalBytesWritten, long long totalBytesExpectedToWrite) {
// NSLog(#"Sent %lld of %lld bytes, %#", totalBytesWritten, totalBytesExpectedToWrite, localDestPath);
// float progress = (float)totalBytesWritten/(float)totalBytesExpectedToWrite;
// [(NSMutableDictionary*)operation.userInfo setObject:[NSString stringWithFormat:#"Downloading %.0f%%", progress*100] forKey:#"downloadStatus"];
// [(NSMutableDictionary*)operation.userInfo setObject:[NSNumber numberWithFloat:progress] forKey:#"downloadProgress"];
// [syncViewController onPermitUpdated];
// }];
[networkQueue addOperation:operation];
}
}
}
My problem is that once this code is run, memory slowly gets eaten up and never given back. Now, these can be large files, which is why I used the outputStream.
Any suggestions would be appreciated.
Off the top of my head - I see that you're not using ARC.
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:requestURL]
Are you releasing this operation somewhere?
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
if (networkQueue.operationCount == 0)
{
if(hasDownloadError || isCancellingSync) {
return ;
}
[self performSelectorInBackground:#selector(processAllFiles) withObject:nil];
}
Here, you're using the networkQueue in the completionBlock and the block retains the networkQueue, you then add the operation to the networkQueue, which retains the operation, which leads to neither of them deallocating. Try making a weak variable of the networkQueue and use that in order to break the cycle.
If these don't work - run instruments and make a note of what objects remain in memory and when their reference count is changed.