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:
Hedden, Kyle Matthew 2024-09-20 20:26:38 -04:00
parent 81676b701b
commit 047039fdc6
2 changed files with 99 additions and 57 deletions

View File

@ -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?)

View File

@ -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) }
}
}