7

I created a NSOperationQueue to download images (from Twitter for Cell):

NSOperationQueue *queue = [[NSOperationQueue alloc]init];
   [queue addOperationWithBlock:^{
    NSString *ImagesUrl = [[NSString alloc]initWithFormat:@"http://api.twitter.com/1/users/profile_image/%@",[[status objectForKey:@"user"]objectForKey:@"screen_name"]];;
        NSURL *imageurl = [NSURL URLWithString:ImagesUrl];
        UIImage *img = [UIImage imageWithData:[NSData dataWithContentsOfURL:imageurl]];
        [[NSOperationQueue mainQueue]addOperationWithBlock:^{
            if (img.size.width == 0 || [ImagesUrl isEqualToString:@"<null>"]) {
                [statusCell.imageCellTL setFrame:CGRectZero];
                statusCell.imageCellTL.image = [UIImage imageNamed:@"Placeholder"] ;
            }else

            [statusCell.imageCellTL setImage:img];

this working fine, but when it appears to move the scroll and view the images are still loading, and they are changing several times until you get a picture.

And I do not like the diagnosis Profile of Time, so I wanted to somehow make this NSOperationQueue in Background

is also possible to show how to make a "Imagecache" no need to download an image already downloaded.

**(Status = NSDictionary of Twitter Timeline).

editing::(All Cell)

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{


    static NSString *CellIdentifier = @"Celulatime";
    UITableViewCell *Cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];


    if ( [Cell isKindOfClass:[TimeLineCell class]] ) {
        TimeLineCell *statusCell = (TimeLineCell *) Cell;
        status = [self.dataSource objectAtIndex:indexPath.row];


        statusCell.TextCellTL.text = [status objectForKey:@"text"];
        statusCell.NomeCellTL.text = [status valueForKeyPath:@"user.name"];
        statusCell.UserCellTL.text = [NSString stringWithFormat:@"@%@", [status valueForKeyPath:@"user.screen_name"]];


        NSDate *created_at = [status valueForKey:@"created_at"];
        if ( [created_at isKindOfClass:[NSDate class] ] ) {
            NSTimeInterval timeInterval = [created_at timeIntervalSinceNow];
            statusCell.timeCellTL.text = [self timeIntervalStringOf:timeInterval];
        } else if ( [created_at isKindOfClass:[NSString class]] ) {
            NSDate *date = [self.twitterDateFormatter dateFromString: (NSString *) created_at];
            NSTimeInterval timeInterval = [date timeIntervalSinceNow];
            statusCell.timeCellTL.text = [self timeIntervalStringOf:timeInterval];
        }

        NSString *imageUrlString = [[NSString alloc]initWithFormat:@"http://api.twitter.com/1/users/profile_image/%@",[[status objectForKey:@"user"]objectForKey:@"screen_name"]];;
        UIImage *imageFromCache = [self.imageCache objectForKey:imageUrlString];

        if (imageFromCache) {
            statusCell.imageCellTL.image = imageFromCache;
            [statusCell.imageCellTL setFrame:CGRectMake(9, 6, 40, 40)]; 
        }
        else
        {
            statusCell.imageCellTL.image = [UIImage imageNamed:@"TweHitLogo57"];
            [statusCell.imageCellTL setFrame:CGRectZero]; 

            [self.imageluckluck addOperationWithBlock:^{
                NSURL *imageurl = [NSURL URLWithString:imageUrlString];
                UIImage *img = [UIImage imageWithData:[NSData dataWithContentsOfURL:imageurl]];

                if (img != nil) {


                    [self.imageCache setObject:img forKey:imageUrlString];

                    // now update UI in main queue
                    [[NSOperationQueue mainQueue] addOperationWithBlock:^{

                        TimeLineCell *updateCell = (TimeLineCell *)[tableView cellForRowAtIndexPath:indexPath];

                        if (updateCell) {
                            [updateCell.imageCellTL setFrame:CGRectMake(9, 6, 40, 40)]; 
                            [updateCell.imageCellTL setImage:img];
                        }
                    }];
                }
            }];
        }


        }
    return Cell;
    }
madth3
  • 7,275
  • 12
  • 50
  • 74
Lcstest Test
  • 113
  • 1
  • 2
  • 9
  • 1
    As an alternative, have a look at SDWebImage. It's a library that lets you download images in the background and cache them to memory or disk as needed automatically. https://github.com/rs/SDWebImage – nebs Jan 21 '13 at 17:43
  • I found very useful .. but wanted to make from zero – Lcstest Test Jan 21 '13 at 17:48

3 Answers3

12

A couple of observations:

  1. You should probably define a NSOperationQueue in your class and initialize it in viewDidLoad (as well as a NSCache) and add operations to that queue, rather than creating a new NSOperationQueue for every image. Also, many servers limit the number of concurrent requests they'll support from each client, so make sure to set maxConcurrentOperationCount accordingly.

    @interface ViewController ()
    @property (nonatomic, strong) NSOperationQueue *imageOperationQueue;
    @property (nonatomic, strong) NSCache *imageCache;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad
    {
         [super viewDidLoad];
    
        self.imageOperationQueue = [[NSOperationQueue alloc]init];
        self.imageOperationQueue.maxConcurrentOperationCount = 4;
    
        self.imageCache = [[NSCache alloc] init];
    }
    
    // the rest of your implementation
    
    @end
    
  2. Your tableView:cellForRowAtIndexPath: should make (a) initialize the image before starting the asynchronous image load (so you don't see the old image from the reused cell there); and (b) make sure the cell is still visible before you update it:

    NSString *imageUrlString = [[NSString alloc]initWithFormat:@"http://api.twitter.com/1/users/profile_image/%@",[[status objectForKey:@"user"]objectForKey:@"screen_name"]];;
    UIImage *imageFromCache = [self.imageCache objectForKey:imageUrlString];
    
    if (imageFromCache) {
        statusCell.imageCellTL.image = imageFromCache;
        [statusCell.imageCellTL setFrame: ...]; // set your frame accordingly
    }
    else
    {
        statusCell.imageCellTL.image = [UIImage imageNamed:@"Placeholder"];
        [statusCell.imageCellTL setFrame:CGRectZero]; // not sure if you need this line, but you had it in your original code snippet, so I include it here
    
        [self.imageOperationQueue addOperationWithBlock:^{
            NSURL *imageurl = [NSURL URLWithString:imageUrlString];
            UIImage *img = [UIImage imageWithData:[NSData dataWithContentsOfURL:imageurl]];
    
            if (img != nil) {
    
                // update cache
                [self.imageCache setObject:img forKey:imageUrlString];
    
                // now update UI in main queue
                [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                    // see if the cell is still visible ... it's possible the user has scrolled the cell so it's no longer visible, but the cell has been reused for another indexPath
                    TimeLineCell *updateCell = (TimeLineCell *)[tableView cellForRowAtIndexPath:indexPath];
    
                    // if so, update the image
                    if (updateCell) {
                        [updateCell.imageCellTL setFrame:...]; // I don't know what you want to set this to, but make sure to set it appropriately for your cell; usually I don't mess with the frame.
                        [updateCell.imageCellTL setImage:img];
                    }
                }];
            }
        }];
    }
    
  3. No special handling of UIApplicationDidReceiveMemoryWarningNotification is needed because although NSCache does not respond to this memory warning, it does automatically evict its objects as memory becomes low.

I haven't tested the above code, but hopefully you get the idea. This is the typical pattern. Your original code had a check for [ImagesUrl isEqualToString:@"<null>"], which I don't see how could ever be the case, but if you need some additional logic besides just my if (img != nil) ..., then adjust that line accordingly.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Couple of comments: (1) It also may be done with dispatch_async(). Still NSOperation provides more control. For example, it may be canceled; (2) In order to set img to zero state for reused cells, it is imho ok to use this: cell.imageView.image = nil; (3) Why to have statusCell.imageCellTL? There is cell.imageView already in place. – Cynichniy Bandera Jan 21 '13 at 18:16
  • 2
    @Umka 1. Yes, you can use `dispatch_async()` but then you can't control the number of concurrent requests. That's why I prefer `NSOperationQueue`. 2. You can use `cell.imageView.image = nil`, but the default `UITableViewCell` sometimes reformats the cell depending upon whether an `image` is present or not, so I always use a blank image to avoid jarring reformatting of the cell (or worse, failure to reformat the cell when image is loaded). 3. You sometimes use a custom cell layout to avoid the problems I allude to in my prior point (or if the it otherwise doesn't conform to a standard layout). – Rob Jan 21 '13 at 18:23
  • a very good answer, but my Cell is a custom cell (get an error) – Lcstest Test Jan 21 '13 at 18:27
  • 1
    @Umka Also, while I didn't demonstrate it, it sometimes is nice to be able to cancel backlogged image download requests (such as when you dismiss the tableview). By using `NSOperationQueue`, you can use `cancelAllOperations`. If you're dealing with small thumbnails, you often don't have to worry about it (because the tableview can keep up reasonably well), but if not, it's nice to be able to cancel requests. – Rob Jan 21 '13 at 18:27
  • and the problem is that the scroll very fast moving images out of order (or repeats all the cells visible) – Lcstest Test Jan 21 '13 at 18:38
  • I will test, and the results speak – Lcstest Test Jan 21 '13 at 18:42
  • Now this a little better .. but still have the problem that the images exchanged several times to get the stick ... and other relevant data from Cell (username tweet etc etc) no longer appear – Lcstest Test Jan 21 '13 at 18:50
  • I really agreed with what you said Rob. Just had another point to look at it. And of course it all depends on context. Btw, while you cannot control number of connections with dispatch_async() in situation like this, it is limited anyaways to 5 (not sure about) if you use imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString:url]]; – Cynichniy Bandera Jan 21 '13 at 19:03
  • Thanks guys, this is nice post, I've got something useful from it :) – Cynichniy Bandera Jan 21 '13 at 19:04
  • TweHitLogo and placeholder images are the same only the name changes – Lcstest Test Jan 21 '13 at 19:29
  • 2
    was my mistake, now is running PERFECTLY :D Thanks friend, I'm trying to work this week. – Lcstest Test Jan 21 '13 at 19:36
  • @Umka Doing some other testing, I was looking at the number of concurrent requests `dispatch_async` will handle. In my anecdotal testing on both iOS 6.0 devices (iPhone 5, iPhone 3GS, iPad 3) and 5.1 device, it looks like `dispatch_async` does 64 concurrent operations, not 5. This illustrates the importance of `maxConcurrentOperationCount` of `NSOperationQueue`. – Rob Jan 24 '13 at 08:43
1

A good example from Ray Wenderlich : http://www.raywenderlich.com/19788/how-to-use-nsoperations-and-nsoperationqueues

It also having cancel function to cancel operation in case user press cancel button.

NSPratik
  • 4,714
  • 7
  • 51
  • 81
0

Download async images in tableview using swift 3

class ViewController: UIViewController {
    var queue = OperationQueue()

    let imageURLs = ["https://amazingslider.com/wp-content/uploads/2012/12/dandelion.jpg", "https://media.treehugger.com/assets/images/2011/10/tiger-running-snow.jpg.600x315_q90_crop-smart.jpg", "https://www.w3schools.com/css/trolltunga.jpg", "https://www.w3schools.com/w3css/img_lights.jpg", "https://cdn.pixabay.com/photo/2015/04/28/20/55/aurora-borealis-744351_960_720.jpg", "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS2L0pETnybA5sld783iz1mgtOFS8vxBTjB4tYXeRtQWDxig3dc"]

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }


}

extension ViewController: UITableViewDelegate, UITableViewDataSource {
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return imageURLs.count
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell: ImagesTableViewCell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! ImagesTableViewCell
        var img: UIImage!
        let operation = BlockOperation(block: {
            img  = Downloader.downloadImageWithURl(self.imageURLs[indexPath.row])
        })

        operation.completionBlock = {
            DispatchQueue.main.async {
                cell.imgView?.image = img
            }

        }
        queue.addOperation(operation)

        return cell
    }
}

class Downloader {
    class func downloadImageWithURl(_ url: String) -> UIImage! {
        if let data = try? Data(contentsOf: URL(string: url)!) {
            return UIImage(data: data)!
        }
        return nil
    }

}
Raj Joshi
  • 2,669
  • 2
  • 30
  • 37