move model replacement to the viewcontroller for model state synchronization & stability. shift replacement molecule handling to the behavior for targeted updates.

This commit is contained in:
Hedden, Kyle Matthew 2023-10-26 12:28:56 -04:00
parent b640863167
commit 50646851ba
13 changed files with 168 additions and 65 deletions

View File

@ -21,6 +21,8 @@ public protocol MoleculeDelegateProtocol: AnyObject {
/// Notifies the delegate that the molecule layout update. Should be called when the layout may change due to an async method. Mainly used for list or collections.
func moleculeLayoutUpdated(_ molecule: MoleculeViewProtocol) //optional
func replaceMoleculeData(_ moleculeModels: [MoleculeModelProtocol])
}
extension MoleculeDelegateProtocol {

View File

@ -50,4 +50,24 @@ public extension MoleculeTreeTraversalProtocol {
return accumulator
}
}
func filterMoleculeTree(options: TreeTraversalOptions = .parentFirst, by condition: (MoleculeModelProtocol)->Bool) -> [MoleculeModelProtocol] {
return reduceDepthFirstTraverse(options: options, depth: 0, initialResult: []) { (accumulator, molecule, depth) in
if condition(molecule) {
return accumulator + [molecule]
}
return accumulator
}
}
func findFirstMolecule(options: TreeTraversalOptions = .parentFirst, by condition: (MoleculeModelProtocol)->Bool) -> MoleculeModelProtocol? {
var foundMolecule: MoleculeModelProtocol?
depthFirstTraverse(options: options, depth: 0) { depth, molecule, isDone in
isDone = condition(molecule)
if isDone {
foundMolecule = molecule
}
}
return foundMolecule
}
}

View File

@ -86,10 +86,10 @@
super.handleNewData()
}
open override func updateUI() {
open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
topViewOutsideOfScrollArea = templateModel?.anchorHeader ?? false
bottomViewOutsideOfScrollArea = templateModel?.anchorFooter ?? false
super.updateUI()
super.updateUI(for: molecules)
}
//--------------------------------------------------

View File

