2

I have a search bar and a table view under it. When I search for something a network call is made and 10 items are added to an array to populate the table. When I scroll to the bottom of the table, another network call is made for another 10 items, so now there is 20 items in the array... this could go on because it's an infinite scroll similar to Facebook's news feed.

Every time I make a network call, I also call self.tableView.reloadData() on the main thread. Since each cell has an image, you can see flickering - the cell images flash white.

I tried implementing this solution but I don't know where to put it in my code or how to. My code is Swift and that is Objective-C.

Any thoughts?

Update To Question 1

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier(R.reuseIdentifier.searchCell.identifier, forIndexPath: indexPath) as! CustomTableViewCell

    let book = booksArrayFromNetworkCall[indexPath.row]

    // Set dynamic text
    cell.titleLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
    cell.authorsLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleFootnote)

    // Update title
    cell.titleLabel.text = book.title

    // Update authors
    cell.authorsLabel.text = book.authors

    /*
    - Getting the CoverImage is done asynchronously to stop choppiness of tableview.
    - I also added the Title and Author inside of this call, even though it is not
    necessary because there was a problem if it was outside: the first time a user
    presses Search, the call for the CoverImage was too slow and only the Title
    and Author were displaying.
    */
    Book.convertURLToImagesAsynchronouslyAndUpdateCells(book, cell: cell, task: task)

    return cell
}

cellForRowAtIndexPath uses this method inside it:

    class func convertURLToImagesAsynchronouslyAndUpdateCells(bookObject: Book, cell: CustomTableViewCell, var task: NSURLSessionDataTask?) {

    guard let coverImageURLString = bookObject.coverImageURLString, url = NSURL(string: coverImageURLString) else {
        return
    }

    // Asynchronous work being done here.
    task = NSURLSession.sharedSession().dataTaskWithURL(url, completionHandler: { (data, response, error) -> Void in

        dispatch_async(dispatch_get_main_queue(), {

            // Update cover image with data

            guard let data = data else {
                return
            }

            // Create an image object from our data
            let coverImage = UIImage(data: data)
            cell.coverImageView.image = coverImage
        })
    })

    task?.resume()
}

When I scroll to the bottom of the table, I detect if I reach the bottom with willDisplayCell. If it is the bottom, then I make the same network call again.

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

    if indexPath.row+1 == booksArrayFromNetworkCall.count {

        // Make network calls when we scroll to the bottom of the table.
        refreshItems(currentIndexCount)
    }

}

This is the network call code. It is called for the first time when I press Enter on the search bar, then it is called everytime I reach the bottom of the cell as you can see in willDisplayCell.

    func refreshItems(index: Int) {

    // Make to network call to Google Books
    GoogleBooksClient.getBooksFromGoogleBooks(self.searchBar.text!, startIndex: index) { (books, error) -> Void in

        guard let books = books else {
            return
        }

        self.footerView.hidden = false

        self.currentIndexCount += 10

        self.booksArrayFromNetworkCall += books

        dispatch_async(dispatch_get_main_queue()) {
            self.tableView.reloadData()
        }
    }
}
Community
  • 1
  • 1
