UITableView
Imagine, we already have an UITableView filled with data, and we want to add the Load More/Infinite Scrolling feature.
First, we create an UITableViewCell with a Xib file and add an UIActivityIndicatorView on the center of the cell.
Control+left click and drag the UIActivityIndicatorView into your .swift file and give the name activityIndicator and add loadingCell as Identifier.
class LoadingCell: UITableViewCell {
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
override func awakeFromNib() {
super.awakeFromNib()
self.activityIndicator.hidesWhenStopped = true
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
}
Now, let’s go into our LoadMoreViewController where we have the UITableView, and register the Loading Cell in the viewDidLoad().
class LoadMoreViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
var items: [String] = []
var cellId = "itemCell"
var cellLoadingId = "loadingCell"
var isLoading = false
let tbl: UITableView = {
let tv = UITableView()
tv.backgroundColor = UIColor.white
tv.translatesAutoresizingMaskIntoConstraints = false
return tv
}()
override func viewDidLoad() {
super.viewDidLoad()
setupTable()
}
func setupTable() {
tbl.delegate = self
tbl.dataSource = self
tbl.register(UITableViewCell.self, forCellReuseIdentifier: cellId)
let loadingCellNib = UINib(nibName: "LoadingCell", bundle: nil)
tbl.register(loadingCellNib, forCellReuseIdentifier: cellLoadingId)
items.append(contentsOf: (1...20).map { index in "Item \(index)"})
view.addSubview(tbl)
NSLayoutConstraint.activate([
tbl.topAnchor.constraint(equalTo: self.view.topAnchor),
tbl.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
tbl.rightAnchor.constraint(equalTo: self.view.rightAnchor),
tbl.leftAnchor.constraint(equalTo: self.view.leftAnchor)
])
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
let contentHeight = scrollView.contentSize.height
if (offsetY > contentHeight - scrollView.frame.height * 4) && !isLoading {
loadMoreData()
}
}
func loadMoreData() {
if !self.isLoading {
self.isLoading = true
DispatchQueue.global().async {
sleep(2)
let start = self.items.count
let end = start + 10
if end < 40 {
self.items.append(contentsOf: (start...end).map { index in "Item \(index)"})
DispatchQueue.main.async {
self.tbl.reloadData()
self.isLoading = false
}
} else {
DispatchQueue.main.async {
self.isLoading = false
self.tbl.reloadSections(IndexSet(integer: 1), with: .automatic)
}
}
}
}
}
}
extension LoadMoreViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return items.count
} else if section == 1 {
// loading cell
return 1
} else {
return 0
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath)
cell.textLabel?.text = items[indexPath.row]
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: cellLoadingId, for: indexPath) as! LoadingCell
if self.isLoading {
cell.activityIndicator.startAnimating()
} else {
cell.activityIndicator.stopAnimating()
}
return cell
}
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.section == 0 {
return 44
} else {
return 55 // loading cell
}
}
}
We use the method scrollViewDidScroll, from the embedded UIScrollView in the UITableView, to detect when the user is close to the end of the list to call loadMoreData method.
UICollectionView
Steps to add Load More/Infinite Scrolling feature to UICollectionView.
UICollectionViewCell and register in UICollectionView.UICollectionReusableView and register in UICollectionView.UICollectionView‘s delegate and datasource, add the referenceSizeForFooterInSection method to return the size for the Loading View when it’s time to show it.UICollectionView footer.
class CollectionViewItemCell: UICollectionViewCell {}
class LoadingReusableView: UICollectionReusableView {
let activityIndicator: UIActivityIndicatorView = {
let av = UIActivityIndicatorView(style: .medium)
av.hidesWhenStopped = true
av.translatesAutoresizingMaskIntoConstraints = false
return av
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.setupUI()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
self.setupUI()
}
func setupUI() {
self.addSubview(activityIndicator)
activityIndicator.startAnimating()
NSLayoutConstraint.activate([
activityIndicator.centerYAnchor.constraint(equalTo: centerYAnchor),
activityIndicator.centerXAnchor.constraint(equalTo: centerXAnchor)
])
}
}
class LoadMoreViewController: UIViewController {
var items: [UIColor] = []
var cellId = "cellId"
var cellLoadingId = "cellLoadingId"
var isLoading = false
var loadingView: LoadingReusableView?
lazy var colorsCollection: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
let cl = UICollectionView(frame: .zero, collectionViewLayout: layout)
cl.backgroundColor = .white
cl.translatesAutoresizingMaskIntoConstraints = false
cl.dataSource = self
cl.delegate = self
cl.register(CollectionViewItemCell.self, forCellWithReuseIdentifier: cellId)
cl.register(LoadingReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: cellLoadingId)
return cl
}()
override func viewDidLoad() {
super.viewDidLoad()
loadMoreData()
setupCollection()
}
func setupCollection() {
view.addSubview(colorsCollection)
NSLayoutConstraint.activate([
colorsCollection.topAnchor.constraint(equalTo: self.view.topAnchor),
colorsCollection.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
colorsCollection.rightAnchor.constraint(equalTo: self.view.rightAnchor),
colorsCollection.leftAnchor.constraint(equalTo: self.view.leftAnchor)
])
}
func getRandomColor() -> UIColor {
let red: CGFloat = CGFloat(drand48())
let green: CGFloat = CGFloat(drand48())
let blue: CGFloat = CGFloat(drand48())
return UIColor(red: red, green: green, blue: blue, alpha: 1.0)
}
func loadMoreData() {
if !self.isLoading {
self.isLoading = true
DispatchQueue.global().async {
// Fake background loading task for 2 seconds
sleep(2)
let start = self.items.count
let end = start + 30
if end < 100 {
self.items.append(contentsOf: (start...end).map { index in self.getRandomColor() })
DispatchQueue.main.async {
self.isLoading = false
self.colorsCollection.reloadData()
}
} else {
DispatchQueue.main.async {
self.isLoading = false
self.colorsCollection.reloadData()
}
}
}
}
}
}
extension LoadMoreViewController: UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! CollectionViewItemCell
cell.backgroundColor = self.items[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 100, height: 100)
}
// size of Loading View
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
if self.isLoading {
return CGSize.zero
} else {
return CGSize(width: collectionView.bounds.size.width, height: 55)
}
}
// set the Loading View
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
if kind == UICollectionView.elementKindSectionFooter {
let aFooterView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: cellLoadingId, for: indexPath) as! LoadingReusableView
loadingView = aFooterView
loadingView?.backgroundColor = UIColor.clear
return aFooterView
}
return UICollectionReusableView()
}
// start and stop the activityIndicator
func collectionView(_ collectionView: UICollectionView, willDisplaySupplementaryView view: UICollectionReusableView, forElementKind elementKind: String, at indexPath: IndexPath) {
if elementKind == UICollectionView.elementKindSectionFooter {
if self.isLoading {
self.loadingView?.activityIndicator.startAnimating()
} else {
self.loadingView?.activityIndicator.stopAnimating()
}
}
}
func collectionView(_ collectionView: UICollectionView, didEndDisplayingSupplementaryView view: UICollectionReusableView, forElementOfKind elementKind: String, at indexPath: IndexPath) {
if elementKind == UICollectionView.elementKindSectionFooter {
self.loadingView?.activityIndicator.stopAnimating()
}
}
// prefetch
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.row == items.count - 10 && !self.isLoading {
loadMoreData()
}
}
}