Basically DataDisplayManager
contains such set of entities.
To simplify explanation of entities relationships we can mark each entity with atomic architecture terms.
Greater component is more complex, and can directly or indirectly operate with smaller components.
RECOMMENDATION Interact, customize or extend atoms and moleculas when it possible and not greater entities.
DataDisplayManager
protocol it is main template with client interface which operating with generators
.
Concrete implementation with injected plugins is page in atomic terms.
In base implementation you can only add generators
and reload collection.
But if your collection can change size dynamically, you need to choose between Manual
and Gravity
manager.
Assume you have such screen with two cells and horizontal scrolling content
Screen:
Cell A:
- item 1
- item 2
...
Cell B:
- item 1
- item 2
...
Each cell content is loading from separate endpoint. It means that sometimes cell A can be loaded first, and sometimes cell B. But cells order should always be the same.
Should we always wait loading of both sections before layout?
No
With manual
approach
private lazy var ddm = tableView.rddm.manualBuilder.build()
private var generatorA: TableCellGenerator?
private var generatorB: TableCellGenerator?
func onContentALoaded(items: [ItemA]) {
let generatorA = GeneratorA(items: items)
if let generatorB = self.generatorB {
ddm.insert(before: generatorB, new: [generatorA])
} else {
ddm.addCellGenerator(generatorA)
ddm.forceRefill()
}
self.generatorA = generatorA
}
func onContentBLoaded(items: [ItemB]) {
let generatorB = GeneratorB(items: items)
if let generatorA = self.generatorA {
ddm.insert(after: generatorA, new: [generatorB])
} else {
ddm.addCellGenerator(generatorB)
ddm.forceRefill()
}
self.generatorB = generatorB
}
With gravity
approach
private lazy var ddm = tableView.rddm.gravityBuilder.build()
func onContentALoaded(items: [ItemA]) {
let generatorA = GeneratorA(items: items)
ddm.addCellGenerator(generatorA)
ddm.forceRefill()
}
func onContentBLoaded(items: [ItemB]) {
let generatorB = GeneratorB(items: items)
ddm.addCellGenerator(generatorB)
ddm.forceRefill()
}
Secret in generators. Each GravityGenerator
has property heaviness. This allow us to forget about sort order on fill stage.
In this example you can see differences between managing approaches and choose one for your needs.
Manual
approach will be more useful in simple collections or when you updating cells based on user actions, like tap on plus button inside specific cell..
- Atom
- Can build cells
- Can configure cells
- Can store closures or current cell state
- Stored inside
DataDisplayManager
and can cached to directly update concrete cell
BaseCellGenerator
is your choice if you need display cell and process selection event.
This is generic generator with only one requirement to cell - conforming to ConfigurableItem
protocol.
Recommended way to create generator YourCellType.rddm.baseGenerator(with: model)
where model is ConfigurableItem.Model
instance.
NOTE If you want store closures, current cell state or extend generator with some protocol, preferable extending BaseCellGenerator
and not create your own.
Extending BaseCellGenerator
to FoldableItem
final class FoldableCellGenerator: BaseCellGenerator<FoldableTableViewCell>, FoldableItem {
// MARK: - FoldableItem
var didFoldEvent = BaseEvent<Bool>()
var isExpanded = false
var childGenerators: [TableCellGenerator] = []
// MARK: - Configuration
override func configure(cell: FoldableTableViewCell, with model: FoldableTableViewCell.Model) {
super.configure(cell: cell, with: model)
didFoldEvent.addListner { isExpanded in
cell.update(expanded: isExpanded)
}
}
}
-
Could not load NIB in bundle ... with name 'YourCellName'
This error appears because you are using class based cells (not UINib), but inside RDDM cells registering via UINib.Your should override default registering in generator
extension YourCellGenerator: TableCellGenerator {
// ...
func registerCell(in tableView: UITableView) {
tableView.register(identifier, forCellReuseIdentifier: String(describing: identifier))
}
// ...
}
or simply fix initialising of your BaseGenerator using BaseCellGenerator.init(with: Cell.Model, registerType: .class)
- Organism
- based on
UITableViewDelegate
orUICollectionViewDelegate
(depends on collection type) - Proxy collection events to plugins
We hope, that bult-in delegate will cover 99% of your needs.
Btw you always can replace delegate with your own implementation.
DO NOT FORGET inherit BaseDelegate or add calls to plugins to not loose bult-in features.
tableView.rddm.baseBuilder.set(delegate: YourCustomDelegate()).build()
- Organism
- Based on
UITableViewDataSource
orUICollectionViewDataSource
(depends on collection type) - Implement
UITableViewDataSourcePrefetching
orUICollectionViewDataSourcePrefetching
(depends on collection type) - Proxy collection events to plugins
We hope, that bult-in datasource will cover 99% of your needs.
Btw you always can replace datasource with your own implementation.
DO NOT FORGET inherit BaseDataSource or add calls to plugins to not loose bult-in features.
tableView.rddm.baseBuilder.set(dataSource: YourCustomDataSource()).build()
- Molecula
- is represent reaction on collection event or events.
- Have access to manager. It means, that you have access to generators and can update collection from plugin.
- Injected in
PluginCollection
- simple list wrapper. It means, that on each event we can have many reactions (plugin-actions). - can not return values to delegate or datasource
You can look at full list of proxy events in enums: TableEvent
, PrefetchEvent
, CollectionEvent
, ScrollEvent
.
Simply add plugin in stage of building
tableView.rddm.baseBuilder.add(plugin: YourCustomPlugin()).build()
And conform generator to concrete PluginAction.GeneratorType
Handling rows selection.
public class TableSelectablePlugin: BaseTablePlugin<TableEvent> {
typealias GeneratorType = SelectableItem
public override init() {}
public override func process(event: TableEvent, with manager: BaseTableManager?) {
switch event {
case .didSelect(let indexPath):
guard let selectable = manager?.generators[indexPath.section][indexPath.row] as? GeneratorType else {
return
}
selectable.didSelectEvent.invoke(with: ())
if selectable.isNeedDeselect {
manager?.view?.deselectRow(at: indexPath, animated: true)
}
default:
break
}
}
}
More examples in bult-in plugins and example project.
- Molecula
- is implement part of delegate, datasource or both
- injected as one optional instance to avoid conflicts
- can return values to delegate or datasource
Basically this entity is adding fixed part of functionality like moving or dragging of cells.
Simply set plugin in stage of building
tableView.rddm.baseBuilder.add(featurePlugin: YourCustomPlugin()).build()
And conform generator to concrete FeaturePlugin.GeneratorType
Look at TableMovablePlugin
, TableSectionTitleDisplayablePlugin
or TableSwipeActionsConfigurationPlugin
and analogs for UICollectionView
.
Little atom created for approaches to animate collection changes.
TableUpdatesAnimator
uses beginUpdates/endUpdates approach which will be deprecated soon.
TableBatchUpdatesAnimator
uses performBatchUpdates approach which available since iOS 11.
Animator is selected based on iOS version, so most likely you never need to change this entity, but we've save such ability for you.
Implement protocol Animator
and set your implementation in builder
tableView.rddm.baseBuilder.set(animator: YourCustomAnimator()).build()