We can paginate a UITableView by implementing the tableView:willDisplayCell:forRowAtIndexPath method. In it we can check if the displayed row index is nearing the last row in the model and if so, we fetch new data, append it to the model and reload the table view.

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    if indexPath.row == self.dataList.count - self.dataOffset && self.shouldFetchMore && !self.isDataLoading {
        UI.showLoadingForTableView(self.tableView)  // Display loader at the bottom
        self.api.fetchSomeData(offset: self.dataList.count, max: Const.dataFetchMaxLimit, success: { xs in
            DispatchQueue.main.async {
                UI.hideLoadingForTableView(self.tableView)  // Hide loader
                self.isDataLoading = false
                self.dataList.append(contentsOf: xs)
                self.reloadTableView()
                if xs.count == 0 { self.shouldFetchMore = false }  // If data returned is 0 => there are no more data to fetch
            }
            self.lastFetchedIndex += Constants.dataFetchMaxLimit  // Increment the fetch index to include current offset
        }, error: { _ in
            self.shouldFetchMore = true
            self.isDataLoading = false
            DispatchQueue.main.async {
                UI.hideLoadingForTableView(self.tableView)
                self.reloadTableView()
           }
        })
    }
}

Here, dataList is the table's data model, dataOffset is a constant, say 5 which means, if there are only five more cells to be displayed, paginate. The shouldFetchMore variable indicates if there are more data available to paginate and isDataLoading indicates if a fetch is already in progress.

We can show a loader activity view at the bottom of the table view to indicate that more data is being fetched.

class UI {
    /// A loading indicator
    private static var spinner: (UITableView) -> UIActivityIndicatorView = { tableView in
        let s = UIActivityIndicatorView(style: .gray)
        s.startAnimating()
        s.frame = CGRect(x: CGFloat(0), y: CGFloat(0), width: tableView.bounds.width, height: CGFloat(44))
        return s
    }

    /// Show loading spinner at the bottom of the given table view
    static func showLoadingForTableView(_ tableView: UITableView) {
        tableView.tableFooterView = self.spinner(tableView)
        tableView.tableFooterView?.isHidden = false
    }

    /// Hide loading spinner for the given table view if present
    static func hideLoadingForTableView(_ tableView: UITableView) {
        tableView.tableFooterView?.subviews.forEach({ view in
            if view == self.spinner(tableView) {
                view.removeFromSuperview()
            }
        })
        tableView.tableFooterView?.isHidden = true
    }
}

TableView with pagination