Skip to content

Protocol-oriented UITableView management, powered by generics and associated types.

License

Notifications You must be signed in to change notification settings

DenTelezhkin/DTTableViewManager

Repository files navigation

Build Status   codecov.io CocoaPod platform   CocoaPod version   Carthage compatible Packagist

DTTableViewManager 5

This is a sister-project for DTCollectionViewManager - great tool for UICollectionView management, built on the same principles.

Powerful generic-based UITableView management framework, written in Swift 3.

Features

  • Powerful mapping system between data models and cells, headers and footers
  • Support for all Swift types - classes, structs, enums, tuples
  • Support for protocols and subclasses as data models
  • Powerful events system, that covers most of UITableView delegate methods
  • Views created from code, XIB, or storyboard
  • Flexible Memory/CoreData/Realm.io storage options
  • Automatic datasource and interface synchronization.
  • Automatic XIB registration and dequeue
  • No type casts required
  • No need to subclass
  • Can be used with UITableViewController, or UIViewController with UITableView, or any other class, that contains UITableView

Requirements

  • Xcode 8 and higher
  • iOS 8.0 and higher / tvOS 9.0 and higher
  • Swift 3

Installation

CocoaPods:

pod 'DTTableViewManager', '~> 5.0.0'

Carthage:

github "DenHeadless/DTTableViewManager" ~> 5.0.0

After running carthage update drop DTTableViewManager.framework and DTModelStorage.framework to Xcode project embedded binaries.

Quick start

DTTableViewManager framework has two parts - core framework, and storage classes. Import them both to your view controller class to start:

import DTTableViewManager
import DTModelStorage

The core object of a framework is DTTableViewManager. Declare your class as DTTableViewManageable, and it will be automatically injected with manager property, that will hold an instance of DTTableViewManager.

Make sure your UITableView outlet is wired to your class and call in viewDidLoad:

	manager.startManaging(withDelegate:self)

Let's say you have an array of Posts you want to display in UITableView. To quickly show them using DTTableViewManager, here's what you need to do:

  • Create UITableViewCell subclass, let's say PostCell. Adopt ModelTransfer protocol
class PostCell : UITableViewCell, ModelTransfer {
	func update(with model: Post) {
		// Fill your cell with actual data
	}
}
  • Call registration methods on your DTTableViewManageable instance
	manager.register(PostCell.self)

ModelType will be automatically gathered from your PostCell. If you have a PostCell.xib file, it will be automatically registered for PostCell. If you have a storyboard with PostCell, set it's reuseIdentifier to be identical to class - "PostCell".

  • Add your posts!
	manager.memoryStorage.addItems(posts)

That's it! It's that easy!

Usage

Mapping and registration

  • register(_:)
  • registerNibNamed(_:for:)
  • registerHeader(_:)
  • registerNibNamed(_:forHeader:)
  • registerFooter(_:)
  • registerNibNamed(_:forFooter:)
  • registerNiblessHeader(_:)
  • registerNiblessFooter(_:)

By default, DTTableViewManager uses section titles and tableView(_:titleForHeaderInSection:) UITableViewDatasource methods. However, if you call any mapping methods for headers or footers, it will automatically switch to using tableView(_:viewForHeaderInSection:) methods and dequeue UITableViewHeaderFooterView instances. Make your UITableViewHeaderFooterView subclasses conform to ModelTransfer protocol to allow them participate in mapping.

You can also use UIView subclasses for headers and footers.

Data models

DTTableViewManager supports all Swift and Objective-C types as data models. This also includes protocols and subclasses.

protocol Food {}
class Apple : Food {}
class Carrot: Food {}

class FoodTableViewCell : UITableViewCell, ModelTransfer {
    func update(with model: Food) {
        // Display food in a cell
    }
}
manager.register(FoodTableViewCell.self)
manager.memoryStorage.addItems([Apple(),Carrot()])

Mappings are resolved simply by calling is type-check. In our example Apple is Food and Carrot is Food, so mapping will work.

Storage classes

DTModelStorage is a framework, that provides storage classes for DTTableViewManager. By default, storage property on DTTableViewManager holds a MemoryStorage instance.

MemoryStorage

MemoryStorage is a class, that manages UITableView models in memory. It has methods for adding, removing, replacing, reordering table view models etc. You can read all about them in DTModelStorage repo. Basically, every section in MemoryStorage is an array of SectionModel objects, which itself is an object, that contains optional header and footer models, and array of table items.

CoreDataStorage

CoreDataStorage is meant to be used with NSFetchedResultsController. It automatically monitors all NSFetchedResultsControllerDelegate methods and updates UI accordingly to it's changes. All you need to do to display CoreData models in your UITableView, is create CoreDataStorage object and set it on your storage property of DTTableViewManager.

It also recommended to use built-in CoreData updater to properly update UITableView:

manager.tableViewUpdater = manager.coreDataUpdater()

Standard flow for creating CoreDataStorage can be something like this:

let request = NSFetchRequest<Post>()
request.entity = NSEntityDescription.entity(forEntityName: String(Post.self), in: context)
request.fetchBatchSize = 20
request.sortDescriptors = [NSSortDescriptor(key: "id", ascending: true)]
let fetchResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
_ = try? fetchResultsController.performFetch()