JEL
  • 1,540
  • 4
  • 23
  • 51
  • Can you show the code when you create the cell (cellForRowAtIndexPath)? I made this kind of search before and I don't have this problem. If only the image flash white, and the text next to it doesn't, maybe when you call reloadData() the image is downloaded again from the source, which causes the flash. In this case you may need to save the images in cache. – Oscar J. Irun Feb 16 '16 at 18:07
  • @OscarJ.Irun Please see updated question, I added the code you requested. After looking at the app again, it is only the image that flashes white, not the text. How would you solve this? – JEL Feb 16 '16 at 18:34
  • Yes, I guess it downloads the image from the source every time you call `reloadData()`. I would recommend to use [SDWebImage](https://github.com/rs/SDWebImage) to cache images and download asynchronously. It is very simple and I use it in most of my projects. To confirm that this is the case, just add a static image from your assets to the cell instead of calling `convertURLToImagesAsynchronouslyAndUpdateCells`, and you will see that it will not flash again. – Oscar J. Irun Feb 16 '16 at 18:50
  • @OscarJ.Irun You're right in that when I add a static image, it does not flash anymore. I never did image caching before, sounds complicated. Is there any tutorials in Swift you would recommend? Also, is this the only way to solve this issue? Thanks! – JEL Feb 16 '16 at 18:58
  • That library does all the hard work. I dont' program in Swift but I see it is as simple as `cell.imageView.sd_setImageWithURL(myImageURL)`. And it's done! Check this link [SDWebImage in Swift](https://github.com/rs/SDWebImage/issues/993) – Oscar J. Irun Feb 16 '16 at 19:05
  • @OscarJ.Irun Ok thank you! Last question, is this library SDWebImage similar to AFNetworking or Alamofire? I never used any of these but have heard of them. – JEL Feb 16 '16 at 19:11
  • SDWebImage is only related to images, AFNetworking has a lot more features (wrapper for POST, PUT, GET, DELETE requests, downloading image as well, etc). I have never used Alamofire before. I will post the above as an answer. After you implement it you can select it as the answer :) – Oscar J. Irun Feb 16 '16 at 19:16
  • @OscarJ.Irun I will try it out, thanks – JEL Feb 16 '16 at 20:21
  • Wouldn't it be better to use `insertRowsAtIndexPaths(_:withRowAnimation:)` instead of reloading the entire `UITableView` every time another bunch of cells need to be appended to the end? – paulvs Feb 16 '16 at 20:51
  • @paulvs Sounds like a good idea. I never had to do something like what you're mentioning. Can you please provide a code example? – JEL Feb 16 '16 at 21:24
  • @JEL I made an example and added it as an answer below. – paulvs Feb 17 '16 at 00:38

3 Answers3

3

If only the image flash white, and the text next to it doesn't, maybe when you call reloadData() the image is downloaded again from the source, which causes the flash. In this case you may need to save the images in cache.

I would recommend to use SDWebImage to cache images and download asynchronously. It is very simple and I use it in most of my projects. To confirm that this is the case, just add a static image from your assets to the cell instead of calling convertURLToImagesAsynchronouslyAndUpdateCells, and you will see that it will not flash again.

I dont' program in Swift but I see it is as simple as cell.imageView.sd_setImageWithURL(myImageURL). And it's done!

Oscar J. Irun
  • 455
  • 5
  • 17
  • Works perfectly! I tried to do it without a library first based on the answer above using `self.tableView.insertRowsAtIndexPaths` and the flickering stopped, but then I ran into an issue with the table being jumpy when it hit the bottom and inserted new rows. This library solved everything and it was so simple, thanks for also sending the link instructing on how to use the library too. – JEL Feb 18 '16 at 03:34
1

Try to change table alpha value before and after calling [tableView reloadData] method..Like

dispatch_async(dispatch_get_main_queue()) {
            self.aTable.alpha = 0.4f;
            self.tableView.reloadData()
            [self.aTable.alpha = 1.0f;

        }

I have used same approach in UIWebView reloading..its worked for me.

iReddy
  • 467
  • 1
  • 5
  • 12
1

Here's an example of infinite scroll using insertRowsAtIndexPaths(_:withRowAnimation:)

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    @IBOutlet weak var tableView: UITableView!

    var dataSource = [String]()
    var currentStartIndex = 0

    // We use this to only fire one fetch request (not multiple) when we scroll to the bottom.
    var isLoading = false

    override func viewDidLoad() {
        super.viewDidLoad()

        // Load the first batch of items.
        loadNextItems()
    }

    // Loads the next 20 items using the current start index to know from where to start the next fetch.
    func loadNextItems() {

        MyFakeDataSource().fetchItems(currentStartIndex, callback: { fetchedItems in

            self.dataSource += fetchedItems // Append the fetched items to the existing items.

            self.tableView.beginUpdates()

            var indexPathsToInsert = [NSIndexPath]()
            for i in self.currentStartIndex..<self.currentStartIndex + 20 {
                indexPathsToInsert.append(NSIndexPath(forRow: i, inSection: 0))
            }
            self.tableView.insertRowsAtIndexPaths(indexPathsToInsert, withRowAnimation: .Bottom)
            self.tableView.endUpdates()

            self.isLoading = false

            // The currentStartIndex must point to next index.
            self.currentStartIndex = self.dataSource.count
        })
    }

    // #MARK: - Table View Data Source Methods

    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dataSource.count
    }

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = UITableViewCell()
        cell.textLabel!.text = dataSource[indexPath.row]
        return cell
    }

    // #MARK: - Table View Delegate Methods

    func scrollViewDidScroll(scrollView: UIScrollView) {
        if isLoading == false && scrollView.contentOffset.y + scrollView.bounds.size.height > scrollView.contentSize.height {
            isLoading = true
            loadNextItems()
        }
    }
}

MyFakeDataSource is irrelevant, it's could be your GoogleBooksClient.getBooksFromGoogleBooks, or whatever data source you're using.

paulvs
  • 11,963
  • 3
  • 41
  • 66
  • This solved the flashing problem which is great, but it introduced another problem. Now whenever I reach the bottom of the table and make a new network call, it begins loading, then the cells animate in and the table "jumps up." So it reaches the bottom then shoots up like 20% of the screen and the new cells are loaded in. The scrollbar moves up. Everything works fine from there on but this did not happen before with my solution. Any ideas why? – JEL Feb 17 '16 at 05:01
  • If I do `self.tableView.insertRowsAtIndexPaths(indexPathsToInsert, withRowAnimation: .None)`, the None still doesn't remove the animation I think. Maybe this is the problem, not sure. Also, I am using `tableView.estimatedRowHeight = 100` and `tableView.rowHeight = UITableViewAutomaticDimension`, not sure if this is the problem? – JEL Feb 17 '16 at 05:09