Jordi Gámez
Jordi Gámez

Reputation: 3543

Speed up TableView in Swift loading JSON

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.

Upvotes: 1

Views: 1370

Answers (3)

kean
kean

Reputation: 1561

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

Upvotes: 0

Abhinav
Abhinav

Reputation: 38162

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;
}

Upvotes: 3

Akshay Agrawal
Akshay Agrawal

Reputation: 217

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

Upvotes: 0

Related Questions