Digital PCT265 story PCT-135: Code review comments, cleanups and isEquals expansion.

This commit is contained in:
Hedden, Kyle Matthew 2024-05-13 21:23:00 -04:00
parent f37e7abcb1
commit 32deda3d3d
19 changed files with 149 additions and 64 deletions

View File

@ -25,4 +25,11 @@ public struct ActionAlertModel: ActionModelProtocol {
public init(alert: AlertModel) { public init(alert: AlertModel) {
self.alert = alert self.alert = alert
} }
public func isEqual(to model: any ModelComparisonProtocol) -> Bool {
guard let model = model as? Self else { return false }
return model.extraParameters == extraParameters
&& model.analyticsData == analyticsData
&& model.alert == alert
}
} }

View File

@ -20,4 +20,12 @@ public struct ActionCollapseNotificationModel: ActionModelProtocol {
self.extraParameters = extraParameters self.extraParameters = extraParameters
self.analyticsData = analyticsData self.analyticsData = analyticsData
} }
// Default
//public func isEqual(to model: any ModelComparisonProtocol) -> Bool {
// guard let model = model as? Self else { return false }
// return model.actionType == actionType
// && model.extraParameters == extraParameters
// && model.analyticsData == analyticsData
//}
} }

View File

@ -20,4 +20,12 @@ public struct ActionDismissNotificationModel: ActionModelProtocol {
self.extraParameters = extraParameters self.extraParameters = extraParameters
self.analyticsData = analyticsData self.analyticsData = analyticsData
} }
// Default
// public func isEqual(to model: any ModelComparisonProtocol) -> Bool {
// guard let model = model as? Self else { return false }
// return model.actionType == actionType
// && model.extraParameters == extraParameters
// && model.analyticsData == analyticsData
//}
} }

View File

@ -29,4 +29,12 @@ public struct ActionOpenPanelModel: ActionModelProtocol {
self.extraParameters = extraParameters self.extraParameters = extraParameters
self.analyticsData = analyticsData self.analyticsData = analyticsData
} }
// Default
public func isEqual(to model: any ModelComparisonProtocol) -> Bool {
guard let model = model as? Self else { return false }
return model.extraParameters == extraParameters
&& model.analyticsData == analyticsData
&& model.panel == panel
}
} }

View File

@ -22,4 +22,11 @@ public struct ActionTopNotificationModel: ActionModelProtocol {
self.extraParameters = extraParameters self.extraParameters = extraParameters
self.analyticsData = analyticsData self.analyticsData = analyticsData
} }
public func isEqual(to model: any ModelComparisonProtocol) -> Bool {
guard let model = model as? Self else { return false }
return model.extraParameters == extraParameters
&& model.analyticsData == analyticsData
&& model.topNotification == topNotification
}
} }

View File

