JTTableViewController
A ViewController with a tableView which manage pagination and loaders for iOS.
Installation
With CocoaPods, add this line to your Podfile.
pod 'JTTableViewController', '~> 2.0'
What's in it?
- avoid parallel requests problem (you start two requests and the first one finish after the second), last request is the only one we want to use
- easily manage pagination
- display a view for the first loading (when your
tableView
is empty) - display a view when there is no results to your first request
- display a loader view (an
UITableViewCell
) for indicate the next page is loading - display a view for errors
Screenshots
Usage
Minimum usage
You can either use JTFullTableViewController
which is almost like UITableViewController
.
Or you can inherit from JTTableViewController
,UITableViewDelegate
, UITableViewDataSource
, create an UITableView
, assign it to self.tableView
, add it to the self.view
and set the delegate
and dataSource
yourself.
If your controller is fullscreen use JTFullTableViewController
else use JTTableViewController
.
import JTTableViewController
class ViewController: JTTableViewController<YourModel>, UITableViewDelegate, UITableViewDataSource {
// Used in this example to manage your pagingation
private var currentPage = 1
// Must be implemented
override func jt_tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")
let anInstanceOfYourModel = self.results[indexPath.row]
/*
whatever you wanna do with `anInstanceOfYourModel` and your `cell`
*/
return cell
}
// Must be implemented
override func fetchResults() {
super.fetchResults()
currentPage = 1
// `lastRequestId` is used to avoid problem with parallel requests
let lastRequestId = self.lastRequestId
YourService.retrieveData(page: currentPage) { (error, results) -> () in
if let error = error {
self.didFailedToFetchResults(error: error, lastRequestId: lastRequestId)
return
}
self.didFetchResults(results: results, lastRequestId: lastRequestId) {
// this block is executed if `lastRequestId` matched with `self.lastRequestId`
self.currentPage += 1
}
}
}
// Must be implemented
override func fetchNextResults() {
super.fetchNextResults()
// `lastRequestId` is used to avoid problem with parallel requests
let lastRequestId = self.lastRequestId
YourService.retrieveData(page: currentPage) { (error, results) -> () in
if let error = error {
self.didFailedToFetchResults(error: error, lastRequestId: lastRequestId)
}
else {
self.didFetchNextResults(results: results, lastRequestId: lastRequestId) {
// this block is executed if `lastRequestId` matched with `self.lastRequestId`
self.currentPage += 1
}
}
}
}
}
Advanced usage
import JTTableViewController
class ViewController: JTTableViewController<YourModel>, UITableViewDelegate, UITableViewDataSource {
// Used in this example to manage your pagingation
private var currentPage = 1
private let refreshControl = UIRefreshControl()
override func viewDidLoad () {
super.viewDidLoad()
// `nextPageLoaderCell` is an `UITableViewCell`
self.nextPageLoaderCell = MyNextPageLoadCell()
// `fecthResults` is call 5 cells before `nextPageLoaderCell` become visible
self.nextPageLoaderOffset = 5
// `noResultsView` is display when `didFetchResults` is called with an `results` empty
let noResultsView = NoResultsView()
self.noResultsView = noResultsView
self.view.addSubview(noResultsView)
// something better than frame with Constraints but not relevant here
noResultsView.frame = self.view.bounds
// `noResultsLoadingView` is display when `fetchResults` is called and `results` is empty
let noResultsLoadingView = NoResultsLoadingView()
self.noResultsLoadingView = noResultsLoadingView
self.view.addSubview(noResultsLoadingView)
// something better than frame with Constraints but not relevant here
noResultsLoadingView.frame = self.view.bounds
// `errorView` is display when `didFailedToFetchResults` is called
let errorView = ErrorView()
self.errorView = errorView
self.view.addSubview(errorView)
// something better than frame with Constraints but not relevant here
errorView.frame = self.view.bounds
refreshControl.addTarget(self, action: #selector(fetchResults), for: .valueChanged)
self.tableView?.addSubview(refreshControl)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
fetchResults()
}
// Must be implemented
override func jt_tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")
let anInstanceOfYourModel = self.results[indexPath.row]
/*
whatever you wanna do with `anInstanceOfYourModel` and your `cell`
*/
return cell
}
// Must be implemented
override func fetchResults() {
self.resetResults()
super.fetchResults()
currentPage = 1
// `lastRequestId` is used to avoid problem with parallel requests
let lastRequestId = self.lastRequestId
YourService.retrieveData(page: currentPage) { (error, results) -> () in
if let error = error {
self.didFailedToFetchResults(error: error, lastRequestId: lastRequestId)
return
}
self.didFetchResults(results: results, lastRequestId: lastRequestId) {
// this block is executed if `lastRequestId` matched with `self.lastRequestId`
self.currentPage += 1
}
}
}
// Must be implemented
override func fetchNextResults() {
super.fetchNextResults()
// `lastRequestId` is used to avoid problem with parallel requests
let lastRequestId = self.lastRequestId
YourService.retrieveData(page: currentPage) { (error, results) -> () in
if let error = error {
self.didFailedToFetchResults(error: error, lastRequestId: lastRequestId)
}
else {
self.didFetchNextResults(results: results, lastRequestId: lastRequestId) {
// this block is executed if `lastRequestId` matched with `self.lastRequestId`
self.currentPage += 1
}
}
}
}
override func didEndFetching () {
super.didEndFetching()
refreshControl.endRefreshing()
}
}
You have to implement fetchResults
and fetchNextResults
methods. They are used to load data (from your web service for example). These methods must call super
.
Optionaly, you can override jt_tableView(tableView:heightForRowAt:)
for defining the height of cells.
fetchResults
is used to retrieve new data (erase all previous data) whereas fetchNextResults
is used for get more data (the pagination).
didFetchResults
must be call when fetchResults
have successfuly retrieve data.
didFetchNextResults
must be call when fetchNextResults
have successfuly retrieve data.
didFailedToFetchResults
must be call if didFetchResults
or didFetchNextResults
have failed to retrieve data.
didEndFetching
is called after didFetchResults
, didFetchNextResults
or didFailedToFetchResults
The data are stored in results
. Just use self.results
to access to them.
If you want to remove some elements in results
you can use self.unsafeResults
, only in specific case (ex: removing one cell).
There are some properties you can customize:
nextPageLoaderCell
is the loader use for the pagination, it's aUITableViewCell
noResultsView
is the view display when the results get from your service are emptynoResultsLoadingView
is the view display when there is no results and you start fetching new data, used for the first loaderrorView
is the view displaydidFailedToFetchResults
is callednextPageLoaderOffset
is the number of cells require before the last cell for callingfetchNextResults
, by default it's 3
You can also override some methods:
didEndFetching
showNoResultsLoadingView
hideNoResultsLoadingView
showNoResultsView
hideNoResultsView
showErrorView
hideErrorView
Sections
JTTableViewController
support section management. You just have to override 2 method:
numberOfSections(tableView:)
jt_tableView(tableView:numberOfRowsInSection:)
import JTTableViewController
class ViewController: JTTableViewController<[Int: [YourModel]]>, UITableViewDelegate, UITableViewDataSource {
@objc(numberOfSectionsInTableView:)
override func numberOfSections(in tableView: UITableView) -> Int {
return self.results.count
}
override func jt_tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return results[section].count
}
}
Subclassing notes
If you want to subclass JTTableViewController
or JTFullTableViewController
, the methods from UITableViewDelegate
and UITableViewDataSource
must have an @objc
annotation.
class XXTableViewController<T>: JTTableViewController<T> {
/*
... Here you add whatever you want to add
*/
}
class MyViewController: XXTableViewController<YourModel> {
// if you don't add `@objc(tableView:didSelectRowAtIndexPath:)` this method is not called
@objc(tableView:didSelectRowAtIndexPath:)
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
/*
...
/*
}
}
Requirements
- iOS 8.0 or higher
- Swift 3.0
Author
License
JTTableViewController is released under the MIT license. See the LICENSE file for more info.