简体   繁体   中英

iOS: How to stop a background thread from updating UI in the main thread?

I implement a UITableView of UIImageView cells, each of which periodically refreshes itself every 5 seconds via NSTimer. Each image is loaded from a server in the background thread, and from that background thread I also update the UI, displaying the new image, by calling performSelectorOnMainThread . So far so good.

Currently I face a problem when I don't wait until an image is loaded at a current cell and I scroll down quickly to a new cell. That new cell, however, displays an image of a previous cell instead of the new cell's. Although the next round of NSTimer will correctly display the image, but this can confuse users at first.

The problem would be disappear if I don't reuse UITableView's cells, but given the number of cells to be displayed in my app, this is not an option.

So the only solution I can think of is to cancel (or kill) the background thread that is going to display an old image if I know that a user performs the scrolling action.

I wonder that this might not be the best practice and, therefore, seek for your advices.

(I also can't use SDWebImage because my requirement is to display a set of images in loop loaded from a server)

// In MyViewController.m
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    ...
    NSTimer* timer=[NSTimer scheduledTimerWithTimeInterval:ANIMATION_SCHEDULED_AT_TIME_INTERVAL 
                                                target:self 
                                              selector:@selector(updateImageInBackground:) 
                                              userInfo:cell.imageView 
                                               repeats:YES];
    ...
}

- (void) updateImageInBackground:(NSTimer*)aTimer
{  
    [self performSelectorInBackground:@selector(updateImage:)
                       withObject:[aTimer userInfo]];
}  

- (void) updateImage:(AnimatedImageView*)animatedImageView 
{      
    @autoreleasepool { 
        [animatedImageView refresh];
    }
}  

// In AnimatedImageView.m
-(void)refresh
{
    if(self.currentIndex>=self.urls.count)
        self.currentIndex=0;

    ASIHTTPRequest *request=[[ASIHTTPRequest alloc] initWithURL:[self.urls objectAtIndex:self.currentIndex]];
    [request startSynchronous];

    UIImage *image = [UIImage imageWithData:[request responseData]];

    // How do I cancel this operation if I know that a user performs a scrolling action, therefore departing from this cell.
    [self performSelectorOnMainThread:@selector(performTransition:)
                       withObject:image
                    waitUntilDone:YES];
}

-(void)performTransition:(UIImage*)anImage
{
    [UIView transitionWithView:self duration:1.0 options:(UIViewAnimationOptionTransitionCrossDissolve | UIViewAnimationOptionAllowUserInteraction) animations:^{ 
        self.image=anImage;
        currentIndex++;
    } completion:^(BOOL finished) {
    }];
}

Ok... Another way to do that would be to put your 'image requesting code' INTO your AnimatedImageView, and invalidate pending request each time you set a new Image URL

// your controller
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
    ...
    [cell.imageView setDistantImage:imageURL];
    ...    
    }

Then, in your Cell class,'setDistantImage' creates an ASIHttpRequest, and invalidates the previous one

//your ImageView class

@property (...) ASIHttpRequest *distantImageRequest;

- (void) setDistantImageUrl:(NSUrl *)imageUrl
{
    //clear previous request
    [distantImageRequest clearDelegatesAndCancel];
    //set a new request, with the callBack embedded directly in this ImageView class
    ... 
}

//animation and callBacks methods in this ImageView class too...

I had the exact same problem when loading many images in CollectionView & TableView with reuse cells overlaying all the downloaded images with a quick and long scrolling.

For the TableView, I solved it looking at the WWDC 2012 where the 'didEndDisplayingCell' method was presented but this wasn't usefull for the CollectionView that hadn't this method. :o(

Finally, I found out a solution for both... fortunately. The idea is to create a class ImageLoader called by the cell from TableView or CollectionView and that will be in charge of downloading images . In your implementation, add this snippet to hide the variable to the rest of the world :

@interface ImageLoader() {
__block NSURLSession *_session;
}

Then, add the following function in your implementation :

- (void)getViewForCell:(UIImageView*)cell
               FromURL:(NSURL*)url {

NSBlockOperation *_loadImageOp;
__block NSOperationQueue *_opQueue;

_opQueue = [[NSOperationQueue alloc]init];

if (_session != nil) {
    [_session invalidateAndCancel];
}

_loadImageOp = [[NSBlockOperation alloc]init];
[_loadImageOp addExecutionBlock:^{

    NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
    _session = [NSURLSession sessionWithConfiguration:config
                                             delegate:nil
                                        delegateQueue:_opQueue];

    NSURLSessionDownloadTask *downloadTask = [_session downloadTaskWithURL:url
                                                         completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
        UIImage *imageCell = [UIImage imageWithData:[NSData dataWithContentsOfURL:location]];

        if (imageCell) {
            [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                cell.image = imageCell;
            }];
        } else {
            [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                cell.image = [UIImage imageNamed:@"imageNotWorking.png"];
            }];
        }
    }];

    [downloadTask resume];
}];

[_opQueue addOperation:_loadImageOp];
}

The idea is that 1/. you create a block in which the member variable _session will get the url given in incoming parameter via a downloadTask, 2/. you display the image to the user interface via the mainQueue when it's downloaded... 3/. the initial created block being added to a queue dedicated to this specific reused cell.

The important thing is the 'invalidateAndCancel' function called if the member variable isn't nil because each time you want to reuse the cell, the former session won't bring you back the last image but the new one defined by the incoming parameter 'url'.

Afterwards, don't forget to put 1/.the following snippet in your TableView/CollectionView cell class in order to use only one imageLoader for each reuse cell and 2/.the method to be called in the method 'tableView:cellForRowAtIndexPath:' of your CollectionView/TableView:

@interface TableCell () { //...or CollectionCell  :o)
ImageLoader *_imgLoader;
}
@end

@implementation TableCell
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
_imgLoader = [[ImageLoader alloc]init];
return self;
}

- (void)imageURL:(NSURL*)url {
[_imgLoader getViewForCell:self.imageViewWhereTheDownloadedImageIs
                   FromURL:url];
}
@end

Once done, just call [cell imageURL:url] in the method 'tableView:cellForRowAtIndexPath:' of your CollectionView/TableView.

Now, you can scroll as quickly as you wish and you'll have always the proper image and only this one.

I hope that it will help... be well. :o)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM