Digital PCT265 defect MVAPCT-272: Intial compiling cut of ListTemplate moving to UITableviewDiffableDataSource. Warning -- Still very runtime unstable.
This commit is contained in:
parent
d2e841c533
commit
2c71269518
@ -14,25 +14,20 @@ public protocol MoleculeListProtocol {
|
||||
/// Asks the delegate to add molecules.
|
||||
func addMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], indexPath: IndexPath, animation: UITableView.RowAnimation?)
|
||||
|
||||
/// Asks the delegate to remove molecules.
|
||||
func removeMolecules(at indexPaths: [IndexPath], animation: UITableView.RowAnimation?)
|
||||
///
|
||||
func addMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], after 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 particular molecules from the list.
|
||||
func removeMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], animation: UITableView.RowAnimation?)
|
||||
|
||||
/// Asks the delegate to swap batches of molecules.
|
||||
func swapMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], with replacements: [ListItemModelProtocol & MoleculeModelProtocol], at indexPath: IndexPath, animation: UITableView.RowAnimation?)
|
||||
}
|
||||
|
||||
extension MoleculeListProtocol {
|
||||
/// Convenience function that removes the passed molecule
|
||||
public func removeMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], animation: UITableView.RowAnimation?) {
|
||||
var indexPaths: [IndexPath] = []
|
||||
for molecule in molecules {
|
||||
guard let indexPath = getIndexPath(for: molecule) else { continue }
|
||||
indexPaths.append(indexPath)
|
||||
}
|
||||
if indexPaths.count > 0 {
|
||||
removeMolecules(at: indexPaths, animation: animation)
|
||||
}
|
||||
}
|
||||
|
||||
/// Asks the delegate to swap batches of molecules.
|
||||
func swapMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], with replacements: [ListItemModelProtocol & MoleculeModelProtocol], after molecule: (ListItemModelProtocol & MoleculeModelProtocol)?, animation: UITableView.RowAnimation?)
|
||||
}
|
||||
|
||||
public extension MVMCoreUIDelegateObject {
|
||||
|
||||
@ -8,16 +8,69 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
public typealias MoleculeInfo = (identifier: String, class: AnyClass, molecule: MoleculeModelProtocol)
|
||||
public struct MoleculeInfo: Hashable {
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
|
||||
public static func == (lhs: MoleculeInfo, rhs: MoleculeInfo) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
|
||||
public let id: String
|
||||
public let cellReuseId: String
|
||||
public let cellType: MoleculeViewProtocol.Type
|
||||
public var model: ListItemModelProtocol & MoleculeModelProtocol
|
||||
|
||||
init?(listItemModel: ListItemModelProtocol & MoleculeModelProtocol, delegateObject: MVMCoreUIDelegateObject? = nil) {
|
||||
guard let moleculeClass = ModelRegistry.getMoleculeClass(listItemModel)
|
||||
else { return nil }
|
||||
|
||||
self.model = listItemModel
|
||||
self.id = model.id
|
||||
self.cellType = moleculeClass
|
||||
self.cellReuseId = moleculeClass.nameForReuse(with: listItemModel, delegateObject) ?? listItemModel.moleculeName
|
||||
}
|
||||
|
||||
func doesMatch(_ molecule: MoleculeModelProtocol) -> Bool {
|
||||
id == molecule.id
|
||||
}
|
||||
|
||||
func contains(oneOf moleculeModels: [MoleculeModelProtocol]) -> MoleculeModelProtocol? {
|
||||
model.findFirstMolecule(by: { existingMolecule in
|
||||
moleculeModels.contains { moleculeModel in
|
||||
existingMolecule.moleculeName == moleculeModel.moleculeName && existingMolecule.id == moleculeModel.id
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == MoleculeInfo {
|
||||
|
||||
func filter(byMolecules molecules: [MoleculeModelProtocol]) -> [MoleculeInfo] {
|
||||
filter { listItemRef in molecules.contains { $0.id == listItemRef.model.id } }
|
||||
}
|
||||
|
||||
func registerTypes(with tableView: UITableView) {
|
||||
forEach { tableView.register($0.cellType, forCellReuseIdentifier: $0.cellReuseId) }
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == ListItemModelProtocol & MoleculeModelProtocol {
|
||||
func asMoleculeInfoRef() -> [MoleculeInfo] {
|
||||
compactMap { MoleculeInfo(listItemModel: $0) }
|
||||
}
|
||||
}
|
||||
|
||||
open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol {
|
||||
|
||||
public typealias ListDataSnapshot = NSDiffableDataSourceSnapshot<Int, MoleculeInfo>
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Stored Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
public var moleculesInfo: [MoleculeInfo]?
|
||||
|
||||
var observer: NSKeyValueObservation?
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -46,6 +99,29 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
|
||||
// MARK: - Methods
|
||||
//--------------------------------------------------
|
||||
|
||||
public var dataSource: UITableViewDiffableDataSource<Int, MoleculeInfo>!
|
||||
|
||||
open override func createTableView() -> TableView {
|
||||
let tableView = super.createTableView()
|
||||
dataSource = UITableViewDiffableDataSource(tableView: tableView) { [weak self] tableView, indexPath, moleculeInfo in
|
||||
guard let self = self,
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: moleculeInfo.cellReuseId)
|
||||
else { return UITableViewCell() }
|
||||
cell.isHidden = moleculeInfo.model.gone == true
|
||||
(cell as? MoleculeViewProtocol)?.reset()
|
||||
(cell as? MoleculeListCellProtocol)?.setLines(with: templateModel?.line, delegateObject: delegateObjectIVar, additionalData: nil, indexPath: indexPath)
|
||||
if let moleculeView = cell as? MoleculeViewProtocol {
|
||||
updateMoleculeView(moleculeView, from: moleculeInfo.model)
|
||||
}
|
||||
(cell as? MVMCoreViewProtocol)?.updateView(tableView.bounds.width)
|
||||
// Neded to fix an apple defect where the cell is not the correct size on certain devices for certain cells
|
||||
cell.setNeedsLayout()
|
||||
return cell
|
||||
}
|
||||
tableView.dataSource = dataSource
|
||||
return tableView
|
||||
}
|
||||
|
||||
open override func parsePageJSON(loadObject: MVMCoreLoadObject) throws -> PageModelProtocol {
|
||||
return try parseTemplate(loadObject: loadObject)
|
||||
}
|
||||
@ -103,12 +179,6 @@ 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 index = moleculesInfo?.firstIndex(where: { $0.molecule.id == molecule.id }) {
|
||||
moleculesInfo?[index].molecule = molecule
|
||||
}
|
||||
})
|
||||
newData(for: molecules)
|
||||
}
|
||||
|
||||
@ -140,46 +210,46 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
|
||||
|
||||
open override func registerWithTable() {
|
||||
super.registerWithTable()
|
||||
guard let moleculesInfo = moleculesInfo else { return }
|
||||
|
||||
for moleculeInfo in moleculesInfo {
|
||||
tableView?.register(moleculeInfo.class, forCellReuseIdentifier: moleculeInfo.identifier)
|
||||
for moleculeInfo in dataSource.snapshot().itemIdentifiers {
|
||||
tableView?.register(moleculeInfo.cellType, forCellReuseIdentifier: moleculeInfo.cellReuseId)
|
||||
}
|
||||
}
|
||||
|
||||
open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
return (getMoleculeInfo(for: indexPath)?.molecule as? GoneableProtocol)?.gone == true ? 0 : UITableView.automaticDimension
|
||||
return (dataSource.itemIdentifier(for: indexPath)?.model as? GoneableProtocol)?.gone == true ? 0 : UITableView.automaticDimension
|
||||
}
|
||||
|
||||
open func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
guard let moleculeInfo = getMoleculeInfo(for: indexPath),
|
||||
let estimatedHeight = (moleculeInfo.class as? MoleculeViewProtocol.Type)?.estimatedHeight(with: moleculeInfo.molecule, delegateObject() as? MVMCoreUIDelegateObject)
|
||||
guard let moleculeInfo = dataSource.itemIdentifier(for: indexPath),
|
||||
let estimatedHeight = moleculeInfo.cellType.estimatedHeight(with: moleculeInfo.model, delegateObject() as? MVMCoreUIDelegateObject)
|
||||
else { return 0 }
|
||||
|
||||
return estimatedHeight
|
||||
}
|
||||
|
||||
open override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
moleculesInfo?.count ?? 0
|
||||
}
|
||||
|
||||
open override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
|
||||
guard let moleculeInfo = getMoleculeInfo(for: indexPath),
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: moleculeInfo.identifier)
|
||||
else { return UITableViewCell() }
|
||||
cell.isHidden = (getMoleculeInfo(for: indexPath)?.molecule as? GoneableProtocol)?.gone == true
|
||||
(cell as? MoleculeViewProtocol)?.reset()
|
||||
(cell as? MoleculeListCellProtocol)?.setLines(with: templateModel?.line, delegateObject: delegateObjectIVar, additionalData: nil, indexPath: indexPath)
|
||||
if let moleculeView = cell as? MoleculeViewProtocol {
|
||||
updateMoleculeView(moleculeView, from: moleculeInfo.molecule)
|
||||
}
|
||||
(cell as? MVMCoreViewProtocol)?.updateView(tableView.bounds.width)
|
||||
// Neded to fix an apple defect where the cell is not the correct size on certain devices for certain cells
|
||||
cell.setNeedsLayout()
|
||||
return cell
|
||||
}
|
||||
// open override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
// debugLog("Number of rows: \(moleculesInfo?.count ?? 0)")
|
||||
// return moleculesInfo?.count ?? 0
|
||||
// }
|
||||
|
||||
// open override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
//
|
||||
// guard let moleculeInfo = getMoleculeInfo(for: indexPath),
|
||||
// let cell = tableView.dequeueReusableCell(withIdentifier: moleculeInfo.identifier)
|
||||
// else { return UITableViewCell() }
|
||||
// cell.isHidden = (getMoleculeInfo(for: indexPath)?.molecule as? GoneableProtocol)?.gone == true
|
||||
// (cell as? MoleculeViewProtocol)?.reset()
|
||||
// (cell as? MoleculeListCellProtocol)?.setLines(with: templateModel?.line, delegateObject: delegateObjectIVar, additionalData: nil, indexPath: indexPath)
|
||||
// if let moleculeView = cell as? MoleculeViewProtocol {
|
||||
// updateMoleculeView(moleculeView, from: moleculeInfo.molecule)
|
||||
// }
|
||||
// (cell as? MVMCoreViewProtocol)?.updateView(tableView.bounds.width)
|
||||
// // Neded to fix an apple defect where the cell is not the correct size on certain devices for certain cells
|
||||
// cell.setNeedsLayout()
|
||||
// return cell
|
||||
// }
|
||||
|
||||
open func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
(cell as? MoleculeListCellProtocol)?.willDisplay()
|
||||
}
|
||||
@ -235,7 +305,8 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
|
||||
// If the view is in a cell, refresh the table ui.
|
||||
let point = molecule.convert(molecule.bounds.origin, to: tableView)
|
||||
if let indexPath = tableView.indexPathForRow(at: point), tableView.indexPathsForVisibleRows?.contains(indexPath) ?? false {
|
||||
refreshTable()
|
||||
//TOOD: Find alternate.
|
||||
//refreshTable()
|
||||
}
|
||||
}
|
||||
|
||||
@ -254,29 +325,25 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
|
||||
}
|
||||
}
|
||||
|
||||
guard let moleculesInfo = moleculesInfo else { return }
|
||||
|
||||
let indicies = moleculesInfo.indices.filter({ index -> Bool in
|
||||
return moleculesInfo[index].molecule.findFirstMolecule(by: { existingMolecule in
|
||||
molecules.contains { newMolecule in
|
||||
existingMolecule.moleculeName == newMolecule.moleculeName && equal(moleculeA: existingMolecule, moleculeB: newMolecule)
|
||||
}
|
||||
}) != nil
|
||||
})
|
||||
|
||||
// Refresh the cell. (reload loses cell selection)
|
||||
let selectedIndex = tableView.indexPathForSelectedRow
|
||||
let indexPaths = indicies.map {
|
||||
return IndexPath(row: $0, section: 0)
|
||||
var snapshot = dataSource.snapshot()
|
||||
let updatedListItems: [MoleculeInfo] = snapshot.itemIdentifiers.compactMap { listItemRef -> MoleculeInfo? in
|
||||
var listItemRef = listItemRef
|
||||
guard let matchedMolecule = listItemRef.contains(oneOf: molecules) else { return nil }
|
||||
if let matchedMolecule = matchedMolecule as? (ListItemModelProtocol & MoleculeModelProtocol), listItemRef.doesMatch(matchedMolecule) {
|
||||
listItemRef.model = matchedMolecule // Replace the top level molecule if it changed.
|
||||
}
|
||||
return listItemRef
|
||||
}
|
||||
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)
|
||||
}
|
||||
dataSource.apply(snapshot)
|
||||
|
||||
debugLog("Refreshing rows \(indexPaths.map { $0.row })")
|
||||
|
||||
// All rows should have been layed out already on the first newDataBuildScreen reload with the getMoleculeInfoList call. Therefore, we can be safe to assume the top level cell configuration will not be modified and only the child content will be updated allowing us to leverage this more efficient method.
|
||||
// A full reload can cause a flicker / animation when fetching an entirely new cell. Better to avoid with reconfigure method.
|
||||
tableView.reconfigureRows(at: indexPaths)
|
||||
|
||||
if let selectedIndex = selectedIndex {
|
||||
// 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)
|
||||
}
|
||||
|
||||
@ -300,42 +367,38 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
|
||||
// MARK: - Convenience
|
||||
//--------------------------------------------------
|
||||
|
||||
/// Returns the (identifier, class) of the molecule for the given model.
|
||||
func createMoleculeInfo(with listItem: MoleculeModelProtocol?) -> MoleculeInfo? {
|
||||
|
||||
guard let listItem = listItem,
|
||||
let moleculeClass = ModelRegistry.getMoleculeClass(listItem)
|
||||
else { return nil }
|
||||
|
||||
let moleculeName = moleculeClass.nameForReuse(with: listItem, delegateObject() as? MVMCoreUIDelegateObject) ?? listItem.moleculeName
|
||||
|
||||
return (moleculeName, moleculeClass, listItem)
|
||||
}
|
||||
|
||||
/// Returns the (identifier, class) of the molecule for the indexPath.
|
||||
func getMoleculeInfo(for indexPath: IndexPath) -> MoleculeInfo? {
|
||||
moleculesInfo?[safe: indexPath.row]
|
||||
}
|
||||
// func getMoleculeInfo(for indexPath: IndexPath) -> MoleculeInfo? {
|
||||
// moleculesInfo?[safe: indexPath.row]
|
||||
// }
|
||||
|
||||
/// Sets up the molecule list and ensures no errors loading all content.
|
||||
func getMoleculeInfoList() -> [MoleculeInfo]? {
|
||||
|
||||
var moleculeList: [MoleculeInfo] = []
|
||||
|
||||
if let molecules = templateModel?.molecules {
|
||||
for molecule in molecules {
|
||||
if let info = createMoleculeInfo(with: molecule) {
|
||||
moleculeList.append(info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return moleculeList.count > 0 ? moleculeList : nil
|
||||
}
|
||||
// func getMoleculeInfoList() -> [MoleculeInfo]? {
|
||||
//
|
||||
// var moleculeList: [MoleculeInfo] = []
|
||||
//
|
||||
// if let molecules = templateModel?.molecules {
|
||||
// for molecule in molecules {
|
||||
// if let info = MoleculeInfo(with: molecule) {
|
||||
// moleculeList.append(info)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return moleculeList.count > 0 ? moleculeList : nil
|
||||
// }
|
||||
|
||||
/// Sets up the header, footer, molecule list and ensures no errors loading all content.
|
||||
func setup() {
|
||||
moleculesInfo = getMoleculeInfoList()
|
||||
guard let listItems = templateModel?.molecules?.compactMap({ MoleculeInfo(listItemModel: $0, delegateObject: delegateObject() as? MVMCoreUIDelegateObject)}) else {
|
||||
debugLog("There is no data to display.")
|
||||
return
|
||||
}
|
||||
|
||||
var initialDataSnapshot = ListDataSnapshot()
|
||||
initialDataSnapshot.appendSections([0])
|
||||
initialDataSnapshot.appendItems(listItems)
|
||||
dataSource.apply(initialDataSnapshot)
|
||||
}
|
||||
|
||||
/// Adds modules from requiredModules() to the MVMCoreViewControllerMapping.requiredModules map.
|
||||
@ -368,90 +431,84 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// 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()
|
||||
// }
|
||||
|
||||
/// Convenience function that removes the passed molecule
|
||||
public func removeMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], animation: UITableView.RowAnimation?) {
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.deleteItems(molecules.asMoleculeInfoRef())
|
||||
dataSource.apply(snapshot, animatingDifferences: true) {
|
||||
self.updateViewConstraints()
|
||||
self.view.setNeedsLayout()
|
||||
}
|
||||
|
||||
guard let animation = animation,
|
||||
indexPaths.count > 0 else { return }
|
||||
tableView?.deleteRows(at: indexPaths, with: animation)
|
||||
updateViewConstraints()
|
||||
view.setNeedsLayout()
|
||||
}
|
||||
|
||||
public func addMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], indexPath: IndexPath, animation: UITableView.RowAnimation?) {
|
||||
var indexPaths: [IndexPath] = []
|
||||
|
||||
for molecule in molecules {
|
||||
if let info = self.createMoleculeInfo(with: molecule) {
|
||||
self.tableView?.register(info.class, forCellReuseIdentifier: info.identifier)
|
||||
let index = indexPath.row + indexPaths.count
|
||||
self.moleculesInfo?.insert(info, at: index)
|
||||
indexPaths.append(IndexPath(row: index, section: 0))
|
||||
}
|
||||
var snapshot = dataSource.snapshot()
|
||||
guard let listItemRef = snapshot.itemIdentifiers[safe: indexPath.row] else { return }
|
||||
snapshot.insertItems(molecules.asMoleculeInfoRef(), afterItem: listItemRef)
|
||||
dataSource.apply(snapshot, animatingDifferences: true) {
|
||||
self.updateViewConstraints()
|
||||
self.view.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
public func addMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], after molecule: (ListItemModelProtocol & MoleculeModelProtocol), animation: UITableView.RowAnimation?) {
|
||||
guard let listMoleculeRef = MoleculeInfo(listItemModel: molecule) else { return }
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.insertItems(molecules.asMoleculeInfoRef(), afterItem: listMoleculeRef)
|
||||
dataSource.apply(snapshot, animatingDifferences: true) {
|
||||
self.updateViewConstraints()
|
||||
self.view.setNeedsLayout()
|
||||
}
|
||||
|
||||
guard let animation = animation,
|
||||
indexPaths.count > 0 else { return }
|
||||
self.tableView?.insertRows(at: indexPaths, with: animation)
|
||||
self.updateViewConstraints()
|
||||
self.view.setNeedsLayout()
|
||||
}
|
||||
|
||||
public func swapMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], with replacements: [ListItemModelProtocol & MoleculeModelProtocol], at indexPath: IndexPath, animation: UITableView.RowAnimation?) {
|
||||
swapMolecules(molecules, with: replacements, after: dataSource.snapshot().itemIdentifiers[safe: indexPath.row] as? (ListItemModelProtocol & MoleculeModelProtocol), animation: animation)
|
||||
}
|
||||
|
||||
tableView.beginUpdates()
|
||||
public func swapMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], with replacements: [ListItemModelProtocol & MoleculeModelProtocol], after targetMolecule: (ListItemModelProtocol & MoleculeModelProtocol)?, animation: UITableView.RowAnimation?) {
|
||||
|
||||
var indexPaths: [IndexPath] = []
|
||||
for molecule in molecules {
|
||||
guard let indexPath = getIndexPath(for: molecule) else { continue }
|
||||
indexPaths.append(indexPath)
|
||||
}
|
||||
for (index, indexPath) in indexPaths.sorted().enumerated() {
|
||||
let removeIndex = indexPath.row - index
|
||||
moleculesInfo?.remove(at: removeIndex)
|
||||
guard let tableView else { return }
|
||||
|
||||
let removingRefs = molecules.asMoleculeInfoRef()
|
||||
|
||||
var snapshot = dataSource.snapshot()
|
||||
|
||||
let replacementRefs = molecules.asMoleculeInfoRef()
|
||||
replacementRefs.registerTypes(with: tableView)
|
||||
|
||||
if let targetMolecule, let targetRef = MoleculeInfo(listItemModel: targetMolecule) {
|
||||
snapshot.insertItems(replacementRefs, afterItem: targetRef)
|
||||
} else if let targetRef = removingRefs.first {
|
||||
snapshot.insertItems(replacementRefs, beforeItem: targetRef)
|
||||
}
|
||||
|
||||
var exitAnimation = UITableView.RowAnimation.automatic
|
||||
switch animation {
|
||||
case .left: exitAnimation = .right
|
||||
case .right: exitAnimation = .left
|
||||
case .top: exitAnimation = .bottom
|
||||
case .bottom: exitAnimation = .top
|
||||
default: break
|
||||
snapshot.deleteItems(removingRefs)
|
||||
|
||||
dataSource.apply(snapshot) {
|
||||
self.updateViewConstraints()
|
||||
self.view.setNeedsLayout()
|
||||
}
|
||||
|
||||
if indexPaths.count > 0 {
|
||||
tableView?.deleteRows(at: indexPaths, with: exitAnimation)
|
||||
}
|
||||
|
||||
indexPaths = []
|
||||
for molecule in replacements {
|
||||
if let info = self.createMoleculeInfo(with: molecule) {
|
||||
self.tableView?.register(info.class, forCellReuseIdentifier: info.identifier)
|
||||
let index = indexPath.row + indexPaths.count
|
||||
self.moleculesInfo?.insert(info, at: index)
|
||||
indexPaths.append(IndexPath(row: index, section: 0))
|
||||
}
|
||||
}
|
||||
|
||||
if let animation = animation,
|
||||
indexPaths.count > 0 {
|
||||
self.tableView?.insertRows(at: indexPaths, with: animation)
|
||||
}
|
||||
|
||||
tableView.endUpdates()
|
||||
|
||||
self.updateViewConstraints()
|
||||
self.view.setNeedsLayout()
|
||||
}
|
||||
|
||||
public func getIndexPath(for molecule: ListItemModelProtocol & MoleculeModelProtocol) -> IndexPath? {
|
||||
guard let index = moleculesInfo?.firstIndex(where: { (moleculeInfo) -> Bool in
|
||||
return equal(moleculeA: molecule, moleculeB: moleculeInfo.molecule)
|
||||
}) else { return nil }
|
||||
public func getIndexPath(for molecule: any ListItemModelProtocol & MoleculeModelProtocol) -> IndexPath? {
|
||||
let snapshot = dataSource.snapshot()
|
||||
guard let listItemRef = MoleculeInfo(listItemModel: molecule),
|
||||
let index = snapshot.indexOfItem(listItemRef)
|
||||
else { return nil }
|
||||
|
||||
return IndexPath(row: index, section: 0)
|
||||
}
|
||||
|
||||
@ -37,7 +37,7 @@ open class SectionListTemplate: MoleculeListTemplate {
|
||||
sectionMoleculesInfo = sectionList.count > 0 ? sectionList : nil
|
||||
}
|
||||
|
||||
override func getMoleculeInfo(for indexPath: IndexPath) -> MoleculeInfo? {
|
||||
func getMoleculeInfo(for indexPath: IndexPath) -> MoleculeInfo? {
|
||||
sectionMoleculesInfo?[indexPath.section].rows[indexPath.row]
|
||||
}
|
||||
|
||||
@ -137,4 +137,18 @@ open class SectionListTemplate: MoleculeListTemplate {
|
||||
open override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
sectionMoleculesInfo?[section].rows.count ?? 0
|
||||
}
|
||||
|
||||
func createMoleculeInfo(with listItem: MoleculeModelProtocol?) -> MoleculeInfo? {
|
||||
|
||||
guard let listItem = listItem,
|
||||
let moleculeClass = ModelRegistry.getMoleculeClass(listItem)
|
||||
else { return nil }
|
||||
|
||||
let moleculeName = moleculeClass.nameForReuse(with: listItem, delegateObject() as? MVMCoreUIDelegateObject) ?? listItem.moleculeName
|
||||
|
||||
return (moleculeName, moleculeClass, listItem)
|
||||
}
|
||||
|
||||
public typealias MoleculeInfo = (identifier: String, class: AnyClass, molecule: MoleculeModelProtocol)
|
||||
|
||||
}
|
||||
|
||||
@ -62,6 +62,8 @@ import Combine
|
||||
// MARK: - Response handling
|
||||
//--------------------------------------------------
|
||||
|
||||
typealias PageUpdateBatch = (pageUpdates: [String : Any]?, pageModel: PageModelProtocol?, moduleUpdates: [String : Any]?)
|
||||
|
||||
open func observeForResponseJSONUpdates() {
|
||||
guard observingForResponses == nil,
|
||||
(pagesToListenFor()?.count ?? 0 > 0 || modulesToListenFor()?.count ?? 0 > 0)
|
||||
@ -69,24 +71,33 @@ import Combine
|
||||
|
||||
observingForResponses = NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: NotificationResponseLoaded))
|
||||
.receive(on: self.pageUpdateQueue) // Background serial queue.
|
||||
// Receive new data for this page and filter out any that do not apply.
|
||||
.compactMap { [weak self] notification in
|
||||
self?.pullUpdates(from: notification) ?? nil
|
||||
}
|
||||
// Merge all page and module updates into one update event.
|
||||
.scan((nil, nil, nil)) { accumulator, next in
|
||||
.scan(((nil,nil,nil), 0)) { (accumulator: (PageUpdateBatch, Int), next: PageUpdateBatch ) in
|
||||
let (accumulatedUpdates, batchCount) = accumulator
|
||||
// Always take the latest page and the latest modules with same key.
|
||||
return (next.0 ?? accumulator.0, next.1 ?? accumulator.1, next.2?.mergingRight(accumulator.2 ?? [:]))
|
||||
let updatedBatch: PageUpdateBatch = (
|
||||
next.pageUpdates ?? accumulatedUpdates.pageUpdates,
|
||||
next.pageModel ?? accumulatedUpdates.pageModel,
|
||||
next.moduleUpdates?.mergingRight(accumulatedUpdates.moduleUpdates ?? [:])
|
||||
)
|
||||
let updates = (updatedBatch, batchCount + 1)
|
||||
return updates
|
||||
}
|
||||
// Delay allowing the previous model update to settle before triggering a re-render.
|
||||
.throttle(for: .seconds(0.25), scheduler: RunLoop.main, latest: true)
|
||||
.sink { [weak self] (pageUpdates: [String : Any]?, pageModel: PageModelProtocol?, moduleUpdates: [String : Any]?) in
|
||||
.throttle(for: .seconds(0.5), scheduler: RunLoop.main, latest: true)
|
||||
.sink { [weak self] (pendingUpdates: PageUpdateBatch, batchCount: Int) in
|
||||
guard let self = self else { return }
|
||||
let (pageUpdates, pageModel, moduleUpdates) = pendingUpdates
|
||||
if let pageUpdates, pageModel != nil {
|
||||
self.loadObject?.pageJSON = pageUpdates
|
||||
}
|
||||
let mergedModuleUpdates = (loadObject?.modulesJSON ?? [:]).mergingLeft(moduleUpdates ?? [:])
|
||||
self.loadObject?.modulesJSON = mergedModuleUpdates
|
||||
self.debugLog("Applying async update page model \(pageModel.debugDescription) and modules \(mergedModuleUpdates.keys) to page.")
|
||||
self.debugLog("Applying async update page model \(pageModel.debugDescription) and modules \(mergedModuleUpdates.keys) to page. (Batches: \(batchCount))")
|
||||
self.handleNewData(pageModel)
|
||||
}
|
||||
}
|
||||
@ -97,7 +108,7 @@ import Combine
|
||||
self.observingForResponses = nil
|
||||
}
|
||||
|
||||
func pullUpdates(from notification: Notification) -> (pageUpdates: [String : Any]?, pageModel: PageModelProtocol?, moduleUpdates: [String : Any]?)? {
|
||||
func pullUpdates(from notification: Notification) -> PageUpdateBatch? {
|
||||
// Get the page data.
|
||||
let pageUpdates = extractInterestedPageType(from: notification.userInfo?.optionalDictionaryForKey(KeyPageMap) ?? [:])
|
||||
// Convert the page data into a new model.
|
||||
|
||||
@ -79,9 +79,8 @@ public class AddRemoveMoleculesBehavior: PageCustomActionHandlerBehavior, PageMo
|
||||
public func willRender(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) {
|
||||
guard let list = delegate?.moleculeListDelegate else { return }
|
||||
for case let model as (MoleculeModelProtocol & ListItemModelProtocol & AddMolecules) in rootMolecules {
|
||||
if let moleculesToAdd = model.getRecursiveMoleculesToAdd(),
|
||||
let indexPath = list.getAdjustedIndexPath(for: model, position: moleculesToAdd.1) {
|
||||
list.addMolecules(moleculesToAdd.0, indexPath: indexPath, animation: nil)
|
||||
if let moleculesToAdd = model.getRecursiveMoleculesToAdd() {
|
||||
list.addMolecules(moleculesToAdd.0, after: model, animation: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -98,24 +97,26 @@ public class AddRemoveMoleculesBehavior: PageCustomActionHandlerBehavior, PageMo
|
||||
let moleculesToAdd = sourceModel.getRecursiveMoleculesToAdd(),
|
||||
let indexPath = list.getAdjustedIndexPath(for: sourceModel, position: moleculesToAdd.1) else { break }
|
||||
await MainActor.run {
|
||||
list.addMolecules(moleculesToAdd.0, indexPath: indexPath, animation: model.animation)
|
||||
// TODO: Pipe the position.
|
||||
list.addMolecules(moleculesToAdd.0, after: sourceModel, animation: model.animation)
|
||||
}
|
||||
case let model as RemoveMoleculesActionModel:
|
||||
guard let list = delegate?.moleculeListDelegate,
|
||||
let sourceModel = MVMCoreUIActionHandler.getSourceModel(from: additionalData) as? (ListItemModelProtocol & MoleculeModelProtocol & RemoveMolecules),
|
||||
let moleculesToRemove = sourceModel.getRecursiveMoleculesToRemove() else { break }
|
||||
await MainActor.run {
|
||||
list.removeMolecules(moleculesToRemove, animation: model.animation)
|
||||
list.removeMolecules(moleculesToRemove, animation: model.animation)
|
||||
}
|
||||
case let model as SwapMoleculesActionModel:
|
||||
guard let list = delegate?.moleculeListDelegate,
|
||||
let sourceModel = MVMCoreUIActionHandler.getSourceModel(from: additionalData) as? (ListItemModelProtocol & MoleculeModelProtocol & RemoveMolecules & AddMolecules),
|
||||
let moleculesToRemove = sourceModel.getRecursiveMoleculesToRemove(),
|
||||
let moleculesToAdd = sourceModel.getRecursiveMoleculesToAdd(),
|
||||
let indexPath = list.getAdjustedIndexPath(for: sourceModel, position: moleculesToAdd.1)
|
||||
let moleculesToAdd = sourceModel.getRecursiveMoleculesToAdd()
|
||||
//let indexPath = list.getAdjustedIndexPath(for: sourceModel, position: moleculesToAdd.1)
|
||||
else { break }
|
||||
await MainActor.run {
|
||||
list.swapMolecules(moleculesToRemove, with: moleculesToAdd.0, at: indexPath, animation: model.animation)
|
||||
// TODO: Take into account position.
|
||||
list.swapMolecules(moleculesToRemove, with: moleculesToAdd.0, after: sourceModel, animation: model.animation)
|
||||
}
|
||||
default:
|
||||
break
|
||||
|
||||
Loading…
Reference in New Issue
Block a user