Digital PCT265 defect MVAPCT-272: Create a list item molecule cache for adding additional list items and quick lookups to the model tree. Add the cell reuse ID to MoleculeInfo identify for quick identification of structural changes.
This commit is contained in:
parent
81676b701b
commit
047039fdc6
@ -11,14 +11,17 @@ public protocol MoleculeListProtocol {
|
||||
/// Asks the delegate for the index of molecule.
|
||||
func getIndexPath(for molecule: ListItemModelProtocol & MoleculeModelProtocol) -> IndexPath?
|
||||
|
||||
/// Asks the delegate to add molecules.
|
||||
/// Asks the delegate to add molecules. Prefer using the molecule relatoinal method over this one to avoid misplacing things mid-transition.
|
||||
func addMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], indexPath: IndexPath, animation: UITableView.RowAnimation?)
|
||||
|
||||
///
|
||||
/// Asks the delegate to add molecules in relation to another molecule. This is the preferred method for relativity.
|
||||
func addMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], position: AddMoleculesPosition, molecule: (ListItemModelProtocol & MoleculeModelProtocol), animation: UITableView.RowAnimation?)
|
||||
|
||||
/// Asks the delegate to remove molecules. Never ask to remove a molecule at an index.
|
||||
//func removeMolecules(at indexPaths: [IndexPath], animation: UITableView.RowAnimation?)
|
||||
/** Asks the delegate to remove molecules. Never ask to remove a molecule at an index.
|
||||
|
||||
Note: Avoid doing this to prevent accidental deletion mid transition. Prefer the declarative approach of deleting a paricular molecule with removeMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol].
|
||||
*/
|
||||
func removeMolecules(at indexPaths: [IndexPath], animation: UITableView.RowAnimation?)
|
||||
|
||||
/// Asks the delegate to remove particular molecules from the list.
|
||||
func removeMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], animation: UITableView.RowAnimation?)
|
||||
|
||||
@ -12,10 +12,11 @@ public struct MoleculeInfo: Hashable, CustomDebugStringConvertible {
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(moleculeId)
|
||||
hasher.combine(cellReuseId)
|
||||
}
|
||||
|
||||
public static func == (lhs: MoleculeInfo, rhs: MoleculeInfo) -> Bool {
|
||||
lhs.moleculeId == rhs.moleculeId
|
||||
lhs.moleculeId == rhs.moleculeId && lhs.cellReuseId == rhs.cellReuseId
|
||||
}
|
||||
|
||||
public var moleculeId: String
|
||||
@ -32,6 +33,10 @@ public struct MoleculeInfo: Hashable, CustomDebugStringConvertible {
|
||||
self.cellReuseId = moleculeClass.nameForReuse(with: listItemModel, delegateObject) ?? listItemModel.moleculeName
|
||||
}
|
||||
|
||||
public func register(with tableView: UITableView) {
|
||||
tableView.register(cellType, forCellReuseIdentifier: cellReuseId)
|
||||
}
|
||||
|
||||
public var debugDescription: String {
|
||||
return "\(Self.self) \(moleculeId)"
|
||||
}
|
||||
@ -39,13 +44,9 @@ public struct MoleculeInfo: Hashable, CustomDebugStringConvertible {
|
||||
|
||||
extension Array where Element == MoleculeInfo {
|
||||
|
||||
func filter(byMolecules molecules: [MoleculeModelProtocol]) -> [MoleculeInfo] {
|
||||
filter { listItemRef in molecules.contains { $0.id == listItemRef.moleculeId } }
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func registerTypes(with tableView: UITableView) -> [MoleculeInfo] {
|
||||
forEach { tableView.register($0.cellType, forCellReuseIdentifier: $0.cellReuseId) }
|
||||
forEach { $0.register(with: tableView) }
|
||||
return self
|
||||
}
|
||||
}
|
||||
@ -92,6 +93,8 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
|
||||
// MARK: - Methods
|
||||
//--------------------------------------------------
|
||||
|
||||
@MainActor public var moleculeModelCache = [String: (ListItemModelProtocol & MoleculeModelProtocol)]()
|
||||
|
||||
public var dataSource: UITableViewDiffableDataSource<Int, MoleculeInfo>!
|
||||
|
||||
open override func createTableView() -> TableView {
|
||||
@ -99,9 +102,9 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
|
||||
dataSource = UITableViewDiffableDataSource(tableView: tableView) { [weak self] tableView, indexPath, moleculeInfo in
|
||||
guard let self = self,
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: moleculeInfo.cellReuseId),
|
||||
let moleculeModel = molecule(for: moleculeInfo.moleculeId)
|
||||
let moleculeModel = moleculeModelCache[moleculeInfo.moleculeId]
|
||||
else { return UITableViewCell() }
|
||||
cell.isHidden = (moleculeModel as? GoneableProtocol)?.gone == true
|
||||
cell.isHidden = moleculeModel.gone
|
||||
(cell as? MoleculeViewProtocol)?.reset()
|
||||
(cell as? MoleculeListCellProtocol)?.setLines(with: templateModel?.line, delegateObject: delegateObjectIVar, additionalData: nil, indexPath: indexPath)
|
||||
if let moleculeView = cell as? MoleculeViewProtocol {
|
||||
@ -116,13 +119,6 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
|
||||
return tableView
|
||||
}
|
||||
|
||||
// TODO: This assumes a molecule is always present in the model tree. What if its in another data store? Also we should attempt O(1) for being in the render phase.
|
||||
func molecule(for id: String) -> MoleculeModelProtocol? {
|
||||
templateModel?.findFirstMolecule() { molecule in
|
||||
molecule.id == id
|
||||
}
|
||||
}
|
||||
|
||||
open override func parsePageJSON(loadObject: MVMCoreLoadObject) throws -> PageModelProtocol {
|
||||
return try parseTemplate(loadObject: loadObject)
|
||||
}
|
||||
@ -167,7 +163,7 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
|
||||
|
||||
if pageModel != nil {
|
||||
setup()
|
||||
registerWithTable()
|
||||
registerWithTable() // In this template, the cells are registered in setup. However, in subclasses we might still be registering cells in a secondary table outside of this template's content.
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,7 +175,14 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
|
||||
|
||||
guard let molecules else { return }
|
||||
|
||||
// For updating individual specfied molecules. (Not a full table reload done in the base class.) These molecule types should remain the same type by replacement standards.
|
||||
molecules.forEach({ molecule in
|
||||
// Replace any top level cell data if required.
|
||||
if let molecule = molecule as? (ListItemModelProtocol & MoleculeModelProtocol), moleculeModelCache.keys.contains(molecule.id) {
|
||||
moleculeModelCache[molecule.id] = molecule
|
||||
}
|
||||
})
|
||||
|
||||
// For updating individual specfied molecules. (Not a full table reload done in the base class.)
|
||||
newData(for: molecules)
|
||||
}
|
||||
|
||||
@ -211,13 +214,14 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
|
||||
|
||||
open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
guard let moleculeId = dataSource.itemIdentifier(for: indexPath)?.moleculeId,
|
||||
let moleculeModel = molecule(for: moleculeId) else { return 0 }
|
||||
return (moleculeModel as? GoneableProtocol)?.gone == true ? 0 : UITableView.automaticDimension
|
||||
let moleculeModel = moleculeModelCache[moleculeId] else { return 0 }
|
||||
return moleculeModel.gone ? 0 : UITableView.automaticDimension
|
||||
}
|
||||
|
||||
open func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
guard let moleculeInfo = dataSource.itemIdentifier(for: indexPath),
|
||||
let moleculeModel = molecule(for: moleculeInfo.moleculeId),
|
||||
let moleculeModel = moleculeModelCache[moleculeInfo.moleculeId],
|
||||
!moleculeModel.gone,
|
||||
let estimatedHeight = moleculeInfo.cellType.estimatedHeight(with: moleculeModel, delegateObject() as? MVMCoreUIDelegateObject)
|
||||
else { return 0 }
|
||||
|
||||
@ -299,31 +303,57 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
|
||||
}
|
||||
|
||||
var snapshot = dataSource.snapshot()
|
||||
let updatedListItems: [MoleculeInfo] = snapshot.itemIdentifiers.filter { listItemRef -> Bool in
|
||||
guard let model = molecule(for: listItemRef.moleculeId) else { return false }
|
||||
|
||||
// Find all the listItems that have structures and table cell references.
|
||||
let replacementPairs: [(MoleculeInfo, MoleculeInfo)] = snapshot.itemIdentifiers.compactMap { listItemRef in
|
||||
guard let model = moleculeModelCache[listItemRef.moleculeId],
|
||||
let updatedListItemRef = MoleculeInfo(listItemModel: model, delegateObject: delegateObjectIVar)
|
||||
else { return nil }
|
||||
if updatedListItemRef.moleculeId == listItemRef.moleculeId && updatedListItemRef != listItemRef {
|
||||
updatedListItemRef.register(with: tableView)
|
||||
return (listItemRef, updatedListItemRef)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Swap them out for new list items.
|
||||
for (originalRef, replacementRef) in replacementPairs {
|
||||
snapshot.insertItems([replacementRef], afterItem: originalRef)
|
||||
snapshot.deleteItems([originalRef])
|
||||
}
|
||||
|
||||
// Out of the remaining items, check which contain molecules that have been updated.
|
||||
let updatedListItems = snapshot.itemIdentifiers.filter({ listItemRef in
|
||||
// Ignore the brand new.
|
||||
guard !replacementPairs.map({ $0.1 }).contains(listItemRef) else { return false }
|
||||
guard let model = moleculeModelCache[listItemRef.moleculeId] else { return false }
|
||||
return model.findFirstMolecule(by: { existingMolecule in
|
||||
molecules.contains { moleculeModel in
|
||||
existingMolecule.moleculeName == moleculeModel.moleculeName && existingMolecule.id == moleculeModel.id
|
||||
}
|
||||
}) != nil
|
||||
}
|
||||
})
|
||||
|
||||
if #available(iOS 15.0, *) {
|
||||
snapshot.reconfigureItems(updatedListItems)
|
||||
} else {
|
||||
// A full reload can cause a flicker / animation. Better to avoid with above reconfigure method.
|
||||
snapshot.reloadItems(updatedListItems)
|
||||
}
|
||||
// TODO: Thoughts on fading on cell structural replacements?
|
||||
// dataSource.defaultRowAnimation = .fade
|
||||
dataSource.apply(snapshot)
|
||||
|
||||
// Refresh the cell. (reload loses cell selection). TODO: Check if necessary and timing.
|
||||
if let selectedIndex = tableView.indexPathForSelectedRow {
|
||||
tableView.selectRow(at: selectedIndex, animated: false, scrollPosition: .none)
|
||||
dataSource.defaultRowAnimation = .fade
|
||||
dataSource.apply(snapshot) { [self] in
|
||||
dataSource.defaultRowAnimation = .automatic
|
||||
|
||||
// Refresh the cell. (reload loses cell selection).
|
||||
if let selectedIndex = tableView.indexPathForSelectedRow {
|
||||
tableView.selectRow(at: selectedIndex, animated: false, scrollPosition: .none)
|
||||
}
|
||||
|
||||
// If the height of the cells change, we need to update the constraints.
|
||||
view.setNeedsUpdateConstraints()
|
||||
}
|
||||
|
||||
// If the height of the cells change, we need to update the constraints.
|
||||
view.setNeedsUpdateConstraints()
|
||||
}
|
||||
|
||||
///Helper functions to update header/footer view
|
||||
@ -343,14 +373,21 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
|
||||
//--------------------------------------------------
|
||||
|
||||
/// Sets up the header, footer, molecule list and ensures no errors loading all content.
|
||||
func setup() {
|
||||
guard let listItems = templateModel?.molecules?.asMoleculeInfoRef(delegateObject: delegateObjectIVar).registerTypes(with: tableView) else { return }
|
||||
@MainActor func setup() {
|
||||
guard let listItemModels = templateModel?.molecules else { return }
|
||||
moleculeModelCache.removeAll() // Refresh the cache for full page reload.
|
||||
let listItems = register(listItemModels: listItemModels)
|
||||
var initialDataSnapshot = ListDataSnapshot()
|
||||
initialDataSnapshot.appendSections([0])
|
||||
initialDataSnapshot.appendItems(listItems)
|
||||
dataSource.apply(initialDataSnapshot, animatingDifferences: false)
|
||||
}
|
||||
|
||||
func register(listItemModels: [any ListItemModelProtocol & MoleculeModelProtocol]) -> [MoleculeInfo] {
|
||||
listItemModels.forEach { moleculeModelCache[$0.id] = $0 }
|
||||
return listItemModels.asMoleculeInfoRef(delegateObject: delegateObjectIVar).registerTypes(with: tableView)
|
||||
}
|
||||
|
||||
/// Adds modules from requiredModules() to the MVMCoreViewControllerMapping.requiredModules map.
|
||||
open func updateRequiredModules() {
|
||||
if let requiredModules = requiredModules(), let pageType = pageType {
|
||||
@ -373,27 +410,26 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
|
||||
|
||||
return modules
|
||||
}
|
||||
|
||||
/// Checks if the two molecules are equal
|
||||
private func equal(moleculeA: MoleculeModelProtocol, moleculeB: MoleculeModelProtocol) -> Bool {
|
||||
moleculeA.id == moleculeB.id
|
||||
}
|
||||
}
|
||||
|
||||
extension MoleculeListTemplate: MoleculeListProtocol {
|
||||
|
||||
// public func removeMolecules(at indexPaths: [IndexPath], animation: UITableView.RowAnimation?) {
|
||||
// for (index, indexPath) in indexPaths.sorted().enumerated() {
|
||||
// let removeIndex = indexPath.row - index
|
||||
// moleculesInfo?.remove(at: removeIndex)
|
||||
// }
|
||||
//
|
||||
// guard let animation = animation,
|
||||
// indexPaths.count > 0 else { return }
|
||||
// tableView?.deleteRows(at: indexPaths, with: animation)
|
||||
// updateViewConstraints()
|
||||
// view.setNeedsLayout()
|
||||
// }
|
||||
public func removeMolecules(at indexPaths: [IndexPath], animation: UITableView.RowAnimation?) {
|
||||
var snapshot = dataSource.snapshot()
|
||||
let rowsToDelete = indexPaths.map { $0.row }
|
||||
let itemsToDelete = snapshot.itemIdentifiers.enumerated().compactMap { index, itemIdentifier in
|
||||
return rowsToDelete.contains(index) ? itemIdentifier : nil
|
||||
}
|
||||
snapshot.deleteItems(itemsToDelete)
|
||||
if let animation {
|
||||
dataSource.defaultRowAnimation = animation
|
||||
}
|
||||
dataSource.apply(snapshot) {
|
||||
self.dataSource.defaultRowAnimation = .automatic
|
||||
}
|
||||
updateViewConstraints()
|
||||
view.setNeedsLayout()
|
||||
}
|
||||
|
||||
/// Convenience function that removes the passed molecule
|
||||
public func removeMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], animation: UITableView.RowAnimation?) {
|
||||
@ -402,11 +438,13 @@ extension MoleculeListTemplate: MoleculeListProtocol {
|
||||
dataSource.apply(snapshot, animatingDifferences: true) {
|
||||
self.updateViewConstraints()
|
||||
self.view.setNeedsLayout()
|
||||
molecules.forEach { self.moleculeModelCache.removeValue(forKey: $0.id) }
|
||||
}
|
||||
}
|
||||
|
||||
public func addMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], indexPath: IndexPath, animation: UITableView.RowAnimation?) {
|
||||
let additionalMoleculesRef = molecules.asMoleculeInfoRef(delegateObject: delegateObjectIVar).registerTypes(with: tableView)
|
||||
|
||||
var snapshot = dataSource.snapshot()
|
||||
debugLog("[ADD] State before: \(snapshot.itemIdentifiers)")
|
||||
if let targetMoleculeRef = snapshot.itemIdentifiers[safe: indexPath.row] {
|
||||
@ -428,7 +466,7 @@ extension MoleculeListTemplate: MoleculeListProtocol {
|
||||
var snapshot = dataSource.snapshot()
|
||||
debugLog("[ADD] State before: \(snapshot.itemIdentifiers)")
|
||||
|
||||
let additionalMoleculesRef = molecules.asMoleculeInfoRef(delegateObject: delegateObjectIVar).registerTypes(with: tableView)
|
||||
let additionalMoleculesRef = register(listItemModels: molecules)
|
||||
switch position {
|
||||
case .below:
|
||||
snapshot.insertItems(additionalMoleculesRef, afterItem: targetMoleculeRef)
|
||||
@ -463,7 +501,7 @@ extension MoleculeListTemplate: MoleculeListProtocol {
|
||||
debugLog("[SWAP] State after delete: \(snapshot.itemIdentifiers)")
|
||||
|
||||
if let molecule, let targetRef = MoleculeInfo(listItemModel: molecule) {
|
||||
let replacementRefs = replacements.asMoleculeInfoRef(delegateObject: delegateObjectIVar).registerTypes(with: tableView)
|
||||
let replacementRefs = register(listItemModels: replacements)
|
||||
switch position {
|
||||
case .below:
|
||||
snapshot.insertItems(replacementRefs, afterItem: targetRef)
|
||||
@ -478,6 +516,7 @@ extension MoleculeListTemplate: MoleculeListProtocol {
|
||||
self.dataSource.defaultRowAnimation = .automatic
|
||||
self.updateViewConstraints()
|
||||
self.view.setNeedsLayout()
|
||||
molecules.forEach { self.moleculeModelCache.removeValue(forKey: $0.id) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user