Data Mediator
Data View Mediator
This article describes an approach to separate some routine code from view controllers.
Working with UITableView
or UICollectionView
, we often have to write some routine code which repeats from controller to controller. It could be loading indicators, specific footers or supplementary views. The very common solution is to create a base view controller, put such logic there and subclass it.
But what if we can avoid such inheritance? This article describes a possible solution. Just an approach, not a framework, because it’s very project-specific.
Mediator to the rescue!
The main idea is to create a layer between view controller and table/collection view dataSource & delegate.
Example
Let’s take a view controllers with UITableView
s. For example we’ll implement a loading indicator for infinite scroll and add UIRefreshControl
for data reloading.
Apart from implementing UITableViewDataSource
& UITableViewDelegate
methods (see diagram) our controllers should be able to:
- refresh loaded data if one uses
UIRefreshControl
- load next page if one reaches a table’s end while scrolling
So we put refresh/scroll handling to the mediator and it’s absolutely doesn’t matter what data we load, how we load, how it should be rendered and so on.
Implementation
We’ll do a very easy implementation to show the approach.
The main part is proxying:
- the mediator exposes its own protocols
TableViewMediatorDataSource
&TableViewMediatorDelegate
- the mediator sets itself as a table
dataSource
&delegate
- the mediator proxies every unhandled call from table protocols to its own protocols
public class TableViewMediator: NSObject {
private var tableView: UITableView!
private(set) weak var dataSource: TableViewMediatorDataSource?
private(set) weak var delegate: TableViewMediatorDelegate?
private var refreshControl: UIRefreshControl?
public init(tableView: UITableView) {
super.init()
self.tableView = tableView
}
public func setDataSource(dataSource: TableViewMediatorDataSource, delegate: TableViewMediatorDelegate?) {
self.dataSource = dataSource
self.delegate = delegate
tableView.dataSource = self
tableView.delegate = self
refreshControl = UIRefreshControl()
refreshControl!.addTarget(self, action: "refreshControlDidChangeValue:", forControlEvents: .ValueChanged)
tableView.addSubview(refreshControl!)
}
}
The mediator protocols
public protocol TableViewMediatorDataSource: UITableViewDataSource {
}
@objc public protocol TableViewMediatorDelegate: UITableViewDelegate {
// more: true - load next page, false - reload data
optional func tableView(tableView: UITableView, mediator: TableViewMediator, shouldLoadMore more: Bool) -> Bool
}
Both protocols are subclassed from corresponding UITableView
protocols.
Time to proxy
public class TableViewMediator: NSObject {
...
// MARK: Proxy
public override func respondsToSelector(aSelector: Selector) -> Bool {
return (super.respondsToSelector(aSelector)
|| (dataSource?.respondsToSelector(aSelector) ?? false)
|| (delegate?.respondsToSelector(aSelector) ?? false))
}
public override func forwardingTargetForSelector(aSelector: Selector) -> AnyObject? {
if dataSource?.respondsToSelector(aSelector) == true {
return dataSource
}
if delegate?.respondsToSelector(aSelector) == true {
return delegate
}
return super.forwardingTargetForSelector(aSelector)
}
}
Conforming the UITableView
protocols
extension TableViewMediator: UITableViewDataSource {
public func numberOfSectionsInTableView(tableView: UITableView) -> Int {
var count = 1 // by default (see apple doc)
if let num = dataSource?.numberOfSectionsInTableView?(tableView) {
count = num
}
loadingIndicatorSection = count++ // add additional section for the loading indicator
return count
}
}
Notice we add an additional property:
private(set) var loadingIndicatorSection: Int?
The loading indicator cell:
private static let LoadingCellIdentifier = "MediatorLoadingCell"
...
tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: TableViewMediator.LoadingCellIdentifier)
Completing the data source:
extension TableViewMediator: UITableViewDataSource {
...
public func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == loadingIndicatorSection {
return 1 // just one cell
}
// external numbers
return dataSource!.tableView(tableView, numberOfRowsInSection: section)
}
public func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.section == loadingIndicatorSection {
let cell = tableView.dequeueReusableCellWithIdentifier(TableViewMediator.LoadingCellIdentifier, forIndexPath: indexPath)
if let textLabel = cell.textLabel {
textLabel.textColor = UIColor.redColor()
textLabel.text = "Loading..."
textLabel.textAlignment = .Center
}
cell.selectionStyle = .None
return cell
}
// external cells
return dataSource!.tableView(tableView, cellForRowAtIndexPath: indexPath)
}
}
The loading indicator height:
extension TableViewMediator: UITableViewDelegate {
public func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if indexPath.section == loadingIndicatorSection {
if shouldShowLoadingIndicator { // visible
return 30
}
return 0 // hidden
}
// external heights
if let rowHeight = delegate?.tableView?(tableView, heightForRowAtIndexPath: indexPath) {
return rowHeight
}
// default height
return tableView.rowHeight
}
}
shouldShowLoadingIndicator
:
private(set) var loadingState = false
private var shouldShowLoadingIndicator: Bool {
// don't show the loading indicator if the refresh control is in use
return (loadingState || refreshControl?.refreshing == true)
}
Methods to set loadingState
and UIRefreshControl
handling:
extension TableViewMediator {
private func tryLoadMore(more: Bool) -> Bool {
if let result = self.delegate?.tableView?(tableView, mediator: self, shouldLoadMore: more) {
return result
}
return false
}
public func startLoadingMore(more: Bool, updatesTable: Bool = true) {
if !loadingState && refreshControl?.refreshing == false {
loadingState = tryLoadMore(more)
if updatesTable {
tableView.reloadData()
}
}
}
public func stopLoading(updatesTable updatesTable: Bool = true) {
refreshControl?.endRefreshing()
if loadingState {
loadingState = false
if updatesTable {
tableView.reloadData()
}
}
}
@objc private func refreshControlDidChangeValue(refreshControl: UIRefreshControl) {
if loadingState || !refreshControl.refreshing {
return
}
loadingState = tryLoadMore(false) // more = false
if (!loadingState) {
refreshControl.endRefreshing()
}
}
}
Start & stop methods are public, so a controller can use them like:
- start loading - show loading indicator via the mediator
- handle loading completion - stop loading - hide loading indicator via the mediator
Infinite scroll:
extension TableViewMediator: UITableViewDelegate {
...
public func scrollViewDidScroll(scrollView: UIScrollView) {
// don't forget an external delegate
delegate?.scrollViewDidScroll?(scrollView)
if shouldShowLoadingIndicator {
return
}
if scrollView.contentOffset.y > 0 {
let value = (scrollView.contentSize.height - scrollView.contentOffset.y - scrollView.bounds.height - max(scrollView.contentInset.top, 0) - max(scrollView.contentInset.bottom, 0))
// the end is here
if value <= 0 {
startLoadingMore(true) // more = true
}
}
}
}
Controller
We got a simple mediator now! Let’s write a controller:
class ViewController: UIViewController {
private var tableView: UITableView!
private var mediator: TableViewMediator!
override func loadView() {
super.loadView()
tableView = ...
view.addSubview(tableView)
mediator = TableViewMediator(tableView: tableView)
mediator.setDataSource(self, delegate: self)
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
mediator.startLoadingMore(false) // shows indicator & calls delegate's tableView(_:mediator:shouldLoadMore:)
}
}
extension ViewController: TableViewMediatorDataSource {
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// as usual
...
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// as usual
...
}
}
extension ViewController: TableViewMediatorDelegate {
func tableView(tableView: UITableView, mediator: TableViewMediator, shouldLoadMore more: Bool) -> Bool {
// load data and return true, return false if loading is impossible
...
}
}
See full example here