@ -9,7 +9,8 @@
import UIKit import UIKit
import MVMCore import MVMCore
public struct AlertButtonModel: Codable { public struct AlertButtonModel: Codable, Equatable {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Properties // MARK: - Properties
//-------------------------------------------------- //--------------------------------------------------
@ -62,14 +63,21 @@ public struct AlertButtonModel: Codable {
try container.encodeModel(action, forKey: .action) try container.encodeModel(action, forKey: .action)
try container.encodeIfPresent(preferred, forKey: .preferred) try container.encodeIfPresent(preferred, forKey: .preferred)
} }
public static func == (lhs: AlertButtonModel, rhs: AlertButtonModel) -> Bool {
lhs.title == rhs.title
&& lhs.preferred == rhs.preferred
&& lhs.style == rhs.style
&& lhs.action.isEqual(to: rhs.action)
}
} }
public struct AlertModel: Codable, Identifiable, AlertModelProtocol { public struct AlertModel: Codable, Identifiable, Equatable, AlertModelProtocol {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Properties // MARK: - Properties
//-------------------------------------------------- //--------------------------------------------------
public var title: String? public var title: String?
public var message: String? public var message: String?
public var preferredStyle: UIAlertController.Style = .alert public var preferredStyle: UIAlertController.Style = .alert
@ -78,7 +86,7 @@ public struct AlertModel: Codable, Identifiable, AlertModelProtocol {
public var id: String public var id: String
public var delegateObject: DelegateObject? public var delegateObject: DelegateObject?
public var actions: [UIAlertAction] { public var actions: [UIAlertAction] {
get { get {
buttonModels.map({ alertButtonModel in buttonModels.map({ alertButtonModel in
@ -94,8 +102,7 @@ public struct AlertModel: Codable, Identifiable, AlertModelProtocol {
}) })
} }
} }
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Init // MARK: - Init
//-------------------------------------------------- //--------------------------------------------------
@ -149,6 +156,14 @@ public struct AlertModel: Codable, Identifiable, AlertModelProtocol {
try container.encodeIfPresent(analyticsData, forKey: .analyticsData) try container.encodeIfPresent(analyticsData, forKey: .analyticsData)
try container.encode(id, forKey: .id) try container.encode(id, forKey: .id)
} }
public static func == (lhs: AlertModel, rhs: AlertModel) -> Bool {
lhs.title == rhs.title
&& lhs.message == rhs.title
&& lhs.preferredStyle == rhs.preferredStyle
&& lhs.buttonModels == rhs.buttonModels
&& lhs.analyticsData == rhs.analyticsData
}
} }
public extension AlertButtonModel { public extension AlertButtonModel {

View File

@ -30,9 +30,9 @@ import UIKit
} }
public override class func nameForReuse(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> String? { public override class func nameForReuse(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> String? {
if let listModel = model as? ListItemModel, listModel.hasStableId { // if let listModel = model as? ListItemModel, listModel.hasStableId {
return "\(MoleculeContainer.nameForReuse(with: model, delegateObject) ?? "")<\(listModel.id)>" // return "\(MoleculeContainer.nameForReuse(with: model, delegateObject) ?? "")<\(listModel.id)>"
} // }
return MoleculeContainer.nameForReuse(with: model, delegateObject) return MoleculeContainer.nameForReuse(with: model, delegateObject)
} }

View File

@ -21,9 +21,6 @@ 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. /// 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 moleculeLayoutUpdated(_ molecule: MoleculeViewProtocol) //optional
/// Attempts to replace the molecules provided. Returns the ones that replaced successfully.
func replaceMoleculeData(_ moleculeModels: [MoleculeModelProtocol], completionHandler: (([MoleculeModelProtocol])->Void)?)
} }
extension MoleculeDelegateProtocol { extension MoleculeDelegateProtocol {

View File

@ -150,7 +150,7 @@ import MVMCore
do { do {
let template = try parsePageJSON(loadObject: loadObject) let template = try parsePageJSON(loadObject: loadObject)
pageModel = template // TODO: Eventually this page parsing should be done outside of this class and then set by the caller. For now, double duty. pageModel = template // TODO: Eventually this page parsing should be done outside of this class and then set by the caller. For now, double duty.
isFirstRender = true isFirstRender = true // Assuming this is only on the first page load from the handler. Might need to revist later.
if let backgroundRequest = loadObject.requestParameters?.backgroundRequest, !backgroundRequest, let pageType, let identifier = loadObject.identifier { if let backgroundRequest = loadObject.requestParameters?.backgroundRequest, !backgroundRequest, let pageType, let identifier = loadObject.identifier {
MVMCoreLoggingHandler.shared()?.logCoreEvent(.pageProcessingComplete(pageType: pageType, requestUUID: identifier, webUrl: nil)) MVMCoreLoggingHandler.shared()?.logCoreEvent(.pageProcessingComplete(pageType: pageType, requestUUID: identifier, webUrl: nil))
} }
@ -234,7 +234,7 @@ import MVMCore
open func handleNewData(_ pageModel: PageModelProtocol? = nil) { open func handleNewData(_ pageModel: PageModelProtocol? = nil) {
guard var newPageModel = pageModel ?? self.pageModel else { return } guard var newPageModel = pageModel ?? self.pageModel else { return }
let originalModel = self.isFirstRender ? self.pageModel as? MVMControllerModelProtocol : nil let originalModel = isFirstRender ? self.pageModel as? MVMControllerModelProtocol : nil
if originalModel != nil, let behaviorContainer = newPageModel as? PageBehaviorContainerModelProtocol { if originalModel != nil, let behaviorContainer = newPageModel as? PageBehaviorContainerModelProtocol {
var behaviorHandler = self var behaviorHandler = self
@ -246,8 +246,10 @@ import MVMCore
newPageModel.navigationBar = navigationItem newPageModel.navigationBar = navigationItem
} }
// Make the template available for onPageNew behavior handling. See if we can have behaviors rely on roots later.
self.pageModel = newPageModel self.pageModel = newPageModel
// Run through behavior tranformations.
var behaviorUpdatedModels = [MoleculeModelProtocol]() var behaviorUpdatedModels = [MoleculeModelProtocol]()
if var newTemplateModel = newPageModel as? TemplateModelProtocol { if var newTemplateModel = newPageModel as? TemplateModelProtocol {
executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in
@ -258,6 +260,8 @@ import MVMCore
debugLog("Behavior updated \(molecule) in template model.") debugLog("Behavior updated \(molecule) in template model.")
behaviorUpdatedModels.append(molecule) // Need to specifically trace molecule updates here as replacements are modifying the original tree. (We don't have a deep copy.) behaviorUpdatedModels.append(molecule) // Need to specifically trace molecule updates here as replacements are modifying the original tree. (We don't have a deep copy.)
} }
} else {
debugLog("Failed to replace \(molecule) in the template model.")
} }
} }
} }
@ -265,13 +269,14 @@ import MVMCore
} }
if formValidator == nil { // TODO: Can't change form rules? if formValidator == nil { // TODO: Can't change form rules?
let rules = (newPageModel as? MVMControllerModelProtocol)?.formRules let rules = (newPageModel as? FormHolderModelProtocol)?.formRules
formValidator = FormValidator(rules) formValidator = FormValidator(rules)
} }
// Reset after tranformations.
self.pageModel = newPageModel self.pageModel = newPageModel
/// Run through the differences between separate page model trees. // Run through the differences between separate page model trees.
var pageUpdatedModels = [MoleculeModelProtocol]() var pageUpdatedModels = [MoleculeModelProtocol]()
if let originalModel, // We had a prior. if let originalModel, // We had a prior.
let newPageModel = newPageModel as? TemplateModelProtocol, let newPageModel = newPageModel as? TemplateModelProtocol,
@ -573,49 +578,6 @@ import MVMCore
// Needed otherwise when subclassed, the extension gets called. // Needed otherwise when subclassed, the extension gets called.
open func moleculeLayoutUpdated(_ molecule: MoleculeViewProtocol) { } open func moleculeLayoutUpdated(_ molecule: MoleculeViewProtocol) { }
public func replaceMoleculeData(_ moleculeModels: [MoleculeModelProtocol], completionHandler: (([MoleculeModelProtocol])->Void)? = nil) {
pageUpdateQueue.addOperation { [self] in
let replacedModels:[(MoleculeModelProtocol, MoleculeModelProtocol)] = moleculeModels.compactMap { model in
guard let replacedMolecule = attemptToReplace(with: model) else {
return nil
}
return (model, replacedMolecule)
}
let uiUpdatedModels: [MoleculeModelProtocol] = replacedModels.compactMap { new, existing in
guard !new.isEqual(to: existing) else {
debugLog("UI for molecules: \(new) is the same. Skip UI update.")
return nil
}
return new
}
if uiUpdatedModels.count > 0 {
debugLog("Updating UI for molecules: \(uiUpdatedModels)")
DispatchQueue.main.sync {
self.updateUI(for: uiUpdatedModels)
}
}
completionHandler?(replacedModels.map { $0.0 })
}
}
open func attemptToReplace(with replacementModel: MoleculeModelProtocol) -> MoleculeModelProtocol? {
guard var templateModel = getTemplateModel() else { return nil }
var replacedMolecule: MoleculeModelProtocol?
do {
replacedMolecule = try templateModel.replaceMolecule(with: replacementModel)
if replacedMolecule == nil {
MVMCoreLoggingHandler.shared()?.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.shared()?.addError(toLog: coreError)
}
return replacedMolecule
}
//-------------------------------------------------- //--------------------------------------------------
// MARK: - MVMCoreUIDetailViewProtocol // MARK: - MVMCoreUIDetailViewProtocol
//-------------------------------------------------- //--------------------------------------------------

View File

@ -159,6 +159,13 @@ public class AddMoleculesActionModel: ActionModelProtocol {
extraParameters = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .extraParameters) extraParameters = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .extraParameters)
analyticsData = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .analyticsData) analyticsData = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .analyticsData)
} }
public func isEqual(to model: any ModelComparisonProtocol) -> Bool {
guard let model = model as? Self else { return false }
return model.extraParameters == extraParameters
&& model.analyticsData == analyticsData
&& model.animation == animation
}
} }
public class RemoveMoleculesActionModel: ActionModelProtocol { public class RemoveMoleculesActionModel: ActionModelProtocol {
@ -186,6 +193,13 @@ public class RemoveMoleculesActionModel: ActionModelProtocol {
extraParameters = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .extraParameters) extraParameters = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .extraParameters)
analyticsData = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .analyticsData) analyticsData = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .analyticsData)
} }
public func isEqual(to model: any ModelComparisonProtocol) -> Bool {
guard let model = model as? Self else { return false }
return model.extraParameters == extraParameters
&& model.analyticsData == analyticsData
&& model.animation == animation
}
} }
public class SwapMoleculesActionModel: ActionModelProtocol { public class SwapMoleculesActionModel: ActionModelProtocol {
@ -213,4 +227,11 @@ public class SwapMoleculesActionModel: ActionModelProtocol {
extraParameters = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .extraParameters) extraParameters = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .extraParameters)
analyticsData = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .analyticsData) analyticsData = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .analyticsData)
} }
public func isEqual(to model: any ModelComparisonProtocol) -> Bool {
guard let model = model as? Self else { return false }
return model.extraParameters == extraParameters
&& model.analyticsData == analyticsData
&& model.animation == animation
}
} }

