This is a sister-project for DTCollectionViewManager - great tool for UICollectionView management, built on the same principles.
Powerful protocol-oriented UITableView management framework, written in Swift 2.
- 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
- Views created from code, XIB, or storyboard
- Flexible Memory/CoreData/Custom storage options
- Support for Realm.io databases
- 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
- Xcode 7 and higher
- iOS 8.0 and higher / tvOS 9.0 and higher
- Swift 2
pod 'DTTableViewManager', '~> 4.6.0'
github "DenHeadless/DTTableViewManager" ~> 4.6.0
After running carthage update
drop DTTableViewManager.framework and DTModelStorage.framework to Xcode project embedded binaries.
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
.
First, call startManagingWithDelegate:
to initiate UITableView management. Make sure your UITableView outlet is wired to your class.
manager.startManagingWithDelegate(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 updateWithModel(model: Post)
{
// Fill your cell with actual data
}
}
- Call registration methods on your
DTTableViewManageable
instance
manager.registerCellClass(PostCell)
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!
registerCellClass:
registerNibNamed:forCellClass:
registerHeaderClass:
registerNibNamed:forHeaderClass:
registerFooterClass:
registerNibNamed:forFooterClass:
registerNiblessHeaderClass:
registerNiblessFooterClass
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.
For more detailed look at mapping in DTTableViewManager, check out dedicated Mapping wiki page.
Starting from 4.4.0 release, DTTableViewManager
supports all Swift and Objective-C types as data models. This also includes protocols and subclasses. So now this works:
protocol Food {}
class Apple : Food {}
class Carrot: Food {}
class FoodTableViewCell : UITableViewCell, ModelTransfer {
func updateWithModel(model: Food) {
// Display food in a cell
}
}
manager.registerCellClass(FoodTableViewCell)
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.
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 updateWithModel(model: Food) {
// Text representation
}
}
class FoodImageCell: UITableViewCell, ModelTransfer {
func updateWithModel(model: Food) {
// Photo representation
}
}
manager.registerCellClass(FoodTextCell)
manager.registerCellClass(FoodImageCell)
If you don't do anything, FoodTextCell mapping will be selected as first mapping, however you can adopt DTViewModelMappingCustomizable
protocol to adjust your mappings:
extension PostViewController : DTViewModelMappingCustomizable {
func viewModelMappingFromCandidates(candidates: [ViewModelMapping], forModel model: Any) -> ViewModelMapping? {
if let foodModel = model as? Food where foodModel.hasPhoto {
return candidates.last
}
return candidates.first
}
}
DTModelStorage is a framework, that provides storage classes for DTTableViewManager
. By default, storage property on DTTableViewManager
holds a MemoryStorage
instance.
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
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
.
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
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
.
For in-depth look at how subclassing storage classes can improve your code base, read this article on wiki.
There are two types of events reaction. The first and recommended one is to pass method pointers to DTTableViewManager
. For example, selection:
manager.cellSelection(PostViewController.selectedPost)
func selectedPost(cell: PostCell, post: Post, indexPath: NSIndexPath) {
// Do something with Post
}
DTTableViewManager
automatically breaks retain cycles, that can happen when you pass method pointers around. There's no need to worry about [weak self] stuff.
There are also methods for configuring cells, headers and footers:
manager.cellConfiguration(PostViewController.configurePostCell)
manager.headerConfiguration(PostViewController.configurePostsHeader)
manager.footerConfiguration(PostViewController.configurePostsFooter)
And of course, you can always use dynamicType instead of directly referencing type name:
manager.cellSelection(self.dynamicType.selectedPost)
Another way of dealing with events, is registrating closures, which work basically in the same way, however you need to break retain cycles yourself.
Important
Unlike methods with method pointers, all events with closures are stored on DTTableViewManager
instance, so be sure to declare [weak self] in capture lists to prevent retain cycles.
Instead of reacting to cell selection at UITableView NSIndexPath, DTTableViewManager
allows you to react when user selects concrete model:
manager.whenSelected(PostCell.self) { [weak self] postCell, post, indexPath in
print("Selected \(post) in \(postCell) at \(indexPath)")
}
Thanks to generics, postCell
and post
are already a concrete type, there's no need to check types and cast.
Although in most cases your cell can update it's UI with model inside updateWithModel:
method, sometimes you may need to additionally configure it from controller. There are three events you can react to:
manager.configureCell(PostCell.self) { [weak self] postCell, post, indexPath in }
manager.configureHeader(PostHeader.self) { [weak self] postHeader, postHeaderModel, sectionIndex in }
manager.configureFooter(PostFooter.self) { [weak self] postFooter, postFooterModel, sectionIndex in }
Sometimes it's convenient to know, when data is updated, for example to hide UITableView, if there's no data. Conform to DTTableViewContentUpdatable
protocol and implement one of the following methods:
extension PostsViewController: DTTableViewContentUpdatable {
func beforeContentUpdate() {
}
func afterContentUpdate() {
}
}
DTTableViewManager
serves as a datasource and the delegate to UITableView
. However, it implements only some of UITableViewDelegate and UITableViewDatasource methods, other methods will be redirected to your controller, if it implements it. So, for example when you need to implement cell height methods, just implement them normally on your UIViewController:
class ViewController : DTTableViewManageable, UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return 44
}
}
There are several convenience model getters, that will allow you to get data model from storage classes. Those include cell, header or footer class types to gather type information and being able to return model of correct type. Again, no need for type casts.
let post = manager.itemForCellClass(PostCell.self, atIndexPath: indexPath)
let postHeaderModel = manager.itemForHeaderClass(PostHeaderClass.self, atSectionIndex: sectionIndex)
let postFooterModel = manager.itemForFooterClass(PostFooterClass.self, atSectionIndex: sectionIndex)
There's also convenience getter, that will allow you to get model from visible UITableViewCell
.
let post = manager.itemForVisibleCell(postCell)
There are various customization options available with DTTableViewManager
. Those are all concentrated in TableViewConfiguration
class. They allow to customize:
- Row insert, update, and delete animation
- Section insert, update and delete animation
- Section header and footer styles - title or view
- Should section header and footer be displayed on empty section
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)
}
DTTableViewManager
is heavily relying on Swift 2 protocol extensions, generics and associated types. Enabling this stuff to work on objective-c right now is not possible. Because of this DTTableViewManager 4 does not support usage in objective-c. If you need to use objective-c, you can use latest compatible version of DTTableViewManager
.
You can view documentation online or you can install it locally using cocoadocs!
Also check out wiki page for some information on DTTableViewManager internals.
pod try DTTableViewManager
- 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.