2

So I've asked a couple of questions regarding the UICollectionView. Understanding how it works, I'm trying to implement lazy loading to load 15 images onto the view controller. I found many examples 1, 2, 3...first and third examples deal with only one operation, second example I don't think uses operations at all, only threads. My question is would it be possible to use a NSOperation class and use/reuse operations? I read that you can't rerun operations but I think you are able to once you initialize them again. Here's my code:

view controller:

UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc]init];
layout.sectionInset = UIEdgeInsetsMake(10, 20, 10, 20);
[layout setItemSize:CGSizeMake(75, 75)];
self.images = [[UICollectionView alloc]initWithFrame:CGRectMake(0, 230, self.view.frame.size.width, 200) collectionViewLayout:layout];
self.images.delegate = self;
self.images.dataSource = self;
[self.images registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"cellIdentifier"];
[self.view addSubview:self.images];
self.operation = [[NSOperationQueue alloc]init];
[self.operation addOperationWithBlock:^{
    NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"getgallery.php?user=%@", userId] relativeToURL:[NSURL URLWithString:@"http://www.mywebsite.com/"]];
    NSData *data = [NSData dataWithContentsOfURL:url];
//datasource for all images
    self.imagesGalleryPaths = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
    [[NSOperationQueue mainQueue]addOperationWithBlock:^{
        [self.images reloadData];
//reload collection view to place placeholders
    }];
}];

- (void)viewDidAppear:(BOOL)animated{
//get the visible cells right away
visibleCellPaths = [NSArray new];
visibleCellPaths = self.images.indexPathsForVisibleItems;
self.processedImages = [[NSMutableDictionary alloc]initWithCapacity:visibleCellPaths.count];
}
#pragma mark - collection view
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView{
return 1;
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
int i;
if (self.imagesGalleryPaths.count == 0)
    i = 25;
else
    i = self.imagesGalleryPaths.count;
return i;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
UICollectionViewCell *cell = [UICollectionViewCell new];
cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cellIdentifier" forIndexPath:indexPath];
cell.layer.borderWidth = 1;
cell.layer.borderColor = [[UIColor whiteColor]CGColor];
UIImageView *image = [[UIImageView alloc]init];
if (self.imagesGalleryPaths.count != 0) {
    if ([visibleCellPaths containsObject:indexPath]) {
        [self setUpDownloads:visibleCellPaths];
    }
    image.image = [UIImage imageNamed:@"default.png"];
    image.frame = CGRectMake(0, 0, cell.frame.size.width, cell.frame.size.height);
    [cell.contentView addSubview:image];
}
return cell;
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
visibleCellPaths = [NSArray new];
visibleCellPaths = self.images.indexPathsForVisibleItems;
[self setUpDownloads:visibleCellPaths];
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
visibleCellPaths = [NSArray new];
visibleCellPaths = self.images.indexPathsForVisibleItems;
[self setUpDownloads:visibleCellPaths];
}

- (void)setUpDownloads:(NSArray *)visiblePaths{
//I want to pass the visiblePaths to the NSOperation class if visible cells changed
GalleryOps *gallery = [[GalleryOps alloc]init];
//I will use a dictionary to keep track of which indexPaths are being downloaded...
//...so there are no duplicate downloads
}

GalleryOps.m

@implementation GalleryOps

- (void)main{
    //would I initialize all the operations here and perform them?
}

It's almost pointless to show the empty GalleryOps class because I have no idea how to initialize it with multiple operations. I know I have to override the main method, and once I get the image data from URL, I'll need to update the UI, for which I need a delegate method...another thing I don't yet know how to do but there are many examples to figure that out. My biggest question is how to pass the visible cells into this class and run multiple operations? When new visible cells come in, I'll run a check to see which to cancel, which to keep. Any advice here? Thanks in advance!

Community
  • 1
  • 1
