1

I'm currently developing a JSON TableView with the information from my database (some products with the image and their names). Everything is fine but it's very slow when I scroll down (or up). I have made a lot of research regarding this topic but I have tried their codes and I still don't know how to store the images in the cache to speed up the TableView.

Here is my code:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! BuscarCellTableViewCell

    if searchController.active {
        cell.nombre.text = searchResults[indexPath.row].nombre

        cell.marca.text = searchResults[indexPath.row].marca

        if let url = NSURL(string: searchResults[indexPath.row].imagen) {
            if let data = NSData(contentsOfURL: url) {
                cell.imagen.image = UIImage(data: data)
            }
        }
    }
    else {
        cell.nombre.text = productos[indexPath.row].nombre

        cell.marca.text = productos[indexPath.row].marca

        if let url = NSURL(string: productos[indexPath.row].imagen) {
            if let data = NSData(contentsOfURL: url) {
                cell.imagen.image = UIImage(data: data)
            }
        }
    }

    return cell
}

override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {

    // Define the initial state (Before the animation)
    cell.alpha = 0.25

    // Define the final state (After the animation)
    UIView.animateWithDuration(1.0, animations: { cell.alpha = 1 })
}

func getLatestLoans() {
    let request = NSURLRequest(URL: NSURL(string: LoadURL)!)
    let urlSession = NSURLSession.sharedSession()
    let task = urlSession.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in

        let res = response as! NSHTTPURLResponse!
        var err: NSError?

        if error != nil {
            println(error.localizedDescription)
        }

        // Parse JSON data
        self.productos = self.parseJsonData(data)

        // Reload table view
        dispatch_async(dispatch_get_main_queue(), {
            self.tableView.reloadData()
        })
    })

    task.resume()
}

func parseJsonData(data: NSData) -> [Producto] {
    var productos = [Producto]()
    var error:NSError?

    let jsonResult = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: &error) as? NSDictionary

    // Return nil if there is any error
    if error != nil {
        println(error?.localizedDescription)
    }

    // Parse JSON data
    let jsonProductos = jsonResult?["lista_productos"] as! [AnyObject]
    for jsonProducto in jsonProductos {

        let producto = Producto()
        producto.nombre = jsonProducto["nombre"] as! String
        producto.imagen = jsonProducto["imagen"] as! String

        productos.append(producto)
    }

    return productos
}

How can I speed up my TableView when scrolling down the "products" from my JSON file?

Thanks in advance,

Regards.

Jordi Gámez
  • 3,400
  • 3
  • 22
  • 35

3 Answers3

3

cellForRowAtIndexPath: gets called for each new/reused cell rendering. You should not make server call in this method. This is how you should do it:

  1. After your data object "productos" is populated, call a new method fetchImages which can trigger NSOperations in background to fetch the images.
  2. While images are being fetched, you can show loading overlay on each cell image view.
  3. Once image response comes back, remove loading overlay and refresh the cell.
  4. Cache the images in file system to avoid refetch them again while user is scrolling up and down through your table view.

Follow this and you should see desired improvement in your application scrolling behaviour.

EDIT: On OP Request:

Step 1: Expecting you to call below fetchImages method as soon as model data is loaded from server. If you are loading data locally then call fetchImages in loadView:

- (void)fetchImages {
    NSMutableArray *anImageURLsList = [NSMutableArray new]; // Assuming this contains your image URL list

    if ([anImageURLsList count] > 0) {
        __weak MyController *aBlockSelf = self;

        [[MyImageFetchController sharedImageFetchController] addURLs:anImageURLsList withDescription:[self description] andCompletionBlock:^(NSMutableArray *iFailedURLs) {
            dispatch_async(dispatch_get_main_queue(), ^{[UIApplication sharedApplication].networkActivityIndicatorVisible = NO; });

            if (aBlockSelf) {
                [[MyImageFetchController sharedImageFetchController].objectTokens seValue:[NSString stringWithFormat:@"%d", NO] forKey:[aBlockSelf description]];
                [aBlockSelf performSelectorOnMainThread:@selector(refresh) withObject:nil waitUntilDone:YES];
            }
        }];

        [[MyImageFetchController sharedImageFetchController].objectTokens setValue:[NSString stringWithFormat:@"%d", YES] forKey:[self description]];
        [MyImageFetchController sharedImageFetchController].delegate = self;
        [[MyImageFetchController sharedImageFetchController] startOperations];

    dispatch_async(dispatch_get_main_queue(), ^{[UIApplication sharedApplication].networkActivityIndicatorVisible = YES; });
    }
}


- (void)refresh {
    [self.tableView reloadData];
}

Step 2: Now lets write NSOperation subclass to fetch images via operation queue. Below code covers only important aspect with respect to the discussion here. Here, I've given a delegate call back for image caching implementation.

- (void)addURLs:(NSMutableArray *)iURLs withDescription:(NSString *)iObjectDescription andCompletionBlock:(ImageFetchCompletionBlock)iImageFetchCompletionBlock {
    self.urls = iURLs;
    [self.objectTokens removeAllObjects];
    self.imageFetchCompletionBlock = iImageFetchCompletionBlock;
    self.objectDescription = iObjectDescription;

    if (self.failedURLs) {
        [self.failedURLs removeAllObjects];
    }
}