manager.storage = CoreDataStorage(fetchedResultsController: fetchResultsController)

Keep in mind, that MemoryStorage is not limited to objects in memory. For example, if you have CoreData database, and you now for sure, that number of items is not big, you can choose not to use CoreDataStorage and NSFetchedResultsController. You can fetch all required models, and store them in MemoryStorage.

RealmStorage

RealmStorage is a class, that is meant to be used with realm.io databases. To use RealmStorage with DTTableViewManager, add following line to your Podfile:

    pod 'DTModelStorage/Realm'

If you are using Carthage, RealmStorage will be automatically built along with DTModelStorage.

Reacting to events

Event system in DTTableViewManager 5 allows you to react to UITableViewDelegate and UITableViewDataSource events based on view and model types, completely bypassing any switches or ifs when working with UITableView API. For example:

manager.didSelect(PostCell.self) { cell,model,indexPath in
  print("Selected PostCell with \(model) at \(indexPath)")
}

Important

All events with closures are stored on DTTableViewManager instance, so be sure to declare [weak self] in capture lists to prevent retain cycles.

Event types

There are two types of events:

  1. Event where we have underlying view at runtime
  2. Event where we have only data model, because view has not been created yet.

In the first case, we are able to check view and model types, and pass them into closure. In the second case, however, if there's no view, we can't make any guarantees of which type it will be, therefore it loses view generic type and is not passed to closure. These two types of events have different signature, for example:

// Signature for didSelect event
// We do have a cell, when UITableView calls "tableView(_:didSelectRowAt:)" method
open func didSelect<T:ModelTransfer>(_ cellClass:  T.Type, _ closure: @escaping (T,T.ModelType, IndexPath) -> Void) where T:UITableViewCell


// Signature for heightForCell event
// When UITableView calls "tableView(_:heightForRowAt:)" method, cell is not created yet, so closure contains two arguments instead of three, and there are no guarantees made about cell type, only model type
open func heightForCell<T>(withItem itemType: T.Type, _ closure: @escaping (T, IndexPath) -> CGFloat)

It's also important to understand, that event system is implemented using responds(to:) method override and is working on the following rules:

  • If DTTableViewManageable is implementing delegate method, responds(to:) returns true
  • If DTTableViewManager has events tied to selector being called, responds(to:) also returns true

What this approach allows us to do, is configuring UITableView knowledge about what delegate method is implemented and what is not. For example, DTTableViewManager is implementing tableView(_:heightForRowAt:) method, however if you don't call heightForCell(withItem:_:) method, you are safe to use self-sizing cells in UITableView. While 37 delegate methods are implemented, only those that have events or are implemented by delegate will be called by UITableView.

DTTableViewManager has the same approach for handling each delegate and datasource method:

  • Try to execute event, if cell and model type satisfy requirements
  • Try to call delegate or datasource method on DTTableViewManageable instance
  • If two previous scenarios fail, fallback to whatever default UITableView has for this delegate or datasource method

Events list

Here's full list of all delegate and datasource methods implemented:

UITableViewDataSource

DataSource method Event method Comment
cellForItemAt: configure(::) Called after update(with:) method was called
viewForHeaderInSection: configureHeader(::) Called after update(with:) method was called
viewForFooterInSection: configureFooter(::) Called after update(with:) method was called
commit:forRowAt: commitEditingStyle(for:_:) -
canEditRowAt: canEditCell(withItem:_:) -
canMoveRowAt: canMove(::) -

UITableViewDelegate

Delegate method Event method Comment
heightForRowAt: heightForCell(withItem:_:) -
estimatedHeightForRowAt: estimatedHeightForCell(withItem:_:) -
indentationLevelForRowAt: indentationLevelForCell(withItem:_:) -
willDisplay:forRowAt: willDisplay(::) -
editActionsForRowAt: editActions(for:_:) iOS only
accessoryButtonTappedForRowAt: accessoryButtonTapped(in:_:) -
willSelectRowAt: willSelect(::) -
didSelectRowAt: didSelect(::) -
willDeselectRowAt: willDeselect(::) -
didDeselectRowAt: didDeselect(::) -
willSelectRowAt: willSelect(::) -
heightForHeaderInSection: heightForHeader(withItem:_:) -
heightForFooterInSection: heightForFooter(withItem:_:) -
estimatedHeightForHeaderInSection: estimatedHeightForHeader(withItem:_:) -
estimatedHeightForFooterInSection: estimatedHeightForFooter(withItem:_:) -
heightForHeaderInSection: heightForHeader(withItem:_:) -
willDisplayHeaderView:forSection: willDisplayHeaderView(::) -
willDisplayFooterView:forSection: willDisplayFooterView(::) -
willBeginEditingRowAt: willBeginEditing(::) iOS only
didEndEditingRowAt: didEndEditing(::) iOS only
editingStyleForRowAt: editingStyle(for:_:) -
titleForDeleteConfirmationButtonForRowAt: titleForDeleteConfirmationButton(in:_:) iOS only
shouldIndentWhileEditingRowAt: shouldIndentWhileEditing(::) -
didEndDisplaying:forRowAt: didEndDisplaying(::) -
didEndDisplayingHeaderView:forSection: didEndDisplayingHeaderView(::) -
didEndDisplayingFooterView:forSection: didEndDisplayingFooterView(::) -
shouldShowMenuForRowAt: shouldShowMenu(for:_:) -
canPerformAction:forRowAt:withSender: canPerformAction(for:_:) -
performAction:forRowAt:withSender: performAction(for:_:) -
shouldHighlightRowAt: shouldHighlight(::) -
didHighlightRowAt: didHighlight(::) -
didUnhighlightRowAt: didUnhighlight(::) -
canFocusRowAt: canFocus(::) iOS/tvOS 9.0+