denikov
  • 877
  • 2
  • 17
  • 35
  • Why would you not have an operation instance per download? Then you can control how many run concurrently with the queue. – Wain Apr 24 '14 at 17:56
  • Can you give me an example? In the NSOperation class or in the view controller? – denikov Apr 24 '14 at 18:06
  • 1
    You're already doing `addOperationWithBlock:`, that adds an operation instance to the queue - can you do the same thing again... – Wain Apr 24 '14 at 18:10
  • @Rob, I understand there are many libraries but as part of the learning process, I want to learn how the basics work before making my life easier. – denikov Apr 24 '14 at 18:26
  • @Wain, I think you are right. I can just loop through the visible cells with the queue block operation, all the while checking what to pass as the parameter indexPath, comparing already loaded images, and one by one insert images. Cancel all operations on viewWillDisappear. Might be the best way to go now if it's impossible to do multiple NSOperations. – denikov Apr 24 '14 at 19:02
  • @Rob If I subclass NSOperation, does that still mean that I can only run one operation at a time? Going back to my example, would I only be able to load one image at a time? And what do you think to my comment back to Wain? Would that logic work? – denikov Apr 24 '14 at 20:40
  • @denikov First, no, if you subclass `NSOperation`, you're not limited to running one at a time. In fact, I suggest using `maxConcurrentOperationCount` of 4 or 5 (but not greater) on your `NSOperationQueue`. Second, no you don't want to "loop through the visible cells". Generally people just use `cellForItemAtIndexPath` to trigger the asynchronous image retrieval, not any loop. That's much easier, IMHO. – Rob Apr 24 '14 at 20:57

4 Answers4

1

You can use the queue and operations themselves to manage multiple operations. If I am reading your question correctly, you have one operation (get the list of image URLs from JSON) that you want to spawn child operations. You can do this by having the parent operation add the child operations to the queue, or by using dependent operations (child would have the parent as a dependancy).

For what you're trying to do you do not need to subclass NSOperation, NSBlockOperation should meet your needs. Subclassing NSOperation is trickier than it looks because of the KVO dependancies (it's very easy to get wrong).

But to the specifics of your question:

My question is would it be possible to use a NSOperation class and use/reuse operations? I read that you can't rerun operations but I think you are able to once you initialize them again

If you're initializing them again they're new objects (or at least, they should be). NSOperations can't be re-run because they have internal state - the tricky KVO bits I mention above. Once they go to "finished", that instance can't be returned to a clean state.

Operations should be fairly lightweight objects and there should not be any significant value in reusing them, and plenty of potential trouble. Creating new operations should be the way to go.

The Apple sample code "LazyTableImages" may give you some hints as how to accomplish what you're trying to do.

