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() } } }