Search code examples
swiftuitableviewasynchronousprefetch

Perform time consuming tasks inside UITableViewCell, pausing on scrolling


I have TableView with customs cell representing events. It looks very close to first and third image here.

enter image description here

As you can see (sorry for small resolution) on some events there are photos of friends that are going to participate. Unfortunately information about friends is not loaded with other information about events.

So after I got list of events I can make request to load list of friends that are going to participate in each event.

Right now I use code like

class EventCell : UITableViewCell {
  var eventForCell : Event? {
      didSet {
          eventTitleLabel.text = eventForCell.title
          eventDateLabel.text = eventForCell.date
          presentFriends(eventID : eventForCell.id)
      }
   }

   func presentFriends(eventID : Int) {
        //searching for friends for event with specific eventID
        .......
        .......

        //for every friend found adding UIImageView in cell
        for friend in friendsOnEvent {
          let avatar = UIImageView(....)
          self.addSubview(avatar)
        }
   }
}

This code works but photos are not presented in smooth way. Also if you scroll list fast they start to blink. Maybe it is even not necessary to load them if user scrolls fast list of events. So I have two questions:

  1. How can I make smooth scrolling experience taking in consideration that presenting friends for every event can take time and sometimes it finishes after cell was scrolled away.
  2. If I had loaded list of events and already presenting cells with them. How can I update those cells after I get information about friends that are going to participate?
  3. When user is scrolling and I am creating async tasks to display some images in cell I think I should use weak reference to self and maybe check it not to equal nil so task would be canceled if cell is not visible now. How should it be done?

Update:

I found info about tableView(_:prefetchRowsAt:) method inside UITableViewPrefetchingDataSource protocol, should I use it for this case? Maybe someone has some experience with it?


Solution

  • 1. (Re)creating a view objects during cellForRowAt is generally a bad practice. From the screenshot I assume that there is a limit to how many avatars are there on a single cell - I would recommend creating all the UIImageView objects in the cell initializer, and then in presentFriends just set images to them, and either hide the unused ones (isHidden = true) or set their alpha to 0 (of course, don't forget to unhide those that are used).

    2. If you are using SDWebImage to load images, implement prepareForReuse and cancel current downloads to get a bit of performance boost during scrolling, and prevent undefined behaviour (when you try to set another image while the previous one was not yet downloaded). Based on this question, this one and this one I would expect something like:

        override func prepareForReuse() {
            super.prepareForReuse()
        
            self.imageView.sd_cancelCurrentImageLoad()
        }
    
     Or you can use [this gist][4] for an inspiration.
    

    P.S.: Also, you will have to count with some blinking, since the images are downloaded from web - there will always be some delay. By caching you can get instantly those that were already downloaded, but new ones will have delay - there is no way to prevent that (unless you preload all the images that can appear in tableView before presenting tableView).

    P.S.2: You can try to implement prefetching using [UITableViewDataSourcePrefetching][6]. This could help you out with blinking caused by downloading the avatars from web. This would make things a bit more complicated, but if you really want to remove that blinking you will have to get your hands dirty.

    First of all, as you can see from the documentation, prefetchRowsAt does not give you a cell object - thus you will have to prefetch images to your model object instead of simply using sd_setImage on the UIImageView object at a given cell. Maybe the aforementioned gist would help you out with downloading images to model.

    Now also as the documentation states:

    Loading Data Asynchronously

    The tableView(_:prefetchRowsAt:) method is not necessarily called for every cell in the table view. Your implementation of tableView(_:cellForRowAt:) must therefore be able to cope with the following potential situations:

    Data has been loaded via the prefetch request, and is ready to be displayed.

    Data is currently being prefetched, but is not yet available.

    Data has not yet been requested.

    Simply said, when you are in cellForRowAt, you cannot rely on prefetchRowsAt being called - it might have been called, and it might not have been called (or maybe the call is in progress).

    Documentation continues to suggest using Operation as a backing tool for downloading the resources - prefetchRowsAt could create Operation objects that would be responsible for downloading avatars for a given row. cellForRowAt can then ask the model for the Operation objects for the given row - if there are not any, it means that prefetchRowsAt was not called for that row, and you have to create Operation objects at that point. Otherwise you can use the operations created by prefetchRowsAt.

    Anyway, if you decide that it is worth it, you can use this tutorial as an inspiration for implementing prefetching.