View File

@ -20,6 +20,12 @@ public class PageGetContactBehaviorModel: PageBehaviorModelProtocol {
public var shouldAllowMultipleInstances: Bool { false } public var shouldAllowMultipleInstances: Bool { false }
public init() { } public init() { }
// Default
// public func isEqual(to model: any MVMCore.ModelComparisonProtocol) -> Bool {
// guard let model = model as? Self else { return false }
// return behaviorName == model.behaviorName
//}
} }
public class PageGetContactBehavior: PageVisibilityBehavior { public class PageGetContactBehavior: PageVisibilityBehavior {

View File

@ -15,6 +15,12 @@ public class GetNotificationAuthStatusBehaviorModel: PageBehaviorModelProtocol {
public var shouldAllowMultipleInstances: Bool { false } public var shouldAllowMultipleInstances: Bool { false }
public init() { } public init() { }
// Default
// public func isEqual(to model: any ModelComparisonProtocol) -> Bool {
// guard let model = model as? Self else { return false }
// return behaviorName == model.behaviorName
//}
} }
public class GetNotificationAuthStatusBehavior: PageVisibilityBehavior { public class GetNotificationAuthStatusBehavior: PageVisibilityBehavior {

View File

@ -46,6 +46,15 @@ public class PollingBehaviorModel: PageBehaviorModelProtocol {
try container.encode(refreshOnFirstLoad, forKey: .refreshOnFirstLoad) try container.encode(refreshOnFirstLoad, forKey: .refreshOnFirstLoad)
try container.encode(refreshOnShown, forKey: .refreshOnShown) try container.encode(refreshOnShown, forKey: .refreshOnShown)
} }
public func isEqual(to model: any ModelComparisonProtocol) -> Bool {
guard let model = model as? Self else { return false }
return runWhileHidden == model.runWhileHidden
&& refreshOnShown == model.refreshOnShown
&& refreshOnFirstLoad == model.refreshOnFirstLoad
&& refreshInterval == model.refreshInterval
&& refreshAction.isEqual(to: model.refreshAction)
}
} }
extension PollingBehaviorModel: CustomDebugStringConvertible { extension PollingBehaviorModel: CustomDebugStringConvertible {

View File

@ -33,6 +33,7 @@ public extension PageBehaviorHandlerProtocol {
mutating func applyBehaviors(pageBehaviorModel: PageBehaviorContainerModelProtocol, delegateObject: MVMCoreUIDelegateObject?) { mutating func applyBehaviors(pageBehaviorModel: PageBehaviorContainerModelProtocol, delegateObject: MVMCoreUIDelegateObject?) {
// Pull the existing behaviors. // Pull the existing behaviors.
var behaviors = (behaviors ?? []).filter { $0.transcendsPageUpdates } var behaviors = (behaviors ?? []).filter { $0.transcendsPageUpdates }
// Create and append any new behaviors based on the incoming models. // Create and append any new behaviors based on the incoming models.
let newBehaviors = createBehaviors(for: pageBehaviorModel.behaviors ?? [], delegateObject: delegateObject) let newBehaviors = createBehaviors(for: pageBehaviorModel.behaviors ?? [], delegateObject: delegateObject)
behaviors.append(contentsOf: newBehaviors) behaviors.append(contentsOf: newBehaviors)

View File

@ -33,4 +33,9 @@ public extension PageBehaviorModelProtocol {
static var categoryName: String { static var categoryName: String {
"\(PageBehaviorModelProtocol.self)" "\(PageBehaviorModelProtocol.self)"
} }
func isEqual(to model: any ModelComparisonProtocol) -> Bool {
guard let model = model as? Self else { return false }
return behaviorName == model.behaviorName
}
} }

View File

@ -13,6 +13,11 @@ public class ReplaceableMoleculeBehaviorModel: PageBehaviorModelProtocol {
public class var identifier: String { "replaceMoleculeBehavior" } public class var identifier: String { "replaceMoleculeBehavior" }
public var shouldAllowMultipleInstances: Bool { true } public var shouldAllowMultipleInstances: Bool { true }
public var moleculeIds: [String] public var moleculeIds: [String]
public func isEqual(to model: any MVMCore.ModelComparisonProtocol) -> Bool {
guard let model = model as? Self else { return false }
return moleculeIds == model.moleculeIds
}
} }
public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior, CoreLogging { public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior, CoreLogging {
@ -92,7 +97,11 @@ public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior, Co
hasReplacement = true hasReplacement = true
} }
} catch { } catch {
let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: String(describing: type(of: self)))!
if let error = error as? HumanReadableDecodingErrorProtocol {
coreError.messageToLog = "Error replacing molecule \"\(newMolecule.id)\": \(error.readableDescription)"
}
MVMCoreLoggingHandler.shared()?.addError(toLog: coreError)
} }
} }
return parentMolecule return parentMolecule