- (void)urlFailed:(NSString *)iFailedURL {
    @synchronized(self) {
        if (iFailedURL) {
            [self.failedURLs addObject:iFailedURL];
        }
    }
}


- (void)startOperations {
    MyImageFetchOperation *anImageFetchOperation = nil;
    NSMutableArray *anOperationList = [[NSMutableArray alloc] initWithCapacity:self.urls.count];
    self.queue = [NSOperationQueue new];

    for (NSString *aURL in [self.urls copy]) {
        anImageFetchOperation = [[MyImageFetchOperation alloc] initWithImageURL:aURL delegate:self];
        [anOperationList addObject:anImageFetchOperation];
    }

    [self.queue setMaxConcurrentOperationCount:3];
    [self.queue addOperations:anOperationList waitUntilFinished:NO];
}


- (void)operationCompletedForURL:(NSString *)iImageURL {
    @synchronized(self) {
        [self.urls removeObject:iImageURL];

        if ([self.urls count] == 0) {
            if ([[self.objectTokens valueForKey:self.objectDescription] boolValue]) {
                self.imageFetchCompletionBlock([self.failedURLs mutableCopy]);
            } else {
                dispatch_async(dispatch_get_main_queue(), ^{[UIApplication sharedApplication].networkActivityIndicatorVisible = NO; });
            }

            if (self.failedURLs && [self.failedURLs count] > 0) {
                [self.failedURLs removeAllObjects];
            }

            self.queue = nil;
        }
    }
}


- (NSString *)cacheDirectoryForImages {
    NSString *anImageCacheDirectory = kImageCacheDirectoryKey; // Your image cache directory 

    if (self.delegate && [self.delegate respondsToSelector:@selector(cacheDirectoryForImages)]) {
        anImageCacheDirectory = [self.delegate cacheDirectoryForImages];
    }

    return anImageCacheDirectory;
}

Step 3: Now, lets write cellForRowAtIndexPath. Here, I am using a custom cell which implements an imageView and also expecting a custom loading overlay. You can put your loading overlay here.

- (UITableViewCell *)tableView:(UITableView *)iTableView cellForRowAtIndexPath:(NSIndexPath *)iIndexPath {
    NSString *cellType = @"defaultCell";
    MyCustomTableViewCell *cell = (MyCustomTableViewCell *)[iTableView dequeueReusableCellWithIdentifier:cellType];

    if (!cell) {
        cell = [[MyCustomTableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:aCellType];
    }

    cell.textLabel.text = @“Dummy”; // Set your cell data

    NSString *anImageURL = self.productos[iIndexPath.row][@“image"];

    [cell.loadingOverlay removeView];
    cell.loadingOverlay = nil;

    // If image is present in Cache
    if (anImageURL && [anImageURL existsInCache]) {
        cell.imageView.image = [UIImage imageNamed:<Image from cache>];
    } else if ([[[MyImageFetchController sharedImageFetchController].objectTokens valueForKey:[self description]] boolValue]) {
        cell.imageView.image = [UIImage imageNamed:defaultImage];
        cell.loadingOverlay = [MyLoadingOverlay loadingOverlayInView:aCell.imageView];
    } else {
        cell.imageView.image = [UIImage imageNamed:defaultImage];
    }

    return cell;
}
Abhinav
  • 37,684
  • 43
  • 191
  • 309
  • Hi @Abhinav, I appreciate your guidelines and I will follow them. Unfortunately, I'm not an expert in the iOS Development and I really appreciate some code because I don't know where to start...! Thanks in advance. – Jordi Gámez Sep 16 '15 at 08:10
  • Sure, give me few minutes to come up with some helper code for you. It won't be all connecting but would give you guidance on how to do this. – Abhinav Sep 16 '15 at 08:14
  • @Abhinav: No need for that, there are plenty of examples for example on Apple's website. – gnasher729 Sep 16 '15 at 08:30
  • I had quickly written something and have pasted here :). Please excuse for some of the dirty coding standards! – Abhinav Sep 16 '15 at 08:34
  • Much appreciated @Abhinav, I will follow your code and I will try to extend it to my example. Regards :) – Jordi Gámez Sep 16 '15 at 08:46
  • No worries buddy! All the best. – Abhinav Sep 16 '15 at 08:47
  • I'm not able to do this example because I'm really lost, I need more information and some tutorial or step by step to follow...! Regards. – Jordi Gámez Sep 22 '15 at 08:01
0

for Lazy loading you should use SDWebImage Library . It provides you the catching of images & also provides the functionality to add loader when the image is loading for each cell.
Follow this link to integrate SDWebImage in your app.
https://github.com/rs/SDWebImage

Akshay Agrawal
  • 217
  • 3
  • 12
  • Thanks @Akshay, is it a third party accepted by Apple? Regards – Jordi Gámez Sep 16 '15 at 08:45
  • Yes, third party are most welcome. In general you should not create solutions for problems that has already been solved. Regarding to image loading - please see [this answer](http://stackoverflow.com/questions/32602784/speed-up-tableview-in-swift-loading-json/32605691#32605691) – kean Sep 16 '15 at 10:13
0

Regarding image loading performance (move it to background thread, store image into memory cache, etc). Here are duplicate questions:

display downloaded images in tableview

Async image loading from url inside a UITableView cell - image changes to wrong image while scrolling

Community
  • 1
  • 1
kean
  • 1,561
  • 14
  • 22