Advanced usage

Reacting to content updates

Sometimes it's convenient to know, when data is updated, for example to hide UITableView, if there's no data. TableViewUpdater has willUpdateContent and didUpdateContent properties, that can help:

updater.willUpdateContent = { update in
  print("UI update is about to begin")
}

updater.didUpdateContent = { update in
  print("UI update finished")
}

Customizing UITableView updates

DTTableViewManager uses TableViewUpdater class by default. However for CoreData you might want to tweak UI updating code. For example, when reloading cell, you might want animation to occur, or you might want to silently update your cell. This is actually how Apple's guide for NSFetchedResultsController suggests you should do. Another interesting thing it suggests that .Move event reported by NSFetchedResultsController should be animated not as a move, but as deletion of old index path and insertion of new one.

If you want to work with CoreData and NSFetchedResultsController, just call:

manager.tableViewUpdater = manager.coreDataUpdater()

TableViewUpdater constructor allows customizing it's basic behaviour:

let updater = TableViewUpdater(tableView: tableView, reloadRow: { indexPath in
  // Reload row
}, animateMoveAsDeleteAndInsert: false)

These are all default options, however you might implement your own implementation of TableViewUpdater, the only requirement is that object needs to conform to StorageUpdating protocol. This gives you full control on how and when DTTableViewManager will update UITableView.

TableViewUpdater also contains all animation options, that can be changed, for example:

updater.deleteSectionAnimation = UITableViewRowAnimation.fade
updater.insertRowAnimation = UITableViewRowAnimation.automatic

Display header on empty section

By default, headers are displayed if there's header model for them in section, even if there are no items in section. This behaviour can be changed:

manager.configuration.displayHeaderOnEmptySection = false
// or
manager.configuration.displayFooterOnEmptySection = false

Also you can use simple String models for header and footer models, without any registration, and they will be used in tableView(_:titleForHeaderInSection:) method automatically.

Customizing mapping resolution

There can be cases, where you might want to customize mappings based on some criteria. For example, you might want to display model in several kinds of cells:

class FoodTextCell: UITableViewCell, ModelTransfer {
    func update(with model: Food) {
        // Text representation
    }
}

class FoodImageCell: UITableViewCell, ModelTransfer {
    func update(with model: Food) {
        // Photo representation
    }
}

manager.register(FoodTextCell.self)
manager.register(FoodImageCell.self)

If you don't do anything, FoodTextCell mapping will be selected as first mapping, however you can adopt ViewModelMappingCustomizing protocol to adjust your mappings:

extension PostViewController : ViewModelMappingCustomizing {
    func viewModelMapping(fromCandidates candidates: [ViewModelMapping], forModel model: Any) -> ViewModelMapping? {
        if let foodModel = model as? Food where foodModel.hasPhoto {
            return candidates.last
        }
        return candidates.first
    }
}

Unregistering mappings

You can unregister cells, headers and footers from DTTableViewManager and UITableView by calling:

manager.unregister(FooCell.self)
manager.unregisterHeader(HeaderView.self)
manager.unregisterFooter(FooterView.self)

This is equivalent to calling tableView(register:nil,forCellWithReuseIdenfier: "FooCell")

Error reporting

In some cases DTTableViewManager will not be able to create cell, header or footer view. This can happen when passed model is nil, or mapping is not set. By default, 'fatalError' method will be called and application will crash. You can improve crash logs by setting your own error handler via closure:

manager.viewFactoryErrorHandler = { error in
    // DTTableViewFactoryError type
    print(error.description)
}

ObjectiveC support

DTTableViewManager is heavily relying on Swift protocol extensions, generics and associated types. Enabling this stuff to work on Objective-c right now is not possible. Because of this DTTableViewManager 4 and greater only supports building from Swift. If you need to use Objective-C, you can use latest Objective-C compatible version of DTTableViewManager.

Documentation

You can view documentation online or you can install it locally using cocoadocs!

Running example project

pod try DTTableViewManager

Thanks

  • Alexey Belkevich for providing initial implementation of CellFactory.
  • Michael Fey for providing insight into NSFetchedResultsController updates done right.
  • Nickolay Sheika for great feedback, that helped shaping 3.0 release.
  • Artem Antihevich for great discussions about Swift generics and type capturing.

About

Protocol-oriented UITableView management, powered by generics and associated types.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published