Digital PCT265 defect MVAPCT-272: Intial compiling cut of ListTemplate moving to UITableviewDiffableDataSource. Warning -- Still very runtime unstable.

This commit is contained in:
Hedden, Kyle Matthew 2024-09-12 19:57:57 -04:00
parent d2e841c533
commit 2c71269518
5 changed files with 265 additions and 187 deletions

View File

@ -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 {

View File

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

View File

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

View File

@ -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.

View File

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