I have a UITableView that loads thumbnails into cells aynchronously as follows:
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:
^{
ThumbnailButtonView *thumbnailButtonView = [tableViewCell.contentView.subviews objectAtIndex:i];
UIImage *image = [self imageAtIndex:startingThumbnailIndex + i];
[self.thumbnailsCache setObject: image forKey:[NSNumber numberWithInt:startingThumbnailIndex + i]];
[[NSOperationQueue mainQueue] addOperationWithBlock:
^{
UITableViewCell *tableViewCell = [self cellForRowAtIndexPath:indexPath];
if (tableViewCell)
{
[activityIndicatorView stopAnimating];
[self setThumbnailButtonView:thumbnailButtonView withImage:image];
}
}];
}];
[self.operationQueue addOperation:operation];
[self.operationQueues setObject:operation forKey:[NSNumber numberWithInt:startingThumbnailIndex + i]];
As per a technique I learned in a WWDC presentation, I store all of my operation queues in a NSCache called operationQueues so that later on I can cancel them if the cell scrolls off the screen (there are 3 thumbnails in a cell):
- (void) tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
NSInteger startingThumbnailIndex = [indexPath row] * self.thumbnailsPerCell;
for (int i = 0; i < 3; i++)
{
NSNumber *key = [[NSNumber alloc] initWithInt:i + startingThumbnailIndex];
NSOperation *operation = [self.operationQueues objectForKey:key];
if (operation)
{
[operation cancel];
[self.operationQueues removeObjectForKey:key];
}
}
}
However, I notice if I repeatedly launch, load, then close my UITableView, I start recieving memory warnings, and then eventually the app crashes. When I remove this line:
[self.operationQueues setObject:operation forKey:[NSNumber numberWithInt:startingThumbnailIndex + i]];
The memory issues go away. Does anyone have any clue on why storing the operation queues in a cache or an array causes the app to crash?
Note: I learnt about NSCache and NSOperationQueue a couple of days ago so I might be wrong.
I don't think that this is a problem with NSOperationQueue, you are adding images to your thumbnailsCache but when the view scrolls off-screen they are still in memory. I am guessing that when the cells scroll back in, you re-create your images. This is probably clogs your memory.
Also, shouldn't you be caching your images instead of your operations?
EDIT
I did some detailed testing with NSCache by adding images and strings until my app crashed. It doesn't seem to be evicting any items so I wrote my custom cache, which seems to work:
#implementation MemoryManagedCache : NSCache
- (id)init
{
self = [super init];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(reduceMemoryFootprint) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
return self;
}
- (void)reduceMemoryFootprint
{
[self setCountLimit:self.countLimit/2];
}
#end
Related
I'm loading images from a remote server using SDWebImage into a UICollectionView using the following code:
[myCell.imageView setImageWithURL:imgURL placeholderImage:nil options:SDWebImageRetryFailed success:^(UIImage *image)
{
[_imageCache storeImage:image forKey:[imgURL absoluteString] toDisk:YES];
} failure:^(NSError *error){
NSLog(#"ERROR: %#", error);
}];
For most cells, this code works fine - it loads the images and saves them to my local disk. However, after several (it seems random?) images, they stop loading. I then get the following error:
ERROR: Error Domain=NSURLErrorDomain Code=-1001 "The request timed out." UserInfo=0x1d33fdc0 {NSErrorFailingURLStringKey=http://path/to/image.jpg, NSErrorFailingURLKey=http://path/to/image.jpg, NSLocalizedDescription=The request timed out., NSUnderlyingError=0x1d34c0f0 "The request timed out."}
When this happens, my app seems to stop sending NSURLRequests altogether. After a period of time, probably about 20-30 seconds, I can refresh the table and the failed images will load in correctly and the app will resume responding to all NSURLRequests perfectly fine.
I find that this tends to happen more often if I scroll down my collection view fast. Could it be trying to download too many at once? Is there a way to limit the number of concurrent downloads? This method appears to be deprecated in the latest SDWebImage code.
Figured it out. I was using MWPhotoBrowser in another part of my app. MWPhotoBrowser comes with an older/modified version of SDWebImage. I downloaded the latest version of SDWebImage from Github, renamed/refactored all of the files, and imported my newly updated and modified SDWebImage alongside the one MWPhotoBrowser relies on.
The new version of SDWebImage has solved my problem completely!
It's better if use asynchronous download by share manager and display when finish download with SDWebImage. Now, you can scroll fast to test.
- (UICollectionViewCell *)collectionView:(UICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
MyCell *myCell = (MyCell *)[cv dequeueReusableCellWithReuseIdentifier:#"MyCell" forIndexPath:indexPath];
//Set placeholder
myCell.imageView.image = [UIImage imageNamed:#"placeholder.png"];
NSURL *cellImageURL = [NSURL URLWithString:[NSString stringWithFormat:#"%#",[self.collectionData objectAtIndex:indexPath.row]]];
[myCell.cellIndicator startAnimating];
SDWebImageManager *manager = [SDWebImageManager sharedManager];
[manager downloadWithURL:cellImageURL
delegate:self
options:0
success:^(UIImage *image, BOOL cached)
{
//Display when finish download
myCell.imageView.image = image;
[myCell.cellIndicator stopAnimating];
} failure:nil];
return cell;
}
*EDIT:
If problem still not solve on device: try to separate download and display
#interface ViewController ()
#property (retain , nonatomic) NSMutableArray *linksArray;
#property (retain , nonatomic) NSMutableArray *imagesArray;
#end
- (void)viewDidLoad
{
[super viewDidLoad];
[self initLinksArray];
//Add placehoder.png to your imagesArray
[self initImagesArray];
//Download to NSMultableArray
[self performSelectorInBackground:#selector(downloadImages) withObject:nil];
}
- (void)initImagesArray{
self.imagesArray = [[[NSMutableArray alloc] init] autorelease];
for (NSInteger i = 0; i < self.linksArray.count; i++) {
[self.imagesArray addObject:[UIImage imageNamed:#"placeholder.png"]];
}
}
- (void)downloadImages{
for (NSInteger i = 0; i < self.linksArray.count; i++) {
NSURL *cellImageURL = [NSURL URLWithString:[self.linksArray objectAtIndex:i]];
SDWebImageManager *manager = [SDWebImageManager sharedManager];
[manager downloadWithURL:cellImageURL
delegate:self
options:0
success:^(UIImage *image, BOOL cached)
{
[self.imagesArray replaceObjectAtIndex:i withObject:image];
} failure:nil];
}
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return [self.imagesArray count];;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
SDCell *cell = (SDCell *)[cv dequeueReusableCellWithReuseIdentifier:#"SDCell" forIndexPath:indexPath];
cell.cellImageView.image = [self.imagesArray objectAtIndex:indexPath.row];
return cell;
}
Is my - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath delegate I have the following code:
if ([movie isDownloaded])
cell.detailTextLabel.text = movie.duration;
else
{
cell.detailTextLabel.text = #"";
[movie downloadInQueue:self.downloadQueue completion:^(BOOL success) {
UITableViewCell *updateCell = [tblView cellForRowAtIndexPath:indexPath];
if (updateCell)
{
updateCell.detailTextLabel.text = movie.duration;
[updateCell setNeedsLayout];
}
}];
}
Which calls into Movie.m and runs this code:
- (void)downloadInQueue:(NSOperationQueue *)queue completion:(void (^)(BOOL success))completion
{
if (!self.isDownloading)
{
self.downloading = YES;
[queue addOperationWithBlock:^{
BOOL success = NO;
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithURL:self.fileURL];
CMTime timeduration = playerItem.duration;
float seconds = CMTimeGetSeconds(timeduration);
self.duration = [self timeFormatted:seconds];
self.downloading = NO;
self.downloaded = YES;
success = YES;
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
completion(success);
}];
}];
}
}
When my cells become not visible, I want to cancel the NSOperation in the Movie object if it hasn't been run yet (remove it from the queue). I know I can subclass UITableViewCell and do something like this:
- (void)willMoveToWindow:(UIWindow *)newWindow
{
[super willMoveToWindow:newWindow];
if (newWindow==nil) {
// Cell is no longer in window so cancel from queue
}
}
Question... how can I cancel my Movie NSOperation from within the UITableViewCell delegate call? With a delegate or NSNotification of some kind? I need to know the indexPath of the cell to get the correct Movie object out of my array and cancel the operation.
As of iOS 6 you can use the tableView:didEndDisplayingCell:forRowAtIndexPath: method of the UITableView Delegate protocol. This gets called when the cell is removed from the table view (which happens when it is no longer visible).
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
// Cancel operation here for cell at indexPath
}
By the way, I just saw this question here, but already answered it here. But I agree with Nebs (and his answer should be accepted).
As Nebs said, in iOS 6, use didEndDisplayingCell. Thus, it might look like:
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
Movie *movie = self.movies[indexPath.row];
if ([movie isDownloading])
[movie cancelDownload];
}
But in earlier versions, you have to do something like responding to scrollViewDidScroll, manually looking at what indexPath objects are no longer included in indexPathsForVisibleRows, and cancel operations from there
In order to cancel the operation, you need to change this downloadInQueue so that rather than just calling addOperationWithBlock, it should create a NSBlockOperation and add that to the queue, but also save a weak reference to it so you can write the cancelDownload method like so:
#interface Movie ()
#property (nonatomic, getter = isDownloaded) BOOL downloaded;
#property (nonatomic, getter = isDownloading) BOOL downloading;
#property (nonatomic, weak) NSOperation *operation;
#end
#implementation Movie
- (void)downloadInQueue:(NSOperationQueue *)queue completion:(void (^)(BOOL success))completion
{
if (!self.isDownloading)
{
self.downloading = YES;
NSOperation *currentOperation = [NSBlockOperation blockOperationWithBlock:^{
BOOL success = NO;
self.playerItem = [AVPlayerItem playerItemWithURL:self.webURL];
if (self.playerItem)
{
success = YES;
CMTime timeduration = self.playerItem.duration;
float seconds = CMTimeGetSeconds(timeduration);
self.durationText = [NSString stringWithFormat:#"%f", seconds];
}
self.downloading = NO;
self.downloaded = YES;
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
completion(success);
}];
}];
[queue addOperation:currentOperation];
self.operation = currentOperation;
}
}
- (void)cancelDownload
{
if ([self isDownloading] && self.operation)
{
self.downloading = NO;
[self.operation cancel];
}
}
#end
It looks like you should pass in either the cell or the index path to downloadInQueue:completion:. Then you can maintain the connection between the cell and its operation, either using a dictionary, an array, or associated objects.
XCode 4.5.2; I'm downloading an image from a remote server like this :
- (void)viewDidLoad
{
[super viewDidLoad];
NSOperationQueue *queue = [NSOperationQueue new];
NSInvocationOperation *operation = [[NSInvocationOperation alloc]
initWithTarget:self
selector:#selector(loadImage)
object:nil];
[queue addOperation:operation];
}
- (void)loadImage{
self.theobject = [RemoteQuery loadObjectWithImage:self.imageKey];
[self performSelectorOnMainThread:#selector(displayImage) withObject:nil waitUntilDone:YES];
}
-(void)displayImage{
UIImage *image = [UIImage imageWithData: self.theobject.imageData];
[self.imageView setImage:image];
}
This works fine on IOS simulator, but doesn't work on a device; it seems like displayImage is called before the data is loaded from [RemoteQuery loadImage]. What would be the best way to ensure that the image has loaded properly before showing it ?
Create a delegate protocol which will call back to the original object when the image download finishes. This NSOperation tutorial has more details on how to do this
Alternatively, use the NSOperation's completionBlock to perform the image display.
I have a problem getting a UIActivityIndicatorView to show when I collect data from a server with help from the NSURLConnection request.
The request I think is asynchronous, i.e., started in a new thread. I have copied from Apple's AdvancedTableViewCells example. And I run it in XCode in the iOS 4.3 iPhone simulator. I have not tested it on a real iPhone yet.
Also I have googled this problem and tried a lot of suggestions but the feeling is that I have forgotten something basic. Below is my code from the class RootViewController.
I just select a row, create and add the activityview, startanimating, and then create the NSUrlConnection object which starts to fetch data from the server in another thread, I believe.
Any ideas?
#interface RootViewController : UITableViewController {
NSMutableData *receivedData;
UIActivityIndicatorView *activityView;
}
#end
...
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// In my rootviewcontroller
activityView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
[self.view addSubview:activityView];
[activityView startAnimating];
…
NSMutableURLRequest *tUrlRequest = [tQuery createUrlRequest:tStatId];
NSURLConnection *tConnectionResponse = [[NSURLConnection alloc] initWithRequest: tUrlRequest delegate: self];
if (!tConnectionResponse) {
NSLog(#"Failed to submit request");
} else {
NSLog(#"Request submitted");
receivedData = [[NSMutableData data] retain];
}
return;
}
...
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(#"Succeeded! Received %d bytes of data",[receivedData length]);
NSXMLParser *tParser = [[NSXMLParser alloc] initWithData: receivedData];
...
[tParser parse];
...
[connection release];
[receivedData release];
[NSThread sleepForTimeInterval: 2.0]; // Just to see if activity view will show up...
NSUInteger row = 1;
if (row != NSNotFound)
{
// Create the view controller and initialize it with the
// next level of data.
VivadataTViewController *vivaViewController = [[VivadataTViewController alloc] init];
if (activityView != nil) {
[activityView stopAnimating];
}
}
}
Had the same exact issue, try to change the color of the UIActivityIndicatorView under Attributes Inspector -> Style to Gray
Been looking for a solution for a long while now, but still no success, perhaps someone here can help me.
I created an application that runs a thread that communicates with a webserver in the background. Occasionally the thread receives information that I'd like to display in a UITableView. Because of the dynamic nature of the content I decided a NSMutableArray should be sufficient to store the information from the webserver.
Whenever I receive information from the webservice my AppDelegate sends a notification. My UITableView registers to receive certain kinds of information which I intend to show in the UITableView.
The initWithStyle method of the UITableViewController contains the following code:
- (void)addContact:(NSNotification *)note {
NSLog(#"%#", [note object]);
NSString *message = (NSString *)[note object];
NSString *name = [[message componentsSeparatedByString:#":"] objectAtIndex:2];
contact = name;
[array addObject:contact];
}
- (void)viewDidLoad {
[super viewDidLoad];
array = [[NSMutableArray alloc] initWithObjects:#"test1", #"test2", nil];
tv.dataSource = self;
tv.delegate = self;
self.tableView = tv;
}
The array in AddContact contains the data displayed in the TableView. All data I manually add to the array in ViewDidLoad: will be shown in the table, but it doesn't happen for the data added in the AddContact: method.
Anyone got an idea what I'm doing wrong?
Is the update happening on the main thread?
You can use this sort of refresh function to make sure it does.
- (void)refresh {
if([NSThread isMainThread])
{
[self.tableView reloadData];
[self.tableView setNeedsLayout];
[self.tableView setNeedsDisplay];
}
else
{
[self performSelectorOnMainThread:#selector(refresh) withObject:nil waitUntilDone:YES];
}
}