View File

@ -7,13 +7,14 @@
// //
public class ScreenBrightnessModifierBehaviorModel: PageBehaviorModelProtocol { public class ScreenBrightnessModifierBehaviorModel: PageBehaviorModelProtocol {
public var shouldAllowMultipleInstances: Bool = false public var shouldAllowMultipleInstances: Bool = false
public static var identifier = "screenBrightnessModifier" public static var identifier = "screenBrightnessModifier"
@Clamping(range: 0...1) var screenBrightness: CGFloat @Clamping(range: 0...1) var screenBrightness: CGFloat
var originalScreenBrightness: CGFloat? var originalScreenBrightness: CGFloat?
//MARK:- Codable //MARK:- Codable
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case screenBrightness case screenBrightness
} }
@ -22,11 +23,16 @@ public class ScreenBrightnessModifierBehaviorModel: PageBehaviorModelProtocol {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self) let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
screenBrightness = try typeContainer.decode(CGFloat.self, forKey: .screenBrightness) screenBrightness = try typeContainer.decode(CGFloat.self, forKey: .screenBrightness)
} }
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self) var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(screenBrightness, forKey: .screenBrightness) try container.encode(screenBrightness, forKey: .screenBrightness)
} }
public func isEqual(to model: ModelComparisonProtocol) -> Bool {
guard let model = model as? Self else { return false }
return screenBrightness == model.screenBrightness
}
} }
public class ScreenBrightnessModifierBehavior: PageVisibilityBehavior { public class ScreenBrightnessModifierBehavior: PageVisibilityBehavior {

View File

@ -15,7 +15,7 @@ public class RuleEqualsModel: RuleCompareModelProtocol {
public static var identifier: String = "equals" public static var identifier: String = "equals"
public var type: String = RuleEqualsModel.identifier public var type: String = RuleEqualsModel.identifier
public var ruleId: String? public var ruleId: String?
public var fields: [String] public var fields: [String]
public var errorMessage: [String: String]? public var errorMessage: [String: String]?

View File

@ -9,7 +9,7 @@
import Foundation import Foundation
import MVMCore import MVMCore
open class NotificationModel: Codable, Identifiable { open class NotificationModel: Codable, Identifiable, Equatable {
public var type: String public var type: String
public var priority = Operation.QueuePriority.normal public var priority = Operation.QueuePriority.normal
public var molecule: MoleculeModelProtocol public var molecule: MoleculeModelProtocol
@ -115,4 +115,14 @@ open class NotificationModel: Codable, Identifiable {
try container.encodeIfPresent(analyticsData, forKey: .analyticsData) try container.encodeIfPresent(analyticsData, forKey: .analyticsData)
try container.encode(id, forKey: .id) try container.encode(id, forKey: .id)
} }
public static func == (lhs: NotificationModel, rhs: NotificationModel) -> Bool {
lhs.persistent == rhs.persistent
&& lhs.priority == rhs.priority
&& lhs.type == rhs.type
&& lhs.persistent == rhs.persistent
&& lhs.dismissTime == rhs.dismissTime
&& lhs.pages == rhs.pages
&& lhs.analyticsData == rhs.analyticsData
}
} }