@ -21,8 +21,8 @@ open class ModalSectionListTemplate: SectionListTemplate {
// MARK: - Lifecycle
//--------------------------------------------------
override open func updateUI() {
super.updateUI()
override open func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
super.updateUI(for: molecules)
_ = MVMCoreUICommonViewsUtility.addCloseButton(to: view, action: { [weak self] _ in
guard let self = self else { return }
let closeAction = (self.templateModel as? ModalSectionListTemplateModel)?.closeAction ??

View File

@ -87,10 +87,17 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
super.handleNewData() // Currently stuck as MoleculeListProtocol being called from AddRemoveMoleculesBehaviorModel.
}
open override func updateUI() {
open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
topViewOutsideOfScrollArea = templateModel?.anchorHeader ?? false
bottomViewOutsideOfScrollArea = templateModel?.anchorFooter ?? false
super.updateUI()
super.updateUI(for: molecules)
molecules?.forEach({ molecule in
if let index = moleculesInfo?.firstIndex(where: { $0.molecule.id == molecule.id }) {
moleculesInfo?[index].molecule = molecule
}
newData(for: molecule)
})
}
open override func viewDidAppear(_ animated: Bool) {
@ -196,30 +203,20 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
open func newData(for molecule: MoleculeModelProtocol) {
//TODO: expand for header, navigation, etc
guard let index = moleculesInfo?.firstIndex(where: { (moleculeInfo) -> Bool in
if equal(moleculeA: molecule, moleculeB: moleculeInfo.molecule) {
return true
} else if let parent = moleculeInfo.molecule as? ParentMoleculeModelProtocol {
// Get all molecules of the same type for faster check.
let molecules: [MoleculeModelProtocol] = parent.reduceDepthFirstTraverse(options: .childFirst, depth: 0, initialResult: []) { (accumulator, currentMolecule, depth) in
if currentMolecule.moleculeName == molecule.moleculeName {
return accumulator + [currentMolecule]
}
return accumulator
}
for moleculeB in molecules {
if equal(moleculeA: molecule, moleculeB: moleculeB) {
return true
}
}
}
return false
}) else { return }
guard let moleculesInfo = moleculesInfo else { return }
let indicies = moleculesInfo.indices.filter({ index -> Bool in
return moleculesInfo[index].molecule.findFirstMolecule(by: {
$0.moleculeName == molecule.moleculeName && equal(moleculeA: molecule, moleculeB: $0)
}) != nil
})
// Refresh the cell. (reload loses cell selection)
let selectedIndex = tableView.indexPathForSelectedRow
let indexPath = IndexPath(row: index, section: 0)
tableView.reloadRows(at: [indexPath], with: .automatic)
let indexPaths = indicies.map {
return IndexPath(row: $0, section: 0)
}
tableView.reloadRows(at: indexPaths, with: .automatic)
if let selectedIndex = selectedIndex {
tableView.selectRow(at: selectedIndex, animated: false, scrollPosition: .none)
}
@ -292,14 +289,7 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
/// Checks if the two molecules are equal
private func equal(moleculeA: MoleculeModelProtocol, moleculeB: MoleculeModelProtocol) -> Bool {
// TODO: move this to a better approach, maybe a UUID for each model.
// Do instance check
if let classMoleculeA = moleculeA as? NSObjectProtocol,
let classMoleculeB = moleculeB as? NSObjectProtocol {
return classMoleculeA === classMoleculeB
}
// Do json check
return moleculeA.toJSON() == moleculeB.toJSON()
return moleculeA.id == moleculeB.id
}
}

View File

@ -26,8 +26,8 @@
}
}
open override func updateUI() {
super.updateUI()
open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
super.updateUI(for: molecules)
heightConstraint?.isActive = true
}
}

View File

@ -19,10 +19,10 @@ import UIKit
try super.parsePageJSON()
}
open override func updateUI() {
open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
topViewOutsideOfScroll = templateModel?.anchorHeader ?? false
bottomViewOutsideOfScroll = templateModel?.anchorFooter ?? false
super.updateUI()
super.updateUI(for: molecules)
}
open override func viewForTop() -> UIView? {

View File

@ -63,8 +63,8 @@ open class ScrollingViewController: ViewController {
registerForKeyboardNotifications()
}
open override func updateUI() {
super.updateUI()
open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
super.updateUI(for: molecules)
// will change scrollView indicatorStyle automatically on the basis of backgroundColor
var greyScale: CGFloat = 0
if view.backgroundColor?.getWhite(&greyScale, alpha: nil) ?? false {

View File

@ -108,8 +108,8 @@ import Foundation
}
}
open override func updateUI() {
super.updateUI()
open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
super.updateUI(for: molecules)
topView?.removeFromSuperview()
bottomView?.removeFromSuperview()
topView = viewForTop()

View File

@ -50,8 +50,11 @@ open class ThreeLayerTableViewController: ProgrammaticTableViewController {
tableView.reloadData()
}
open override func updateUI() {
super.updateUI()
open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
super.updateUI(for: molecules)
guard molecules == nil else { return }
createViewForTableHeader()
createViewForTableFooter()
tableView?.reloadData()

View File

@ -49,8 +49,8 @@ open class ThreeLayerViewController: ProgrammaticScrollViewController {
}
}
open override func updateUI() {
super.updateUI()
open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
super.updateUI(for: molecules)
// Removes the views
topView?.removeFromSuperview()

View File

@ -44,6 +44,13 @@ import MVMCore
public var selectedField: UIView?
public var pageUpdateQueue: OperationQueue = {
let pageUpdateQueue = OperationQueue()
pageUpdateQueue.maxConcurrentOperationCount = 1
pageUpdateQueue.qualityOfService = .userInteractive
return pageUpdateQueue
}()
/// Checks if the screen width has changed
open func screenSizeChanged() -> Bool {
!MVMCoreGetterUtility.cgfequalwiththreshold(previousScreenSize.width, view.bounds.size.width, 0.1)
@ -58,10 +65,6 @@ import MVMCore
(pagesToListenFor()?.count ?? 0 > 0 || modulesToListenFor()?.count ?? 0 > 0)
else { return }
let pageUpdateQueue = OperationQueue()
pageUpdateQueue.maxConcurrentOperationCount = 1
pageUpdateQueue.qualityOfService = .userInteractive
observingForResponses = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil, queue: pageUpdateQueue, using: responseJSONUpdated(notification:))
}
@ -241,8 +244,9 @@ import MVMCore
}
/// Applies the latest model to the UI.
@MainActor
open func updateUI() {
open func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
guard molecules == nil else { return }
executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in
behavior.willRender(rootMolecules: getRootMolecules(), delegateObjectIVar)
}
@ -508,6 +512,40 @@ import MVMCore
// Needed otherwise when subclassed, the extension gets called.
open func moleculeLayoutUpdated(_ molecule: MoleculeViewProtocol) { }
public func replaceMoleculeData(_ moleculeModels: [MoleculeModelProtocol]) {
pageUpdateQueue.addOperation {
let replacedModels:[MoleculeModelProtocol] = moleculeModels.compactMap { model in
guard self.attemptToReplace(with: model) else {
return nil
}
return model
}
if replacedModels.count > 0 {
Task { @MainActor in
self.updateUI(for: replacedModels)
}
}
}
}
open func attemptToReplace(with replacementModel: MoleculeModelProtocol) -> Bool {
guard var templateModel = getTemplateModel() else { return false }
var didReplace = false
do {
didReplace = try templateModel.replaceMolecule(with: replacementModel)
if !didReplace {
MVMCoreLoggingHandler.addError(toLog: MVMCoreErrorObject(title: nil, messageToLog: "Failed to find '\(replacementModel.id)' in the current screen.", code: ErrorCode.viewControllerProcessingJSON.rawValue, domain: ErrorDomainSystem, location: String(describing: type(of: self)))!)
}
} catch {
let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: String(describing: type(of: self)))!
if let error = error as? HumanReadableDecodingErrorProtocol {
coreError.messageToLog = "Error replacing molecule \"\(replacementModel.id)\": \(error.readableDescription)"
}
MVMCoreLoggingHandler.addError(toLog: coreError)
}
return didReplace
}
//--------------------------------------------------
// MARK: - MVMCoreUIDetailViewProtocol
//--------------------------------------------------

