Digital PCT265 defect CXTDT-579050: Rewire asynchronous response JSON parsing to filter, queue, and delay drop UI changes.

This commit is contained in:
Hedden, Kyle Matthew 2024-07-02 19:05:53 -04:00
parent 12bbe5d9a2
commit d8d4b37d1d

View File

@ -8,6 +8,7 @@
import UIKit
import MVMCore
import Combine
@objc open class ViewController: UIViewController, MVMCoreViewControllerProtocol, MVMCoreViewManagerViewControllerProtocol, MoleculeDelegateProtocol, FormHolderProtocol, MVMCoreActionDelegateProtocol, ActionDelegateProtocol, MVMCoreLoadDelegateProtocol, UITextFieldDelegate, UITextViewDelegate, ObservingTextFieldDelegate, MVMCoreUIDetailViewProtocol, PageProtocol, PageBehaviorHandlerProtocol {
@ -38,7 +39,7 @@ import MVMCore
public var behaviors: [PageBehaviorProtocol]?
public var needsUpdateUI = false
private var observingForResponses: NSObjectProtocol?
private var observingForResponses: AnyCancellable?
private var initialLoadFinished = false
public var isFirstRender = true
public var previousScreenSize = CGSize.zero
@ -66,9 +67,46 @@ import MVMCore
(pagesToListenFor()?.count ?? 0 > 0 || modulesToListenFor()?.count ?? 0 > 0)
else { return }
observingForResponses = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil, queue: pageUpdateQueue) { [weak self] notification in
self?.responseJSONUpdated(notification: notification)
}
observingForResponses = NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: NotificationResponseLoaded))
.receive(on: self.pageUpdateQueue) // Background serial queue.
.map { [weak self] notification in
guard let self = self else { return (nil, nil, nil) }
// Get the page data.
let pageUpdates = self.extractInterestedPageType(from: notification.userInfo?.optionalDictionaryForKey(KeyPageMap) ?? [:])
// Convert the page data into a new model.
var pageModel: PageModelProtocol? = nil
if let pageUpdates {
do {
// TODO: Rewiring to parse from plain JSON rather than this protocol indirection.
pageModel = try (self as? any TemplateProtocol & PageBehaviorHandlerProtocol & MVMCoreViewControllerProtocol)?.parseTemplate(pageJSON: pageUpdates)
} catch {
if let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: "updateJSON for pageType: \(String(describing: self.pageType))") {
MVMCoreLoggingHandler.shared()?.addError(toLog: coreError)
}
}
}
// Get the module data.
let moduleUpdates = self.extractInterestedModules(from: notification.userInfo?.optionalDictionaryForKey(KeyModuleMap) ?? [:])
// Bundle the transformations.
return (pageUpdates, pageModel, moduleUpdates)
}
.filter { (pageUpdates: [String : Any]?, pageModel: PageModelProtocol?, moduleUpdates: [String : Any]?) in
// Skip any non-updates.
(pageUpdates != nil && pageModel != nil) || (moduleUpdates != nil && moduleUpdates!.count > 0)
}
// Opportunity: Merge all module only updates into one event.
// Delay allowing the previous model update to settle before triggering a re-render.
.buffer(size: 100, prefetch: .byRequest, whenFull: .dropOldest)
.flatMap(maxPublishers: .max(1)) { Just($0).delay(for: .seconds(0.1), scheduler: RunLoop.main) }
.sink { [weak self] (pageUpdates: [String : Any]?, pageModel: PageModelProtocol?, moduleUpdates: [String : Any]?) in
guard let self = self else { return }
if let pageUpdates, let pageModel {
self.loadObject?.pageJSON = pageUpdates
}
var mergedModuleUpdates = (loadObject?.modulesJSON ?? [:]).mergingLeft(moduleUpdates ?? [:])
self.loadObject?.modulesJSON = mergedModuleUpdates
self.handleNewData(pageModel)
}
}
open func stopObservingForResponseJSONUpdates() {
@ -88,51 +126,22 @@ import MVMCore
return requestModules + behaviorModules
}
@objc open func responseJSONUpdated(notification: Notification) {
// Checks for a page we are listening for.
var hasDataUpdate = false
var pageModel: PageModelProtocol? = nil
if let pagesLoaded = notification.userInfo?.optionalDictionaryForKey(KeyPageMap),
let loadObject,
let pageType = pagesToListenFor()?.first(where: { (pageTypeListened) -> Bool in
guard let page = pagesLoaded.optionalDictionaryForKey(pageTypeListened),
private func extractInterestedPageType(from pageMap: [String: Any]) -> [String: Any]? {
guard let pageType = pagesToListenFor()?.first(where: { pageTypeListened -> Bool in
guard let page = pageMap.optionalDictionaryForKey(pageTypeListened),
let pageType = page.optionalStringForKey(KeyPageType),
pageType == pageTypeListened
else { return false }
return true
}) {
hasDataUpdate = true
loadObject.pageJSON = pagesLoaded.optionalDictionaryForKey(pageType)
// Separate page updates from the module updates to avoid unecessary resets to behaviors and full re-renders.
do {
pageModel = try parsePageJSON(loadObject: loadObject)
} catch {
if let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: "updateJSON for pageType: \(String(describing: pageType))") {
MVMCoreLoggingHandler.shared()?.addError(toLog: coreError)
}
}
}) else { return nil }
return pageMap.optionalDictionaryForKey(pageType)
}
private func extractInterestedModules(from moduleMap: [String: Any]) -> [String: Any]? {
guard let modulesListened = modulesToListenFor() else { return nil }
return moduleMap.filter { (key: String, value: Any) in
modulesListened.contains { $0 == key }
}
// Checks for modules we are listening for.
if let modulesLoaded = notification.userInfo?.optionalDictionaryForKey(KeyModuleMap),
let modulesListened = modulesToListenFor() {
for moduleName in modulesListened {
if let module = modulesLoaded.optionalDictionaryForKey(moduleName) {
hasDataUpdate = true
var currentModules = loadObject?.modulesJSON ?? [:]
currentModules.updateValue(module, forKey: moduleName)
loadObject?.modulesJSON = currentModules
}
}
}
guard hasDataUpdate else { return }
MVMCoreDispatchUtility.performBlock(onMainThread: {
self.handleNewData(pageModel)
})
}
open func shouldFinishProcessingLoad(_ loadObject: MVMCoreLoadObject, error: AutoreleasingUnsafeMutablePointer<MVMCoreErrorObject?>) -> Bool {