Awesome
Taylor Source
Taylor Source is a Swift framework for creating highly configurable and reusable data sources.
Installation
Taylor Source is available through CocoaPods. To install it, simply add the following line to your Podfile:
pod ’TaylorSource’
Usage
Taylor Source defines a DatasourceType
protocol, which has an associated FactoryType
protocol. These two protocols are the building blocks of the framework. Each has basic concrete implementation classes, StaticDatasource
and BasicFactory
, and have been designed to be easily implemented.
The Basics
The factory is responsible for registering and vending views. It is a generic protocol, which allows you to configure the base type for cell & view types etc.
The datasource in initialised with a factory and data, and it can be used to generate UITableViewDataSource
for example.
We recommend that a datasource be composed inside a bespoke class. For example, assuming a model type Event
, a bespoke datasource would be typed:
class EventDatasource: DatasourceProviderType {
typealias Factory = BasicFactory<Event, EventCell, EventHeaderFooter, UITableView>
typealias Datasource = StaticDatasource<Factory>
let datasource: Datasource
}
Configuring Cells
Configuring cells is done by providing a closure along with a key and description of the cell type. Huh? Lets break this down.
The closure type receives three arguments, the cell, an instance of the model, and the index to the item. In the basic factory, this is an NSIndexPath
. But it can be customised.
The cell is described with either, a reuse identifier and class:
.ClassWithIdentifier(EventCell.self, EventCell.reuseIdentifier)
or a reuse identifier and nib:
.NibWithIdentifier(EventCell.nib, EventCell.reuseIdentifier)
(see ReusableView
and ResuableViewDescriptor
)
The key is just a string used to associate the configuration closure with the cell. Putting this all together means registering and configuring cells is as easy as:
datasource.factory.registerCell(.ClassWithIdentifier(EventCell.self, EventCell.reuseIdentifier), inView: tableView, withKey: “Events”) { (cell, event, indexPath) in
cell.textLabel!.text = “\(event.date.timeAgoSinceNow())”
}
although it’s recommended to provide the configuration block as a class function on your custom cell type. See the example project for this.
Supplementary Views
Supplementary views are headers and footers, although UICollectionView
allows you to define your own. This is all supported, but headers and footers have convenience registration methods.
Apart from the kind
of the supplementary view, they function just like cells, except only one closure can be registered per kind
. In other words, the same closure will configure all your table view section headers.
Advanced
The basics only support static immutable data. Because if you want more than that, it’s best to use a database. The example project in the repo, is inspired by the default Core Data templates from Apple, but are for use with YapDatabase. To get started:
pod ’TaylorSource/YapDatabase’
which make new Factory and Datasource types available. The bespoke datasource from earlier would become:
class EventDatasource: DatasourceProviderType {
typealias Factory = YapDBFactory<Event, EventCell, EventHeaderFooter, UITableView>
typealias Datasource = YapDBDatasource<Factory>
let datasource: Datasource
}
YapDBDatasource
implements DatasourceType
except it fetchs its data from a provided YapDatabase
instance. Internally, it uses YapDatabase’s view mappings, which are configured and composed by a database Observer
. It can be used configured with YapDatabase views, filtered views and searches. I strongly recommend you read the YapDatabase wiki pages. But a simple way to think of views, is that they are like saved database queries, and perform filtering, sorting and mapping of your model items. YapDBDatabase
then provides a common interface for UI components - for all of your queries.
YapDBFactory Cell & View Configuration
The index type provided to the cell (and supplementary view) configuration closure is customisable. For BasicFactory
these are both NSIndexPath
. However for YapDBFactory
the cell index type is a structure which provides not only an NSIndexPath
but also a YapDatabaseReadTransaction
. This means that any additional objects required to configure the cell can be read from the database in the closure.
For supplementary view configure blocks, the index type further has the group which the YapDatabaseView
defined for the current section. Often, this is the best way to define a “model” for a supplementary view - by making the group an identifier of another type which can be read from the database using the read transaction.
Multiple Cell & View Types
Often it might be necessary to have different cell designs in the same screen. Or, perhaps different supplementary views. This can be achieved by registering each cell and view with the datasource’s factory. There are some caveats however.
The Datasource’s Factory implements FactoryType
which defines the generic type CellType
and SupplementaryViewType
. When the design encompasses more than one cell class (or supplementary view class), this generic type becomes the common parent class.
For a table view design, all cells inherit from UITableViewCell
, so given two cell subclasses, EventCell
and ReminderCell
the definition from the example above would be:
class EventCell: UITableViewCell { }
class ReminderCell: UITableViewCell { }
class EventDatasource: DatasourceProviderType {
// Note that cell is common parent class.
typealias Factory = YapDBFactory<Event, UITableViewCell, EventHeaderFooter, UITableView>
typealias Datasource = YapDBDatasource<Factory>
let datasource: Datasource
}
In some situations it is often efficient to use a base cell class, for example, ReminderCell
could in fact be a specialized EventCell
.
class EventCell: UITableViewCell { }
class ReminderCell: EventCell { }
class EventDatasource: DatasourceProviderType {
// Note that here EventCell is our common parent cell.
typealias Factory = YapDBFactory<Event, EventCell, EventHeaderFooter, UITableView>
typealias Datasource = YapDBDatasource<Factory>
let datasource: Datasource
}
The same rules apply for supplementary views e.g. section headers and footers, which inherit from UITableHeaderFooterView
for table views, and UICollectionReusableView
for collection views.
Registering multiple cells
Registering a cell requires the containing view, e.g. UITableView
instance. So, recommended best practice is to initialise a custom datasource provider with the table view. Following on with the example of two cell subclasses:
class EventDatasource: DatasourceProviderType {
typealias Factory = YapDBFactory<Event, UITableViewCell, EventHeaderFooter, UITableView>
typealias Datasource = YapDBDatasource<Factory>
let datasource: Datasource
init(db: YapDatabase, view: Factory.ViewType) {
// Create a factory. This closure is discussed below.
let factory = Factory(cell: { (event, index) in return event.isReminder ? "reminder" : "event" } )
// Create the datasource
datasource = Datasource(
// this can be whatever you want, to help debugging.
id: "Events datasource",
// the YapDatabase instance - see YapDB from YapDatabaseExtensions.
database: db,
// the factory we defined above.
factory: factory,
// provided by TaylorSource to auto-update from YapDatabase changes.
processChanges: view.processChanges,
// See example code for info on creating YapDB Configurations. Essentially this is a database fetch request for Event objects.
configuration: eventsConfiguration()
)
// Register the cells
datasource.factory.registerCell(
// See ReusableView in TaylorSource.
.ClassWithIdentifier(EventCell.self, EventCell.reuseIdentifier),
inView: view,
// The key used to look up the correct cell. See discussion below.
withKey: "event",
// This is a static fnction which returns a closure. See below.
configuration: EventCell.configuration()
)
datasource.factory.registerCell(
.NibWithIdentifier(ReminderCell.nib, ReminderCell.reuseIdentifier),
inView: view,
withKey: "reminder",
configuration: ReminderCell.configuration()
)
}
}
This requires both cell classes to implement ReusableView
and return a configuration block. This is a feature of using TaylorSource, the configuration of cells is defined by a static closure on the cell itself, which decouples them from view controllers - increasing usability.
The factory will vend an instance of the cell Therefore inside the configuration closure, it must be cast. For example:
class EventCell: UITableViewCell {
@IBOutlet var iconView: UIImageView!
class func configuration() -> EventsDatasource.Datasource.FactoryType.CellConfiguration {
/* The `cell` constant here is typed as EventsDatasource.Datasource.FactoryType.CellType,
which in this example is UITableViewCell because we also have ReminderCell registered. */
return { (cell, event, index) in
cell.textLabel!.text = “\(event.date.timeAgoSinceNow())”
if let eventCell = cell as! EventCell {
eventCell.iconView.image = event.icon.image
}
// Note that we are only concerned with configuring the EventCell here.
}
}
}
How does the factory dequeue and configure the correct cell class?
The example above glossed over a closure which the factory was initialized with. This is a critical detail when using multiple cell classes in the same container. In such a scenario, some cells are one design, other cells are another. The logic of this switch is provided to TaylorSource's Factory class (which is the base implementation of FactoryType
) via closures at its initialization. These closures are typed as follows:
Factory.GetCellKey = (Item, CellIndexType) -> String
Factory.GetSupplementaryKey = (SupplementaryIndexType) -> String
For cells, this means that the closure will receive the model item, and it's index inside the datasource. For YapDBDatasources, this means that the index will be a struct providing both the NSIndexPath
but also a YapDatabaseReadTransaction
. The closure should use this information and return a String
, which is used as a look up key for the cell. In our example above, the Event
model has an isReminder
boolean property.
The keys returned by the closure are used as the withKey
argument when registering the corresponding cell.
See the Gallery example project for an demonstration of using multiple cell styles in the same datasource.
Using enums for multiple models
If the design calls for a table view (or collection view) with multiple different cells, each with their own model, use an enum to wrap the corresponding models. For the example above, consider that EventCell
requires an Event
model, and ReminderCell
requires a Reminder
model. How would we put this into a datasource?
struct Event {}
struct Reminder {}
enum CellModel { // but come up with a better name!
case Event(ModuleName.Event) // Use full name definition to avoid clashes.
case Reminder(ModuleName.Reminder)
}
extension CellModel {
static var getCellKey: EventDatasource.Datasource.FactoryType.GetCellKey {
return { (item, _) in
switch item {
case .Event(_): return "event"
case .Reminder(_): return "reminder"
}
}
}
}
class EventDatasource: DatasourceProviderType {
typealias Factory = YapDBFactory<CellModel, UITableViewCell, EventHeaderFooter, UITableView>
typealias Datasource = YapDBDatasource<Factory>
let datasource: Datasource
init(db: YapDatabase, view: Factory.ViewType) {
// Create a factory. Use the closure as defined on the cell model.
let factory = Factory(cell: CellModel.getCellKey)
// etc
}
}
Editable Table View Data Sources
Apple’s UITableViewDataSource
has some optional methods which support the table view when it is in editing mode. Editing a table view in general means inserting, deleting or moving rows.
TaylorSource support this functionality via optional closures defined on DatasourceProviderType
. To enable the optional methods on the generated UITableViewDataSource
from TableViewDatasourceProvider
all four closures must be provided, using an empty implementation if necessary.
See the Events example project for how deleting rows from a YapDatabase
backed table view works.
Design Goals
- Be D.R.Y. - I never want to have to implement
func tableView(_: UITableView, numberOfRowsInSection: Int) -> Int
ever again. - Be super easy to decouple data source types from view controllers.
- Support custom cell and supplementary view classes.
- Customise cells and views with type safe closures.
- Be super easy to compose and extend data source types.
Contributing
Until TaylorSource's APIs have stabilized (post Swift 2.0) I am not looking for contribution for code. Contributions in the form of pull requests for improvements to documentation, spelling mistakes, unit tests, integration aids are always welcome.
Author
Daniel Thorpe - @danthorpe
Licence
Taylor Source is available under the MIT license. See the LICENSE file for more info.