Digital PCT265 defect MVAPCT-272: Intial compiling cut of ListTemplate moving to UITableviewDiffableDataSource. Warning -- Still very runtime unstable.
This commit is contained in:
parent
d7756b66b0
commit
bbadce4dc7
@ -14,25 +14,20 @@ public protocol MoleculeListProtocol {
|
|||||||
/// Asks the delegate to add molecules.
|
/// Asks the delegate to add molecules.
|
||||||
func addMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], indexPath: IndexPath, animation: UITableView.RowAnimation?)
|
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.
|
/// Asks the delegate to swap batches of molecules.
|
||||||
func swapMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], with replacements: [ListItemModelProtocol & MoleculeModelProtocol], at indexPath: IndexPath, animation: UITableView.RowAnimation?)
|
func swapMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], with replacements: [ListItemModelProtocol & MoleculeModelProtocol], at indexPath: IndexPath, animation: UITableView.RowAnimation?)
|
||||||
}
|
|
||||||
|
|
||||||
extension MoleculeListProtocol {
|
/// Asks the delegate to swap batches of molecules.
|
||||||
/// Convenience function that removes the passed molecule
|
func swapMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], with replacements: [ListItemModelProtocol & MoleculeModelProtocol], after molecule: (ListItemModelProtocol & MoleculeModelProtocol)?, animation: UITableView.RowAnimation?)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension MVMCoreUIDelegateObject {
|
public extension MVMCoreUIDelegateObject {
|
||||||
|
|||||||
@ -8,16 +8,69 @@
|
|||||||
|
|
||||||
import UIKit
|
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 {
|
open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol {
|
||||||
|
|
||||||
|
public typealias ListDataSnapshot = NSDiffableDataSourceSnapshot<Int, MoleculeInfo>
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Stored Properties
|
// MARK: - Stored Properties
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
|
|
||||||
public var moleculesInfo: [MoleculeInfo]?
|
|
||||||
|
|
||||||
var observer: NSKeyValueObservation?
|
var observer: NSKeyValueObservation?
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
@ -46,6 +99,29 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
|
|||||||
// MARK: - Methods
|
// 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 {
|
open override func parsePageJSON(loadObject: MVMCoreLoadObject) throws -> PageModelProtocol {
|
||||||
return try parseTemplate(loadObject: loadObject)
|
return try parseTemplate(loadObject: loadObject)
|
||||||
}
|
}
|
||||||
@ -103,12 +179,6 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
|
|||||||
guard let molecules else { return }
|
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.
|
// 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)
|
newData(for: molecules)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,45 +210,45 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
|
|||||||
|
|
||||||
open override func registerWithTable() {
|
open override func registerWithTable() {
|
||||||
super.registerWithTable()
|
super.registerWithTable()
|
||||||
guard let moleculesInfo = moleculesInfo else { return }
|
|
||||||
|
|
||||||
for moleculeInfo in moleculesInfo {
|
for moleculeInfo in dataSource.snapshot().itemIdentifiers {
|
||||||
tableView?.register(moleculeInfo.class, forCellReuseIdentifier: moleculeInfo.identifier)
|
tableView?.register(moleculeInfo.cellType, forCellReuseIdentifier: moleculeInfo.cellReuseId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
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 {
|
open func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||||
guard let moleculeInfo = getMoleculeInfo(for: indexPath),
|
guard let moleculeInfo = dataSource.itemIdentifier(for: indexPath),
|
||||||
let estimatedHeight = (moleculeInfo.class as? MoleculeViewProtocol.Type)?.estimatedHeight(with: moleculeInfo.molecule, delegateObject() as? MVMCoreUIDelegateObject)
|
let estimatedHeight = moleculeInfo.cellType.estimatedHeight(with: moleculeInfo.model, delegateObject() as? MVMCoreUIDelegateObject)
|
||||||
else { return 0 }
|
else { return 0 }
|
||||||
|
|
||||||
return estimatedHeight
|
return estimatedHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
open override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
// open override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
moleculesInfo?.count ?? 0
|
// debugLog("Number of rows: \(moleculesInfo?.count ?? 0)")
|
||||||
}
|
// return moleculesInfo?.count ?? 0
|
||||||
|
// }
|
||||||
|
|
||||||
open override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
// open override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
|
//
|
||||||
guard let moleculeInfo = getMoleculeInfo(for: indexPath),
|
// guard let moleculeInfo = getMoleculeInfo(for: indexPath),
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: moleculeInfo.identifier)
|
// let cell = tableView.dequeueReusableCell(withIdentifier: moleculeInfo.identifier)
|
||||||
else { return UITableViewCell() }
|
// else { return UITableViewCell() }
|
||||||
cell.isHidden = (getMoleculeInfo(for: indexPath)?.molecule as? GoneableProtocol)?.gone == true
|
// cell.isHidden = (getMoleculeInfo(for: indexPath)?.molecule as? GoneableProtocol)?.gone == true
|
||||||
(cell as? MoleculeViewProtocol)?.reset()
|
// (cell as? MoleculeViewProtocol)?.reset()
|
||||||
(cell as? MoleculeListCellProtocol)?.setLines(with: templateModel?.line, delegateObject: delegateObjectIVar, additionalData: nil, indexPath: indexPath)
|
// (cell as? MoleculeListCellProtocol)?.setLines(with: templateModel?.line, delegateObject: delegateObjectIVar, additionalData: nil, indexPath: indexPath)
|
||||||
if let moleculeView = cell as? MoleculeViewProtocol {
|
// if let moleculeView = cell as? MoleculeViewProtocol {
|
||||||
updateMoleculeView(moleculeView, from: moleculeInfo.molecule)
|
// updateMoleculeView(moleculeView, from: moleculeInfo.molecule)
|
||||||
}
|
// }
|
||||||
(cell as? MVMCoreViewProtocol)?.updateView(tableView.bounds.width)
|
// (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
|
// // Neded to fix an apple defect where the cell is not the correct size on certain devices for certain cells
|
||||||
cell.setNeedsLayout()
|
// cell.setNeedsLayout()
|
||||||
return cell
|
// return cell
|
||||||
}
|
// }
|
||||||
|
|
||||||
open func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
open func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
(cell as? MoleculeListCellProtocol)?.willDisplay()
|
(cell as? MoleculeListCellProtocol)?.willDisplay()
|
||||||
@ -235,7 +305,8 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
|
|||||||
// If the view is in a cell, refresh the table ui.
|
// If the view is in a cell, refresh the table ui.
|
||||||
let point = molecule.convert(molecule.bounds.origin, to: tableView)
|
let point = molecule.convert(molecule.bounds.origin, to: tableView)
|
||||||
if let indexPath = tableView.indexPathForRow(at: point), tableView.indexPathsForVisibleRows?.contains(indexPath) ?? false {
|
if let indexPath = tableView.indexPathForRow(at: point), tableView.indexPathsForVisibleRows?.contains(indexPath) ?? false {
|
||||||
refreshTable()
|
//TOOD: Find alternate.
|
||||||
|
//refreshTable()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,32 +325,25 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let moleculesInfo = moleculesInfo else { return }
|
var snapshot = dataSource.snapshot()
|
||||||
|
let updatedListItems: [MoleculeInfo] = snapshot.itemIdentifiers.compactMap { listItemRef -> MoleculeInfo? in
|
||||||
let indicies = moleculesInfo.indices.filter({ index -> Bool in
|
var listItemRef = listItemRef
|
||||||
return moleculesInfo[index].molecule.findFirstMolecule(by: { existingMolecule in
|
guard let matchedMolecule = listItemRef.contains(oneOf: molecules) else { return nil }
|
||||||
molecules.contains { newMolecule in
|
if let matchedMolecule = matchedMolecule as? (ListItemModelProtocol & MoleculeModelProtocol), listItemRef.doesMatch(matchedMolecule) {
|
||||||
existingMolecule.moleculeName == newMolecule.moleculeName && equal(moleculeA: existingMolecule, moleculeB: newMolecule)
|
listItemRef.model = matchedMolecule // Replace the top level molecule if it changed.
|
||||||
}
|
}
|
||||||
}) != nil
|
return listItemRef
|
||||||
})
|
|
||||||
|
|
||||||
// Refresh the cell. (reload loses cell selection)
|
|
||||||
let selectedIndex = tableView.indexPathForSelectedRow
|
|
||||||
let indexPaths = indicies.map {
|
|
||||||
return IndexPath(row: $0, section: 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
debugLog("Refreshing rows \(indexPaths.map { $0.row })")
|
|
||||||
|
|
||||||
if #available(iOS 15.0, *) {
|
if #available(iOS 15.0, *) {
|
||||||
// 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.
|
snapshot.reconfigureItems(updatedListItems)
|
||||||
tableView.reconfigureRows(at: indexPaths)
|
|
||||||
} else {
|
} else {
|
||||||
// A full reload can cause a flicker / animation. Better to avoid with above reconfigure method.
|
// A full reload can cause a flicker / animation. Better to avoid with above reconfigure method.
|
||||||
tableView.reloadRows(at: indexPaths, with: .automatic)
|
snapshot.reloadItems(updatedListItems)
|
||||||
}
|
}
|
||||||
if let selectedIndex = selectedIndex {
|
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)
|
tableView.selectRow(at: selectedIndex, animated: false, scrollPosition: .none)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -303,42 +367,38 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
|
|||||||
// MARK: - Convenience
|
// 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.
|
/// Returns the (identifier, class) of the molecule for the indexPath.
|
||||||
func getMoleculeInfo(for indexPath: IndexPath) -> MoleculeInfo? {
|
// func getMoleculeInfo(for indexPath: IndexPath) -> MoleculeInfo? {
|
||||||
moleculesInfo?[safe: indexPath.row]
|
// moleculesInfo?[safe: indexPath.row]
|
||||||
}
|
// }
|
||||||
|
|
||||||
/// Sets up the molecule list and ensures no errors loading all content.
|
/// Sets up the molecule list and ensures no errors loading all content.
|
||||||
func getMoleculeInfoList() -> [MoleculeInfo]? {
|
// func getMoleculeInfoList() -> [MoleculeInfo]? {
|
||||||
|
//
|
||||||
var moleculeList: [MoleculeInfo] = []
|
// var moleculeList: [MoleculeInfo] = []
|
||||||
|
//
|
||||||
if let molecules = templateModel?.molecules {
|
// if let molecules = templateModel?.molecules {
|
||||||
for molecule in molecules {
|
// for molecule in molecules {
|
||||||
if let info = createMoleculeInfo(with: molecule) {
|
// if let info = MoleculeInfo(with: molecule) {
|
||||||
moleculeList.append(info)
|
// moleculeList.append(info)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
return moleculeList.count > 0 ? moleculeList : nil
|
// return moleculeList.count > 0 ? moleculeList : nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
/// Sets up the header, footer, molecule list and ensures no errors loading all content.
|
/// Sets up the header, footer, molecule list and ensures no errors loading all content.
|
||||||
func setup() {
|
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.
|
/// Adds modules from requiredModules() to the MVMCoreViewControllerMapping.requiredModules map.
|
||||||
@ -371,90 +431,84 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension MoleculeListTemplate: MoleculeListProtocol {
|
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,
|
// public func removeMolecules(at indexPaths: [IndexPath], animation: UITableView.RowAnimation?) {
|
||||||
indexPaths.count > 0 else { return }
|
// for (index, indexPath) in indexPaths.sorted().enumerated() {
|
||||||
tableView?.deleteRows(at: indexPaths, with: animation)
|
// let removeIndex = indexPath.row - index
|
||||||
updateViewConstraints()
|
// moleculesInfo?.remove(at: removeIndex)
|
||||||
view.setNeedsLayout()
|
// }
|
||||||
|
//
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func addMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], indexPath: IndexPath, animation: UITableView.RowAnimation?) {
|
public func addMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], indexPath: IndexPath, animation: UITableView.RowAnimation?) {
|
||||||
var indexPaths: [IndexPath] = []
|
var snapshot = dataSource.snapshot()
|
||||||
|
guard let listItemRef = snapshot.itemIdentifiers[safe: indexPath.row] else { return }
|
||||||
for molecule in molecules {
|
snapshot.insertItems(molecules.asMoleculeInfoRef(), afterItem: listItemRef)
|
||||||
if let info = self.createMoleculeInfo(with: molecule) {
|
dataSource.apply(snapshot, animatingDifferences: true) {
|
||||||
self.tableView?.register(info.class, forCellReuseIdentifier: info.identifier)
|
self.updateViewConstraints()
|
||||||
let index = indexPath.row + indexPaths.count
|
self.view.setNeedsLayout()
|
||||||
self.moleculesInfo?.insert(info, at: index)
|
|
||||||
indexPaths.append(IndexPath(row: index, section: 0))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
guard let animation = animation,
|
public func addMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], after molecule: (ListItemModelProtocol & MoleculeModelProtocol), animation: UITableView.RowAnimation?) {
|
||||||
indexPaths.count > 0 else { return }
|
guard let listMoleculeRef = MoleculeInfo(listItemModel: molecule) else { return }
|
||||||
self.tableView?.insertRows(at: indexPaths, with: animation)
|
var snapshot = dataSource.snapshot()
|
||||||
self.updateViewConstraints()
|
snapshot.insertItems(molecules.asMoleculeInfoRef(), afterItem: listMoleculeRef)
|
||||||
self.view.setNeedsLayout()
|
dataSource.apply(snapshot, animatingDifferences: true) {
|
||||||
|
self.updateViewConstraints()
|
||||||
|
self.view.setNeedsLayout()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func swapMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], with replacements: [ListItemModelProtocol & MoleculeModelProtocol], at indexPath: IndexPath, animation: UITableView.RowAnimation?) {
|
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()
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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? {
|
public func swapMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], with replacements: [ListItemModelProtocol & MoleculeModelProtocol], after targetMolecule: (ListItemModelProtocol & MoleculeModelProtocol)?, animation: UITableView.RowAnimation?) {
|
||||||
guard let index = moleculesInfo?.firstIndex(where: { (moleculeInfo) -> Bool in
|
|
||||||
return equal(moleculeA: molecule, moleculeB: moleculeInfo.molecule)
|
guard let tableView else { return }
|
||||||
}) else { return nil }
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot.deleteItems(removingRefs)
|
||||||
|
|
||||||
|
dataSource.apply(snapshot) {
|
||||||
|
self.updateViewConstraints()
|
||||||
|
self.view.setNeedsLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
return IndexPath(row: index, section: 0)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,7 +37,7 @@ open class SectionListTemplate: MoleculeListTemplate {
|
|||||||
sectionMoleculesInfo = sectionList.count > 0 ? sectionList : nil
|
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]
|
sectionMoleculesInfo?[indexPath.section].rows[indexPath.row]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,4 +137,18 @@ open class SectionListTemplate: MoleculeListTemplate {
|
|||||||
open override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
open override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
sectionMoleculesInfo?[section].rows.count ?? 0
|
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
|
// MARK: - Response handling
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
|
|
||||||
|
typealias PageUpdateBatch = (pageUpdates: [String : Any]?, pageModel: PageModelProtocol?, moduleUpdates: [String : Any]?)
|
||||||
|
|
||||||
open func observeForResponseJSONUpdates() {
|
open func observeForResponseJSONUpdates() {
|
||||||
guard observingForResponses == nil,
|
guard observingForResponses == nil,
|
||||||
(pagesToListenFor()?.count ?? 0 > 0 || modulesToListenFor()?.count ?? 0 > 0)
|
(pagesToListenFor()?.count ?? 0 > 0 || modulesToListenFor()?.count ?? 0 > 0)
|
||||||
@ -69,24 +71,33 @@ import Combine
|
|||||||
|
|
||||||
observingForResponses = NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: NotificationResponseLoaded))
|
observingForResponses = NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: NotificationResponseLoaded))
|
||||||
.receive(on: self.pageUpdateQueue) // Background serial queue.
|
.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
|
.compactMap { [weak self] notification in
|
||||||
self?.pullUpdates(from: notification) ?? nil
|
self?.pullUpdates(from: notification) ?? nil
|
||||||
}
|
}
|
||||||
// Merge all page and module updates into one update event.
|
// 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.
|
// 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.
|
// Delay allowing the previous model update to settle before triggering a re-render.
|
||||||
.throttle(for: .seconds(0.25), scheduler: RunLoop.main, latest: true)
|
.throttle(for: .seconds(0.5), scheduler: RunLoop.main, latest: true)
|
||||||
.sink { [weak self] (pageUpdates: [String : Any]?, pageModel: PageModelProtocol?, moduleUpdates: [String : Any]?) in
|
.sink { [weak self] (pendingUpdates: PageUpdateBatch, batchCount: Int) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
let (pageUpdates, pageModel, moduleUpdates) = pendingUpdates
|
||||||
if let pageUpdates, pageModel != nil {
|
if let pageUpdates, pageModel != nil {
|
||||||
self.loadObject?.pageJSON = pageUpdates
|
self.loadObject?.pageJSON = pageUpdates
|
||||||
}
|
}
|
||||||
let mergedModuleUpdates = (loadObject?.modulesJSON ?? [:]).mergingLeft(moduleUpdates ?? [:])
|
let mergedModuleUpdates = (loadObject?.modulesJSON ?? [:]).mergingLeft(moduleUpdates ?? [:])
|
||||||
self.loadObject?.modulesJSON = mergedModuleUpdates
|
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)
|
self.handleNewData(pageModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -97,7 +108,7 @@ import Combine
|
|||||||
self.observingForResponses = nil
|
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.
|
// Get the page data.
|
||||||
let pageUpdates = extractInterestedPageType(from: notification.userInfo?.optionalDictionaryForKey(KeyPageMap) ?? [:])
|
let pageUpdates = extractInterestedPageType(from: notification.userInfo?.optionalDictionaryForKey(KeyPageMap) ?? [:])
|
||||||
// Convert the page data into a new model.
|
// Convert the page data into a new model.
|
||||||
|
|||||||
@ -79,9 +79,8 @@ public class AddRemoveMoleculesBehavior: PageCustomActionHandlerBehavior, PageMo
|
|||||||
public func willRender(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) {
|
public func willRender(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) {
|
||||||
guard let list = delegate?.moleculeListDelegate else { return }
|
guard let list = delegate?.moleculeListDelegate else { return }
|
||||||
for case let model as (MoleculeModelProtocol & ListItemModelProtocol & AddMolecules) in rootMolecules {
|
for case let model as (MoleculeModelProtocol & ListItemModelProtocol & AddMolecules) in rootMolecules {
|
||||||
if let moleculesToAdd = model.getRecursiveMoleculesToAdd(),
|
if let moleculesToAdd = model.getRecursiveMoleculesToAdd() {
|
||||||
let indexPath = list.getAdjustedIndexPath(for: model, position: moleculesToAdd.1) {
|
list.addMolecules(moleculesToAdd.0, after: model, animation: nil)
|
||||||
list.addMolecules(moleculesToAdd.0, indexPath: indexPath, animation: nil)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -98,24 +97,26 @@ public class AddRemoveMoleculesBehavior: PageCustomActionHandlerBehavior, PageMo
|
|||||||
let moleculesToAdd = sourceModel.getRecursiveMoleculesToAdd(),
|
let moleculesToAdd = sourceModel.getRecursiveMoleculesToAdd(),
|
||||||
let indexPath = list.getAdjustedIndexPath(for: sourceModel, position: moleculesToAdd.1) else { break }
|
let indexPath = list.getAdjustedIndexPath(for: sourceModel, position: moleculesToAdd.1) else { break }
|
||||||
await MainActor.run {
|
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:
|
case let model as RemoveMoleculesActionModel:
|
||||||
guard let list = delegate?.moleculeListDelegate,
|
guard let list = delegate?.moleculeListDelegate,
|
||||||
let sourceModel = MVMCoreUIActionHandler.getSourceModel(from: additionalData) as? (ListItemModelProtocol & MoleculeModelProtocol & RemoveMolecules),
|
let sourceModel = MVMCoreUIActionHandler.getSourceModel(from: additionalData) as? (ListItemModelProtocol & MoleculeModelProtocol & RemoveMolecules),
|
||||||
let moleculesToRemove = sourceModel.getRecursiveMoleculesToRemove() else { break }
|
let moleculesToRemove = sourceModel.getRecursiveMoleculesToRemove() else { break }
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
list.removeMolecules(moleculesToRemove, animation: model.animation)
|
list.removeMolecules(moleculesToRemove, animation: model.animation)
|
||||||
}
|
}
|
||||||
case let model as SwapMoleculesActionModel:
|
case let model as SwapMoleculesActionModel:
|
||||||
guard let list = delegate?.moleculeListDelegate,
|
guard let list = delegate?.moleculeListDelegate,
|
||||||
let sourceModel = MVMCoreUIActionHandler.getSourceModel(from: additionalData) as? (ListItemModelProtocol & MoleculeModelProtocol & RemoveMolecules & AddMolecules),
|
let sourceModel = MVMCoreUIActionHandler.getSourceModel(from: additionalData) as? (ListItemModelProtocol & MoleculeModelProtocol & RemoveMolecules & AddMolecules),
|
||||||
let moleculesToRemove = sourceModel.getRecursiveMoleculesToRemove(),
|
let moleculesToRemove = sourceModel.getRecursiveMoleculesToRemove(),
|
||||||
let moleculesToAdd = sourceModel.getRecursiveMoleculesToAdd(),
|
let moleculesToAdd = sourceModel.getRecursiveMoleculesToAdd()
|
||||||
let indexPath = list.getAdjustedIndexPath(for: sourceModel, position: moleculesToAdd.1)
|
//let indexPath = list.getAdjustedIndexPath(for: sourceModel, position: moleculesToAdd.1)
|
||||||
else { break }
|
else { break }
|
||||||
await MainActor.run {
|
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:
|
default:
|
||||||
break
|
break
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user