This document discusses strategies for achieving smooth scrolling in UITableView and UICollectionView. It recommends asynchronously fetching and caching cell data and images to improve rendering performance. It also suggests using self-sizing cells, pre-calculating heights, and dynamically adjusting layouts to optimize scrolling. Key aspects include asynchronously retrieving models, caching view models, loading images on a background thread, reusing cells, and invalidating layouts on orientation changes.
Smooth Scrolling in UITableView and UICollectionView
1. Andrea Prearo
Master Software Engineer - iOS @ Capital One SF
https://github.com/andrea-prearo
https://medium.com/@andrea.prearo
https://twitter.com/andrea_prearo
3. Apple SDK Components to display
Scrollable Data
UITableView
iOS 2.0+
An instance of UITableView (or simply, a table view) is a means for displaying and editing hierarchical lists of information.
UICollectionView
iOS 6.0+
The UICollectionView class manages an ordered collection of data items and presents them using customizable layouts.
Collection views provide the same general function as table views except that a collection view is able to support more
than just single-column layouts. Collection views support customizable layouts that can be used to implement multi-
column grids, tiled layouts, circular layouts, and many more. You can even change the layout of a collection view
dynamically if you want.
4. Scrolling and User Experience
Table views and collection views are both designed to support
displaying sets of data that can be scrolled. However, when
displaying a very large amount of data, it could be very tricky to
achieve a perfectly smooth scrolling. This is not ideal because it
negatively affects the user experience.
5. Strategies to achieve Smooth
Scrolling: UITableView and
UICollectionView
Example: Display a set of users
6. Cells Rendering is a Critical Task
UITableViewCell lifecycle
1. Request the cell: tableView(_:cellForRowAt:)
2. Display the cell: tableView(_:willDisplay:forRowAt:)
3. Remove the cell: tableView(_:didEndDisplaying:forRowAt:)
7. Basic cell rendering
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Table view cells are reused and should be dequeued using a cell identifier.
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath)
// Configure the cell ...
return cell
}
8. User Model
enum Role: String {
case unknown = "Unknown"
case user = "User"
case owner = "Owner"
case admin = "Admin"
static func get(from: String) -> Role {
if from == user.rawValue {
return .user
} else if from == owner.rawValue {
return .owner
} else if from == admin.rawValue {
return .admin
}
return .unknown
}
}
struct User {
let avatarUrl: String
let username: String
let role: Role
init(avatarUrl: String, username: String, role: Role) {
self.avatarUrl = avatarUrl
self.username = username
self.role = role
}
}
9. User View Model (MVVM)
struct UserViewModel {
let avatarUrl: String
let username: String
let role: Role
let roleText: String
init(user: User) {
// Avatar
avatarUrl = user.avatarUrl
// Username
username = user.username
// Role
role = user.role
roleText = user.role.rawValue
}
}
10. Fetch Data Asynchronously and Cache
View Models
• Avoid blocking the main thread while fetching data
• Update the table view right after we retrieve the data
11. User View Model Controller: Wrap and
Cache View Model
class UserViewModelController {
fileprivate var viewModels: [UserViewModel?] = []
[...]
var viewModelsCount: Int {
return viewModels.count
}
func viewModel(at index: Int) -> UserViewModel? {
guard index >= 0 && index < viewModelsCount else { return nil }
return viewModels[index]
}
}
12. User View Model Controller:
Asynchronous Data Fetch
func retrieveUsers(_ completionBlock: @escaping (_ success: Bool, _ error: NSError?) -> ()) {
let urlString = ... // Users Web Service URL
let session = URLSession.shared
guard let url = URL(string: urlString) else {
completionBlock(false, nil)
return
}
let task = session.dataTask(with: url) { [weak self] (data, response, error) in
guard let strongSelf = self else { return }
guard let data = data else {
completionBlock(false, error as NSError?)
return
}
let error = ... // Define a NSError for failed parsing
if let jsonData = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [[String: AnyObject]] {
guard let jsonData = jsonData else {
completionBlock(false, error)
return
}
var users = [User?]()
for json in jsonData {
if let user = UserViewModelController.parse(json) {
users.append(user)
}
}
strongSelf.viewModels = UserViewModelController.initViewModels(users)
completionBlock(true, nil)
} else {
completionBlock(false, error)
}
}
task.resume()
}
13. User View Model Controller Extension:
Parse JSON
private extension UserViewModelController {
static func parse(_ json: [String: AnyObject]) -> User? {
let avatarUrl = json["avatar"] as? String ?? ""
let username = json["username"] as? String ?? ""
let role = json["role"] as? String ?? ""
return User(avatarUrl: avatarUrl, username: username, role: Role.get(from: role))
}
static func initViewModels(_ users: [User?]) -> [UserViewModel?] {
return users.map { user in
if let user = user {
return UserViewModel(user: user)
} else {
return nil
}
}
}
}
14. Scenarios for Fetching Data
• Only the when loading the table view the first time, by
placing it in viewDidLoad()
• Every time the table view is displayed, by placing it in
viewWillAppear(_:)
• On user demand (for instance via a pull-down-to-refresh), by
placing it in the method call that will take care of refreshing
the data
15. Load Images Asynchronously and Cache
Them
Extend UIImage and Leverage URLSession
extension UIImage {
static func downloadImageFromUrl(_ url: String, completionHandler: @escaping (UIImage?) -> Void) {
guard let url = URL(string: url) else {
completionHandler(nil)
return
}
let task: URLSessionDataTask = URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) -> Void in
guard let httpURLResponse = response as? HTTPURLResponse , httpURLResponse.statusCode == 200,
let mimeType = response?.mimeType, mimeType.hasPrefix("image"),
let data = data , error == nil,
let image = UIImage(data: data) else {
completionHandler(nil)
return
}
completionHandler(image)
})
task.resume()
}
}
16. Open Source Libraries for Asynchronous
Image Downloading and Caching
• SDWebImage
• AlamofireImage
17. Customize the Cell
Subclass the Default Cell
class UserCell: UITableViewCell {
@IBOutlet weak var avatar: UIImageView!
@IBOutlet weak var username: UILabel!
@IBOutlet weak var role: UILabel!
func configure(_ viewModel: UserViewModel) {
UIImage.downloadImageFromUrl(viewModel.avatarUrl) { [weak self] (image) in
guard let strongSelf = self,
let image = image else {
return
}
strongSelf.avatar.image = image
}
username.text = viewModel.username
role.text = viewModel.roleText
}
}
18. Use Opaque Layers and Avoid Gradients
class UserCell: UITableViewCell {
@IBOutlet weak var avatar: UIImageView!
@IBOutlet weak var username: UILabel!
@IBOutlet weak var role: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
setOpaqueBackground()
[...]
}
}
private extension UserCell {
static let DefaultBackgroundColor = UIColor.groupTableViewBackgroundColor
func setOpaqueBackground() {
alpha = 1.0
backgroundColor = UserCell.DefaultBackgroundColor
avatar.alpha = 1.0
avatar.backgroundColor = UserCell.DefaultBackgroundColor
}
}
22. Use Self-Sizing Cells for Cells of Variable
Height
Initialize estimatedRowHeight and rowHeight
override func viewDidLoad() {
[...]
tableView.estimatedRowHeight = ... // Estimated default row height
tableView.rowHeight = UITableViewAutomaticDimension
[...]
}
23. Variable Height Cells with no support for
Self-Sizing (iOS7)
• Pre-calculate all the row heights at once
• Return the cached value when
tableView(_:heightForRowAt:) is called