quellish
  • 21,123
  • 4
  • 76
  • 83
  • Thanks for answering my question. Using block operations, can they be canceled? I mean, in my code I update the visible cells when user scrolls...would I be able to somehow check if that current operation's indexPath is part of the new visible cells? If not, cancel the operation? – denikov Apr 24 '14 at 18:29
  • @denikov No, you would want to subclass `NSOperation` if you want to make your operations cancelable. It's more complicated, as quellish says, but it's the right way to do it. – Rob Apr 24 '14 at 20:01
  • I think I understand...because you still add a block operation to the queue and you can only cancel the queue, not the operation itself. – denikov Apr 24 '14 at 20:41
  • @denikov The problem with block operations is that you cannot cancel the operations that are in progress. (You can cancel the ones that haven't started yet, but not the ones currently running.) To do that, the operation has to be programmed to check to see if it's canceled, and the default concrete `NSOperation` classes (e.g. `NSBlockOperation`) don't offer you that capability. That's why you have to subclass `NSOperation`, so you can write your own cancellation logic. – Rob Apr 24 '14 at 20:44
  • NSBlockOperation can be cancelled. See WWDC 2012 session "Building Concurrent User Interfaces". – quellish Apr 25 '14 at 00:23
  • @quellish Quite right. You can cancel non-concurrent operations that consist of some loop like shown in that video. But it's not really the right pattern if you want to create a cancelable network operation. – Rob Apr 25 '14 at 01:28
  • @Rob, denikov did not specify that he wanted to cancel a network operation, only that he was interested in canceling a block operation. Creating an NSOperation that manages a cancelable NSURLConnection - correctly - is not trivial (see MVCNetworking). If you need a cancelable network connection that runs on a specific thread you can use NSURLSession and friends with far less pain. – quellish Apr 25 '14 at 06:17
1

The constituent elements of an NSOperation-based lazy loading of images might include:

  1. Create a dedicated NSOperationQueue that will be used for the download operations. Generally this is configured with a maxConcurrentOperationCount of 4 or 5 so that you enjoy concurrency, but so that you won't exceed the maximum number of concurrent network operations.

    If you don't use this maxConcurrentOperationCount, with slow network connections (e.g. cellular), you risk having network requests time out.

  2. Have a model object (e.g. an array) that backs your collection view or table view. This would generally only have some identifier for the image (e.g. the URL) not the image itself.

  3. Implement a cache mechanism to store the downloaded images, to prevent the need to re-download images that have already been downloaded. Some implementations only do memory based cache (via NSCache). Others (e.g. SDWebImage) will do two tiers of cache, both memory (NSCache, for optimal performance) and a secondary persistent storage cache (so that when memory pressure forces you to purge the NSCache, you still have a rendition saved on the device so you don't have to re-retrieve it from the network). Others (e.g. AFNetworking) rely upon NSURLCache to cache the responses from the server into persistent storage.

  4. Write a NSOperation subclass for downloading a single image. You want to make this cancelable operation. That implies two different design considerations

    • First, regarding the operation itself, you probably want to make a concurrent operation (see the Configuring Operations for Concurrent Execution section in the Concurrency Programming Guide).

    • Second, regarding the network request, you want a cancelable network request. If using NSURLConnection, this means using the delegate-based rendition (not any of the convenience methods). And if using NSURLConnection, the trick is that you have to schedule it in a run loop that persists. There are a number of tricks to accomplish this, but the easiest is to schedule it (with scheduleInRunLoop:forMode:) in the main run loop (though there are more elegant approaches), even though you will be running this from an operation in an NSOperationQueue. Personally I launch a new dedicated thread (like AFNetworking does) for this purpose, but the main run loop is easier and is fine for this sort of process.

      If using NSURLSession, this process is conceivably a tad easier, because you can get away with using the completion block rendition of dataTaskWithRequest and not get into the delegate-based implementation if you don't want to. But this is iOS 7+ only (and if you need to do anything fancy like handle authentication challenge requests, you'll end up going the delegate-based approach anyway).

    • And combining those two prior points, the custom NSOperation subclass would detect when the operation is canceled and then cancel the network request and complete the operation.

    By the way, instances of operations are never reused. You create a new operation instance for each image you are downloading.

  5. By the way, if the images you've downloaded are large (e.g. they have dimensions greater than the number of pixels that the image view needs), you may want to resize the images before using them. When JPG or PNG images are downloaded, they are compressed, but when you use them in an image view they are uncompressed, usually require 4 bytes per pixel (e.g. a 1000x1000 image will require 4mb, even though the JPG is much smaller than that).

    There are lots of image resizing algorithms out there, but here is one: https://stackoverflow.com/a/10859625/1271826

  6. You will want a cellForItemAtIndexPath that then pulls the above pieces together, namely:

    • Check to see if the image is already in the cache, if so, use it.

    • If not, you will start a network request to retrieve the image. You might want to see if this cell (which may be a reused cell from your table view) already has an image operation already in progress, and if so, just cancel it.

      Anyway, you can then instantiate a new NSOperation subclass for the downloading of the image and have the completion block update the cache and then also cell's image view.

    • By the way, when you asynchronously update the cell's image view, make sure the cell is still visible and that the cell hasn't been reused in the interim. You can do this my calling [collectionView cellForItemAtIndexPath:indexPath] (which should not be confused with the similarly named UICollectionViewDataSource method that you're writing here).

Those are the constituent parts of the process, and I'd suggest you tackle them one at a time. There's a lot involved in writing an elegant implementation of lazy loading.

The easiest solution is to consider using an existing UIImageView category (such as provided with SDWebImage) which does all of this for you. Even if you don't use that library, you'll might be able to learn quite a bit by reviewing the source code.

Community
  • 1
  • 1
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Thanks Rob for that lengthy explanation. Even if most of this is way over my head, I'll try to break everything down tomorrow and try to make sense of it. Thanks again! – denikov Apr 24 '14 at 21:16
  • Since iOS 5, NSURLCache does not just in memory caching, but on disk as well. Adding additional caching layers that ignore the HTTP caching instructions sent by the server can be adding complexity just for complexity's sake. The server sends a response and (should) tell the client how long that response is valid for and when it should be revalidated against the server. The URL loading system does all of this for you. – quellish Apr 25 '14 at 00:28
  • @quellish In theory, `NSURLCache` can provide persistent caching, but in practice, it's sometimes problematic. And if you're doing any image manipulation, it can be the wrong level of abstraction (because you may want to cache the manipulated image, not the original response). But some advocate this approach, e.g. AFNetworking (and it's been debated actively on their GitHub "issues" page), so added that alternative to my list of caching options. – Rob Apr 25 '14 at 01:45
1

Looking at your proposed solution, it looks like you want to defer the question of making the operations cancelable. Furthermore, it looks like you want to defer the use of the cache (even though it's no more complicated than your NSMutableDictionary property).

So, setting that aside, your revised code sample has two "big picture" issues:

  1. You can dramatically simplify the image retrieval process. The use of startOperationForVisibleCells and the two scroll delegates is unnecessarily complicated. There is a much simpler pattern in which you can retire those three methods (and achieve an even better UX).

  2. Your cellForItemForIndexPath has a problem, that you're adding subviews. The issue is that cells are reused, so every time a cell is reused, you're adding more redundant subviews.

    You really should subclass UICollectionViewCell (CustomCell in my example below), put the configuration of the cell, including the adding of subviews, there. It simplifies your cellForItemAtIndexPath and eliminates the problem of extra subviews being added.

In addition to these two major issues, there were a bunch of little issues:

  1. You neglected to set maxConcurrentOperationCount for your operation queue. You really want to set that to 4 or 5 to avoid operation timeout errors.

  2. You are keying your imageGalleryData with the NSIndexPath. The problem is that if you ever deleted a row, all of your subsequent indexes would be wrong. I suspect this isn't an issue right now (you're probably not anticipating deleting of items), but if you keyed it by something else, such as the URL, it's just as easy, but it is more future-proof.

  3. I'd suggest renaming your operation queue from operation to queue. Likewise, I'd rename the UICollectionView from images (which might be incorrectly inferred to be an array of images) to something like collectionView. This is stylistic, and you don't have to do that if you don't want, but it's the convention I used below.

  4. Rather than saving the NSData in your NSMutableDictionary called imageGalleryData, you might want to save the UIImage instead. This saves you from having to reconvert from NSData to UIImage (which should make the scrolling process smoother) as you scroll back to previously downloaded cells.

So, pulling that all together, you get something like:

static NSString * const kCellIdentifier = @"CustomCellIdentifier";

- (void)viewDidLoad
{
    [super viewDidLoad];

    UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc]init];
    layout.sectionInset = UIEdgeInsetsMake(10, 20, 10, 20);
    [layout setItemSize:CGSizeMake(75, 75)];

    // renamed `images` collection view to `collectionView` to follow common conventions

    self.collectionView = [[UICollectionView alloc]initWithFrame:CGRectMake(0, 230, self.view.frame.size.width, 200) collectionViewLayout:layout];
    self.collectionView.delegate = self;
    self.collectionView.dataSource = self;

    // you didn't show where you instantiated this in your examples, but I'll do it here

    self.imageGalleryData = [NSMutableDictionary dictionary];

    // register a custom class, not `UICollectionViewCell`

    [self.collectionView registerClass:[CustomCell class] forCellWithReuseIdentifier:kCellIdentifier];
    [self.view addSubview:self.collectionView];

    // (a) change queue variable name;
    // (b) set maxConcurrentOperationCount to prevent timeouts

    self.queue = [[NSOperationQueue alloc]init];
    self.queue.maxConcurrentOperationCount = 5;

    [self.queue addOperationWithBlock:^{
        NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"getgallery.php?user=%@", userId] relativeToURL:[NSURL URLWithString:@"http://www.mywebsite.com/"]];
        NSData *data = [NSData dataWithContentsOfURL:url];
        //datasource for all images
        self.imagesGalleryPaths = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
        [[NSOperationQueue mainQueue]addOperationWithBlock:^{
            [self.collectionView reloadData];
        }];
    }];
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return [self.imagesGalleryPaths count];          // just use whatever is the right value here, don't make this unnecessarily smaller
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    CustomCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kCellIdentifier forIndexPath:indexPath];

    NSString *key = self.imagesGalleryPaths[indexPath.row];                 // I don't know whether this was simply array, or some nested structure, so tweak this accordingly
    UIImage *image = self.imageGalleryData[key]; 

    if (image) {
        cell.imageView.image = image;                                       // if we have image already, just use it
    } else {
        cell.imageView.image = [UIImage imageNamed:@"profile_default.png"]; // otherwise set the placeholder ...

        [self.queue addOperationWithBlock:^{                                // ... and initiate the asynchronous retrieval
            NSURL *url = [NSURL URLWithString:...];                         // build your URL from the `key` as appropriate
            NSData *responseData = [NSData dataWithContentsOfURL:url];
            if (responseData != nil) {
                UIImage *downloadedImage = [UIImage imageWithData:responseData];
                if (downloadedImage) {
                    [[NSOperationQueue mainQueue]addOperationWithBlock:^{
                        self.imageGalleryData[key] = downloadedImage;
                        CustomCell *updateCell = (id)[collectionView cellForItemAtIndexPath:indexPath];
                        if (updateCell) {
                            updateCell.imageView.image = downloadedImage;
                        }
                    }];
                }
            }
        }];
    }

    return cell;
}