View File

@ -17,39 +17,89 @@ public class ReplaceableMoleculeBehaviorModel: PageBehaviorModelProtocol {
public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior {
var moleculeIds: [String]
var modulesToListenFor: [String]
private var observingForResponses: NSObjectProtocol?
private var delegateObject: MVMCoreUIDelegateObject?
public var transcendsPageUpdates: Bool { true }
public required init(model: PageBehaviorModelProtocol, delegateObject: MVMCoreUIDelegateObject?) {
moleculeIds = (model as! ReplaceableMoleculeBehaviorModel).moleculeIds
let shouldListenForListUpdates = delegateObject?.moleculeListDelegate != nil
if shouldListenForListUpdates {
modulesToListenFor = []
listenForModuleUpdates()
} else {
modulesToListenFor = moleculeIds
stopListeningForModuleUpdates()
}
self.delegateObject = delegateObject
guard let pageType = delegateObject?.moleculeDelegate?.getTemplateModel()?.pageType else { return }
MVMCoreViewControllerMappingObject.shared()?.addOptionalModules(toMapping: moleculeIds, forPageType: pageType)
}
public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) {
self.delegateObject = delegateObject
let shouldListenForListUpdates = delegateObject?.moleculeListDelegate != nil
if shouldListenForListUpdates {
modulesToListenFor = []
listenForModuleUpdates()
} else {
modulesToListenFor = moleculeIds
stopListeningForModuleUpdates()
}
guard var templateModel = delegateObject?.moleculeDelegate?.getTemplateModel() else { return }
templateModel.printMolecules()
for moleculeId in moleculeIds {
let moleculeModels = moleculeIds.compactMap { moleculeId in
do {
guard let replacementModel = try delegateObject?.moleculeDelegate?.getModuleWithName(moleculeId) else { continue }
let didReplace = try templateModel.replaceMolecule(with: replacementModel)
if !didReplace {
MVMCoreLoggingHandler.addError(toLog: MVMCoreErrorObject(title: nil, messageToLog: "Failed to find '\(moleculeId)' in the current screen.", code: ErrorCode.viewControllerProcessingJSON.rawValue, domain: ErrorDomainSystem, location: String(describing: type(of: self)))!)
}
return try delegateObject?.moleculeDelegate?.getModuleWithName(moleculeId)
} catch {
let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: String(describing: type(of: self)))!
if let error = error as? HumanReadableDecodingErrorProtocol {
coreError.messageToLog = "Error decoding replacement \"\(moleculeId)\": \(error.readableDescription)"
}
MVMCoreLoggingHandler.addError(toLog: coreError)
return nil
}
}
delegateObject?.moleculeDelegate?.replaceMoleculeData(moleculeModels)
}
public func modulesToListenFor() -> [String] {
moleculeIds
private func listenForModuleUpdates() {
guard observingForResponses == nil else { return }
let pageUpdateQueue = OperationQueue()
pageUpdateQueue.maxConcurrentOperationCount = 1
pageUpdateQueue.qualityOfService = .userInteractive
observingForResponses = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil, queue: pageUpdateQueue, using: responseJSONUpdated(notification:))
}
private func stopListeningForModuleUpdates() {
guard let observingForResponses = observingForResponses else { return }
NotificationCenter.default.removeObserver(observingForResponses)
}
@objc func responseJSONUpdated(notification: Notification) {
guard let modulesLoaded = notification.userInfo?.optionalDictionaryForKey(KeyModuleMap) else { return }
let modules: [MoleculeModelProtocol] = moleculeIds.compactMap { moleculeId in
guard let json = modulesLoaded.optionalDictionaryForKey(moleculeId) else { return nil }
do {
return try convertToModel(moduleJSON: json)
} catch {
let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: String(describing: type(of: self)))!
if let error = error as? HumanReadableDecodingErrorProtocol {
coreError.messageToLog = "Error decoding replacement \"\(moleculeId)\": \(error.readableDescription)"
}
MVMCoreLoggingHandler.addError(toLog: coreError)
return nil
}
}
delegateObject?.moleculeDelegate?.replaceMoleculeData(modules)
}
private func convertToModel(moduleJSON: [String: Any]) throws -> MoleculeModelProtocol {
guard let moleculeName = moduleJSON.optionalStringForKey(KeyMoleculeName),
let modelType = ModelRegistry.getType(for: moleculeName, with: MoleculeModelProtocol.self) else {
throw ModelRegistry.Error.decoderErrorModelNotMapped(identifer: moduleJSON.optionalStringForKey(KeyMoleculeName))
}
return try modelType.decode(jsonDict: moduleJSON as [String : Any]) as! MoleculeModelProtocol
}
}