Andrea Prearo
Master Software Engineer - iOS @ Capital One SF
https://github.com/andrea-prearo
https://medium.com/@andrea.prearo
https://twitter.com/andrea_prearo
Smooth scrolling in
UITableView and
UICollectionView
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.
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.
Strategies to achieve Smooth
Scrolling: UITableView and
UICollectionView
Example: Display a set of users
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:)
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
}
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
}
}
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
}
}
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
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]
}
}
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()
}
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
}
}
}
}
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
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()
}
}
Open Source Libraries for Asynchronous
Image Downloading and Caching
• SDWebImage
• AlamofireImage
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
}
}
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
}
}
Putting Everything Together: Optimized
Cell Rendering
Optimized Cell Rendering
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "UserCell", for: indexPath) as! UserCell
if let viewModel = userViewModelController.viewModel(at: (indexPath as NSIndexPath).row) {
cell.configure(viewModel)
}
return cell
}
Cell Rendering should now be really fast
• We are using the cached View Model data
• We are fetching the images asynchronously
Strategies to achieve Smooth
Scrolling: UITableView only
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
[...]
}
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
Strategies to achieve Smooth
Scrolling: UICollectionView only
Calculate your Cell Size
Implement collectionView(_:layout:sizeForItemAt:)
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize
// Calculate the appropriate cell size
return CGSize(width: ..., height: ...)
}
Handle Size Classes and Orientation
Changes
Implement viewWillTransition(to:with:)
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
collectionView?.collectionViewLayout.invalidateLayout()
}
Refresh Collection View Layout when
• Transitioning to a different Size Class
• Rotating the device
Dynamically Adjust Cell Layout
Override apply(_:)
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
super.apply(layoutAttributes)
// Customize the cell layout
[...]
}
Example: Adjust multi-line UILabel
Maximum Width using
preferredMaxLayoutWidth
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
super.apply(layoutAttributes)
// Customize the cell layout
let width = CGRectGetWidth(layoutAttributes.frame)
username.preferredMaxLayoutWidth = width - 16
}
Code samples
All code is available on GitHub:
SmoothScrolling

Smooth scrolling in UITableView and UICollectionView

  • 1.
    Andrea Prearo Master SoftwareEngineer - iOS @ Capital One SF https://github.com/andrea-prearo https://medium.com/@andrea.prearo https://twitter.com/andrea_prearo
  • 2.
    Smooth scrolling in UITableViewand UICollectionView
  • 3.
    Apple SDK Componentsto 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 UserExperience 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 achieveSmooth Scrolling: UITableView and UICollectionView Example: Display a set of users
  • 6.
    Cells Rendering isa 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 overridefunc 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 Asynchronouslyand Cache View Models • Avoid blocking the main thread while fetching data • Update the table view right after we retrieve the data
  • 11.
    User View ModelController: 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 ModelController: 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 ModelController 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 FetchingData • 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 Asynchronouslyand 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 Librariesfor Asynchronous Image Downloading and Caching • SDWebImage • AlamofireImage
  • 17.
    Customize the Cell Subclassthe 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 Layersand 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 } }
  • 19.
    Putting Everything Together:Optimized Cell Rendering Optimized Cell Rendering override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "UserCell", for: indexPath) as! UserCell if let viewModel = userViewModelController.viewModel(at: (indexPath as NSIndexPath).row) { cell.configure(viewModel) } return cell }
  • 20.
    Cell Rendering shouldnow be really fast • We are using the cached View Model data • We are fetching the images asynchronously
  • 21.
    Strategies to achieveSmooth Scrolling: UITableView only
  • 22.
    Use Self-Sizing Cellsfor Cells of Variable Height Initialize estimatedRowHeight and rowHeight override func viewDidLoad() { [...] tableView.estimatedRowHeight = ... // Estimated default row height tableView.rowHeight = UITableViewAutomaticDimension [...] }
  • 23.
    Variable Height Cellswith no support for Self-Sizing (iOS7) • Pre-calculate all the row heights at once • Return the cached value when tableView(_:heightForRowAt:) is called
  • 24.
    Strategies to achieveSmooth Scrolling: UICollectionView only
  • 25.
    Calculate your CellSize Implement collectionView(_:layout:sizeForItemAt:) func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize // Calculate the appropriate cell size return CGSize(width: ..., height: ...) }
  • 26.
    Handle Size Classesand Orientation Changes Implement viewWillTransition(to:with:) override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) collectionView?.collectionViewLayout.invalidateLayout() }
  • 27.
    Refresh Collection ViewLayout when • Transitioning to a different Size Class • Rotating the device
  • 28.
    Dynamically Adjust CellLayout Override apply(_:) override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { super.apply(layoutAttributes) // Customize the cell layout [...] }
  • 29.
    Example: Adjust multi-lineUILabel Maximum Width using preferredMaxLayoutWidth override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { super.apply(layoutAttributes) // Customize the cell layout let width = CGRectGetWidth(layoutAttributes.frame) username.preferredMaxLayoutWidth = width - 16 }
  • 30.
    Code samples All codeis available on GitHub: SmoothScrolling