// don't forget to purge your gallery data if you run low in memory

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    [self.imageGalleryData removeAllObjects];
}

Now, clearly I don't have access to your server, so I couldn't check this (notably, I don't know if your JSON is returning a full URL or just a filename, or whether there was some nested array of dictionaries). But I don't want to you to get too lost in the details of my code, but rather look at the basic pattern: Eliminate your looping through visible cells and responding to scroll events, and let cellForItemAtIndexPath do all the work for you.

Now, the one thing that I introduced was the concept of CustomCell, which is a UICollectionViewCell subclass that might look like:

//  CustomCell.h

#import <UIKit/UIKit.h>

@interface CustomCell : UICollectionViewCell

@property (nonatomic, weak) UIImageView *imageView;

@end

and then move cell configuration and adding of the subview here to the @implementation:

//  CustomCell.m

#import "CustomCell.h"

@implementation CustomCell

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.layer.borderWidth = 1;
        self.layer.borderColor = [[UIColor whiteColor]CGColor];

        UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)];
        [self addSubview:imageView];
        _imageView = imageView;
    }
    return self;
}
    
@end

By the way, I still contend that if you want to do this properly, you should refer to my other answer. And if you don't want to get lost in those weeds of the proper implementation, then just use a third party UIImageView category that supports asynchronous image retrieval, caching, prioritizing network requests of visible cells, etc., such as SDWebImage.

