diff --git a/MVMCoreUI/Atomic/Actions/ActionAlertModel.swift b/MVMCoreUI/Atomic/Actions/ActionAlertModel.swift index f657304b..1787da20 100644 --- a/MVMCoreUI/Atomic/Actions/ActionAlertModel.swift +++ b/MVMCoreUI/Atomic/Actions/ActionAlertModel.swift @@ -25,4 +25,11 @@ public struct ActionAlertModel: ActionModelProtocol { public init(alert: AlertModel) { 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 + } } diff --git a/MVMCoreUI/Atomic/Actions/ActionCollapseNotificationModel.swift b/MVMCoreUI/Atomic/Actions/ActionCollapseNotificationModel.swift index 94e84744..71ed49af 100644 --- a/MVMCoreUI/Atomic/Actions/ActionCollapseNotificationModel.swift +++ b/MVMCoreUI/Atomic/Actions/ActionCollapseNotificationModel.swift @@ -20,4 +20,12 @@ public struct ActionCollapseNotificationModel: ActionModelProtocol { self.extraParameters = extraParameters 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 + //} } diff --git a/MVMCoreUI/Atomic/Actions/ActionDismissNotificationModel.swift b/MVMCoreUI/Atomic/Actions/ActionDismissNotificationModel.swift index 13816f37..e5e2781a 100644 --- a/MVMCoreUI/Atomic/Actions/ActionDismissNotificationModel.swift +++ b/MVMCoreUI/Atomic/Actions/ActionDismissNotificationModel.swift @@ -20,4 +20,12 @@ public struct ActionDismissNotificationModel: ActionModelProtocol { self.extraParameters = extraParameters 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 + //} } diff --git a/MVMCoreUI/Atomic/Actions/ActionOpenPanelModel.swift b/MVMCoreUI/Atomic/Actions/ActionOpenPanelModel.swift index ec526e10..2a69eb05 100644 --- a/MVMCoreUI/Atomic/Actions/ActionOpenPanelModel.swift +++ b/MVMCoreUI/Atomic/Actions/ActionOpenPanelModel.swift @@ -29,4 +29,12 @@ public struct ActionOpenPanelModel: ActionModelProtocol { self.extraParameters = extraParameters 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 + } } diff --git a/MVMCoreUI/Atomic/Actions/ActionTopNotificationModel.swift b/MVMCoreUI/Atomic/Actions/ActionTopNotificationModel.swift index 07d13230..b5e0584f 100644 --- a/MVMCoreUI/Atomic/Actions/ActionTopNotificationModel.swift +++ b/MVMCoreUI/Atomic/Actions/ActionTopNotificationModel.swift @@ -22,4 +22,11 @@ public struct ActionTopNotificationModel: ActionModelProtocol { self.extraParameters = extraParameters 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 + } } diff --git a/MVMCoreUI/Atomic/Actions/AlertModel.swift b/MVMCoreUI/Atomic/Actions/AlertModel.swift index 8a36ab03..683d87a4 100644 --- a/MVMCoreUI/Atomic/Actions/AlertModel.swift +++ b/MVMCoreUI/Atomic/Actions/AlertModel.swift @@ -9,7 +9,8 @@ import UIKit import MVMCore -public struct AlertButtonModel: Codable { +public struct AlertButtonModel: Codable, Equatable { + //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -62,14 +63,21 @@ public struct AlertButtonModel: Codable { try container.encodeModel(action, forKey: .action) 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 //-------------------------------------------------- - + public var title: String? public var message: String? public var preferredStyle: UIAlertController.Style = .alert @@ -78,7 +86,7 @@ public struct AlertModel: Codable, Identifiable, AlertModelProtocol { public var id: String public var delegateObject: DelegateObject? - + public var actions: [UIAlertAction] { get { buttonModels.map({ alertButtonModel in @@ -94,8 +102,7 @@ public struct AlertModel: Codable, Identifiable, AlertModelProtocol { }) } } - - + //-------------------------------------------------- // MARK: - Init //-------------------------------------------------- @@ -149,6 +156,14 @@ public struct AlertModel: Codable, Identifiable, AlertModelProtocol { try container.encodeIfPresent(analyticsData, forKey: .analyticsData) 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 { diff --git a/MVMCoreUI/Atomic/Molecules/Items/MoleculeTableViewCell.swift b/MVMCoreUI/Atomic/Molecules/Items/MoleculeTableViewCell.swift index 731cfccd..01be79bd 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/MoleculeTableViewCell.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/MoleculeTableViewCell.swift @@ -30,9 +30,9 @@ import UIKit } public override class func nameForReuse(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> String? { - if let listModel = model as? ListItemModel, listModel.hasStableId { - return "\(MoleculeContainer.nameForReuse(with: model, delegateObject) ?? "")<\(listModel.id)>" - } +// if let listModel = model as? ListItemModel, listModel.hasStableId { +// return "\(MoleculeContainer.nameForReuse(with: model, delegateObject) ?? "")<\(listModel.id)>" +// } return MoleculeContainer.nameForReuse(with: model, delegateObject) } diff --git a/MVMCoreUI/Atomic/Protocols/MoleculeDelegateProtocol.swift b/MVMCoreUI/Atomic/Protocols/MoleculeDelegateProtocol.swift index e8cae35b..e78c0b1b 100644 --- a/MVMCoreUI/Atomic/Protocols/MoleculeDelegateProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/MoleculeDelegateProtocol.swift @@ -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. 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 { diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index 0486f6b4..4fd8d5f5 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -150,7 +150,7 @@ import MVMCore do { 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. - 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 { MVMCoreLoggingHandler.shared()?.logCoreEvent(.pageProcessingComplete(pageType: pageType, requestUUID: identifier, webUrl: nil)) } @@ -234,7 +234,7 @@ import MVMCore open func handleNewData(_ pageModel: PageModelProtocol? = nil) { 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 { var behaviorHandler = self @@ -246,8 +246,10 @@ import MVMCore newPageModel.navigationBar = navigationItem } + // Make the template available for onPageNew behavior handling. See if we can have behaviors rely on roots later. self.pageModel = newPageModel + // Run through behavior tranformations. var behaviorUpdatedModels = [MoleculeModelProtocol]() if var newTemplateModel = newPageModel as? TemplateModelProtocol { executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in @@ -258,6 +260,8 @@ import MVMCore 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.) } + } 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? - let rules = (newPageModel as? MVMControllerModelProtocol)?.formRules + let rules = (newPageModel as? FormHolderModelProtocol)?.formRules formValidator = FormValidator(rules) } + // Reset after tranformations. self.pageModel = newPageModel - /// Run through the differences between separate page model trees. + // Run through the differences between separate page model trees. var pageUpdatedModels = [MoleculeModelProtocol]() if let originalModel, // We had a prior. let newPageModel = newPageModel as? TemplateModelProtocol, @@ -573,49 +578,6 @@ import MVMCore // Needed otherwise when subclassed, the extension gets called. 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 //-------------------------------------------------- diff --git a/MVMCoreUI/Behaviors/AddRemoveMoleculeBehavior.swift b/MVMCoreUI/Behaviors/AddRemoveMoleculeBehavior.swift index 3ea65249..0288ee1b 100644 --- a/MVMCoreUI/Behaviors/AddRemoveMoleculeBehavior.swift +++ b/MVMCoreUI/Behaviors/AddRemoveMoleculeBehavior.swift @@ -159,6 +159,13 @@ public class AddMoleculesActionModel: ActionModelProtocol { extraParameters = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .extraParameters) 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 { @@ -186,6 +193,13 @@ public class RemoveMoleculesActionModel: ActionModelProtocol { extraParameters = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .extraParameters) 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 { @@ -213,4 +227,11 @@ public class SwapMoleculesActionModel: ActionModelProtocol { extraParameters = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .extraParameters) 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 + } } diff --git a/MVMCoreUI/Behaviors/GetContactBehavior.swift b/MVMCoreUI/Behaviors/GetContactBehavior.swift index 755c5706..b0281fd8 100644 --- a/MVMCoreUI/Behaviors/GetContactBehavior.swift +++ b/MVMCoreUI/Behaviors/GetContactBehavior.swift @@ -20,6 +20,12 @@ public class PageGetContactBehaviorModel: PageBehaviorModelProtocol { public var shouldAllowMultipleInstances: Bool { false } 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 { diff --git a/MVMCoreUI/Behaviors/GetNotificationAuthStatusBehavior.swift b/MVMCoreUI/Behaviors/GetNotificationAuthStatusBehavior.swift index 980f0dbf..4cbb05b4 100644 --- a/MVMCoreUI/Behaviors/GetNotificationAuthStatusBehavior.swift +++ b/MVMCoreUI/Behaviors/GetNotificationAuthStatusBehavior.swift @@ -15,6 +15,12 @@ public class GetNotificationAuthStatusBehaviorModel: PageBehaviorModelProtocol { public var shouldAllowMultipleInstances: Bool { false } 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 { diff --git a/MVMCoreUI/Behaviors/PollingBehaviorModel.swift b/MVMCoreUI/Behaviors/PollingBehaviorModel.swift index 719e22ea..5f87593b 100644 --- a/MVMCoreUI/Behaviors/PollingBehaviorModel.swift +++ b/MVMCoreUI/Behaviors/PollingBehaviorModel.swift @@ -46,6 +46,15 @@ public class PollingBehaviorModel: PageBehaviorModelProtocol { try container.encode(refreshOnFirstLoad, forKey: .refreshOnFirstLoad) 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 { diff --git a/MVMCoreUI/Behaviors/Protocols/PageBehaviorHandlerProtocol.swift b/MVMCoreUI/Behaviors/Protocols/PageBehaviorHandlerProtocol.swift index 5c6a78bb..566a3f3d 100644 --- a/MVMCoreUI/Behaviors/Protocols/PageBehaviorHandlerProtocol.swift +++ b/MVMCoreUI/Behaviors/Protocols/PageBehaviorHandlerProtocol.swift @@ -33,6 +33,7 @@ public extension PageBehaviorHandlerProtocol { mutating func applyBehaviors(pageBehaviorModel: PageBehaviorContainerModelProtocol, delegateObject: MVMCoreUIDelegateObject?) { // Pull the existing behaviors. var behaviors = (behaviors ?? []).filter { $0.transcendsPageUpdates } + // Create and append any new behaviors based on the incoming models. let newBehaviors = createBehaviors(for: pageBehaviorModel.behaviors ?? [], delegateObject: delegateObject) behaviors.append(contentsOf: newBehaviors) diff --git a/MVMCoreUI/Behaviors/Protocols/PageBehaviorModelProtocol.swift b/MVMCoreUI/Behaviors/Protocols/PageBehaviorModelProtocol.swift index 8b9eabab..510533d2 100644 --- a/MVMCoreUI/Behaviors/Protocols/PageBehaviorModelProtocol.swift +++ b/MVMCoreUI/Behaviors/Protocols/PageBehaviorModelProtocol.swift @@ -33,4 +33,9 @@ public extension PageBehaviorModelProtocol { static var categoryName: String { "\(PageBehaviorModelProtocol.self)" } + + func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return behaviorName == model.behaviorName + } } diff --git a/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift b/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift index 4febdcc1..66712434 100644 --- a/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift +++ b/MVMCoreUI/Behaviors/ReplaceableMoleculeBehaviorModel.swift @@ -13,6 +13,11 @@ public class ReplaceableMoleculeBehaviorModel: PageBehaviorModelProtocol { public class var identifier: String { "replaceMoleculeBehavior" } public var shouldAllowMultipleInstances: Bool { true } 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 { @@ -92,7 +97,11 @@ public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior, Co hasReplacement = true } } 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 diff --git a/MVMCoreUI/Behaviors/ScreenBrightnessModifierBehavior.swift b/MVMCoreUI/Behaviors/ScreenBrightnessModifierBehavior.swift index 59d80211..e830172a 100644 --- a/MVMCoreUI/Behaviors/ScreenBrightnessModifierBehavior.swift +++ b/MVMCoreUI/Behaviors/ScreenBrightnessModifierBehavior.swift @@ -7,13 +7,14 @@ // public class ScreenBrightnessModifierBehaviorModel: PageBehaviorModelProtocol { + public var shouldAllowMultipleInstances: Bool = false public static var identifier = "screenBrightnessModifier" @Clamping(range: 0...1) var screenBrightness: CGFloat var originalScreenBrightness: CGFloat? //MARK:- Codable - + private enum CodingKeys: String, CodingKey { case screenBrightness } @@ -22,11 +23,16 @@ public class ScreenBrightnessModifierBehaviorModel: PageBehaviorModelProtocol { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) screenBrightness = try typeContainer.decode(CGFloat.self, forKey: .screenBrightness) } - + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) 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 { diff --git a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleEqualsModel.swift b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleEqualsModel.swift index be7391ec..73958d74 100644 --- a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleEqualsModel.swift +++ b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleEqualsModel.swift @@ -15,7 +15,7 @@ public class RuleEqualsModel: RuleCompareModelProtocol { public static var identifier: String = "equals" public var type: String = RuleEqualsModel.identifier - public var ruleId: String? + public var ruleId: String? public var fields: [String] public var errorMessage: [String: String]? diff --git a/MVMCoreUI/Notification/NotificationModel.swift b/MVMCoreUI/Notification/NotificationModel.swift index 435d95b6..70736064 100644 --- a/MVMCoreUI/Notification/NotificationModel.swift +++ b/MVMCoreUI/Notification/NotificationModel.swift @@ -9,7 +9,7 @@ import Foundation import MVMCore -open class NotificationModel: Codable, Identifiable { +open class NotificationModel: Codable, Identifiable, Equatable { public var type: String public var priority = Operation.QueuePriority.normal public var molecule: MoleculeModelProtocol @@ -115,4 +115,14 @@ open class NotificationModel: Codable, Identifiable { try container.encodeIfPresent(analyticsData, forKey: .analyticsData) 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 + } }