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:
parent
b640863167
commit
50646851ba
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
|
||||
@ -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 ??
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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? {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
//--------------------------------------------------
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user