Community
  • 1
  • 1
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Thanks for that. I think I mostly understand your process but I don't understand why you deleted the scrolling functions...wouldn't your code just download all the images, even if they are not on the screen? Doesn't your example gets rid of lazy loading? – denikov Apr 28 '14 at 16:14
  • @denikov No, `cellForItemAtIndexPath` is called only for visible cells, and called again for those cells that appear as you scroll. Thus all of your custom scrolling stuff is unnecessary. The only time you'd do custom scrolling stuff is if you were going to fetch not only visible cells, but also try prefetching cells that are not currently visible. But there are many optimizations that are far more important than that. – Rob Apr 28 '14 at 16:16
  • Wow, didn't know that! Thanks again. I'll redo this and continue learning delegates and operations. By the way, I have a question about delegates but it's probably not the best for SO...is there any way you could help? It's from this example: http://www.raywenderlich.com/19788/how-to-use-nsoperations-and-nsoperationqueues#comments. About reason for passing delegates in the init method...hard to wrap my head around delegates – denikov Apr 28 '14 at 17:21
  • I understand the setting of the custom init but I'm confused about the passing of the delegate of "self" from the "root" view controller to the imagedownloader. Then in imagedownloader it's setting the imagedownloader delegate as the "self" from the root controller. So the delegate of imagedownloader is the root??? I'm confused on what that's saying in general terms. Could you explain that part in plain english for me? :) – denikov Apr 28 '14 at 19:03
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/51622/discussion-between-rob-and-denikov) – Rob Apr 28 '14 at 19:14
0

I figure my collection view is not something the user will come back to over and over again, just once in a while. So no reason to cache all the images.

The viewDidAppear is still the same, I get the visible cells right away. The reason why initially I put 25 cells into numberOfItems... is just to get the visible cells right away. So now my cellForItemAtIndexPath is this:

UICollectionViewCell *cell = [UICollectionViewCell new];
cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cellIdentifier" forIndexPath:indexPath];
cell.layer.borderWidth = 1;
cell.layer.borderColor = [[UIColor whiteColor]CGColor];
UIImageView *image = [[UIImageView alloc]init];
if (self.imagesGalleryPaths.count != 0) {
    image.image = [UIImage imageNamed:@"profile_default.png"];
    image.frame = CGRectMake(0, 0, cell.frame.size.width, cell.frame.size.height);
    [cell.contentView addSubview:image];
}
return cell;

In the viewDidLoad I added this:

if (self.imagesGalleryPaths.count != 0) {
    [[NSOperationQueue mainQueue]addOperationWithBlock:^{
        [self.images reloadData];
        [self startOperationForVisibleCells];
    }];
}

This is my startOperationForVisibleCells:

[self.operation addOperationWithBlock:^{
    int i=0;
    while (i < visibleCellPaths.count) {
        NSIndexPath *indexPath = [visibleCellPaths objectAtIndex:i];
        if (![self.imageGalleryData.allKeys containsObject:indexPath]) {
            NSURL *url = [@"myurl"];
            NSData *responseData = [NSData dataWithContentsOfURL:url];
            if (responseData != nil) {
                [self.imageGalleryData setObject:responseData forKey:indexPath];
                [[NSOperationQueue mainQueue]addOperationWithBlock:^{
                    UICollectionViewCell *cell = [self.images cellForItemAtIndexPath:indexPath];
                    UIImageView *image = [UIImageView new];
                    image.image = [UIImage imageWithData:responseData];
                    image.frame = CGRectMake(0, 0, cell.frame.size.width, cell.frame.size.height);
                    [cell.contentView addSubview:image];
                }];
            }
        }
        i++;
    }
}];

And that's how I update the cells one by one. Also when the user scrolls away:

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
visibleCellPaths = [NSArray new];
visibleCellPaths = self.images.indexPathsForVisibleItems;
for (int i=0; i<visibleCellPaths.count; i++) {
    NSIndexPath *indexPath = [visibleCellPaths objectAtIndex:i];
    if ([self.imageGalleryData.allKeys containsObject:indexPath]) {
        UICollectionViewCell *cell = [self.images cellForItemAtIndexPath:indexPath];
        UIImageView *image = [UIImageView new];
        image.image = [UIImage imageWithData:[self.imageGalleryData objectForKey:indexPath]];
        image.frame = CGRectMake(0, 0, cell.frame.size.width, cell.frame.size.height);
        [cell.contentView addSubview:image];
    }else{
        [self startOperationForVisibleCells];
    }
}
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
//same functions as previous
}

I am sure this is a very bad way of doing it but for now, it works. The images are loaded one by one and they stop loading when the user scrolls away.

miken32
  • 42,008
  • 16
  • 111
  • 154
denikov
  • 877
  • 2
  • 17
  • 35