From be196a289e9a5d4102d18918fd963e321c3aa80c Mon Sep 17 00:00:00 2001 From: Kevin G Christiano Date: Tue, 22 Jun 2021 13:10:06 -0400 Subject: [PATCH] aligns behavior for user intiaited corner cases. --- .../Atoms/Selectors/CheckboxModel.swift | 4 +- .../Behaviors/SelectAllBoxesBehavior.swift | 108 +++++++++++++++--- 2 files changed, 94 insertions(+), 18 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/CheckboxModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/CheckboxModel.swift index 0b98d1fc..34f0fd22 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/CheckboxModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/CheckboxModel.swift @@ -7,7 +7,7 @@ // -@objcMembers public class CheckboxModel: MoleculeModelProtocol, SelectableMoleculeModel, FormFieldProtocol { +@objcMembers public class CheckboxModel: NSObject, MoleculeModelProtocol, SelectableMoleculeModel, FormFieldProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -15,7 +15,7 @@ public static var identifier: String = "checkbox" public var backgroundColor: Color? public var accessibilityIdentifier: String? - public var checked: Bool = false + public dynamic var checked: Bool = false public var enabled: Bool = true public var animated: Bool = true public var inverted: Bool = false diff --git a/MVMCoreUI/Behaviors/SelectAllBoxesBehavior.swift b/MVMCoreUI/Behaviors/SelectAllBoxesBehavior.swift index 93d7367e..27cbe69a 100644 --- a/MVMCoreUI/Behaviors/SelectAllBoxesBehavior.swift +++ b/MVMCoreUI/Behaviors/SelectAllBoxesBehavior.swift @@ -24,7 +24,7 @@ public class SelectAllBoxesBehaviorModel: PageBehaviorModelProtocol { //-------------------------------------------------- // MARK: - Codable //-------------------------------------------------- - + private enum CodingKeys: String, CodingKey { case selectAllTitle case deselectAllTitle @@ -41,7 +41,7 @@ public class SelectAllBoxesBehaviorModel: PageBehaviorModelProtocol { self.deselectAllTitle = deselectAllTitle } } - + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(selectAllTitle, forKey: .selectAllTitle) @@ -50,16 +50,23 @@ public class SelectAllBoxesBehaviorModel: PageBehaviorModelProtocol { } /// Selects all the control models presented on a page. -public class SelectAllBoxesBehavior: PageCustomActionHandlerBehavior { +public class SelectAllBoxesBehavior: PageCustomActionHandlerBehavior, PageMoleculeTransformationBehavior { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- /// Status of the select all behavior. Initially false as the action has not been enaged. - var selectAllState = false + var didSelectAllState = false + /// Reference to the general PageBehaviorModel. var model: PageBehaviorModelProtocol + /// Dictionary of KVOs to observing the selected property of each `SelectableMoleculeModel`. + private var observers = [String: NSKeyValueObservation?]() + + /// A store representing the values of the `SelectableMoleculeModel`. + private var valuesMirror = [String: Bool]() + //-------------------------------------------------- // MARK: - Delegate //-------------------------------------------------- @@ -75,6 +82,45 @@ public class SelectAllBoxesBehavior: PageCustomActionHandlerBehavior { self.delegate = delegateObject } + public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject) { + + let selectableModels: [SelectableMoleculeModel] = rootMolecules.allMoleculesOfType() + + guard !selectableModels.isEmpty else { return } + + for model in selectableModels { + if let checkboxModel = model as? CheckboxModel, let key = checkboxModel.fieldKey { + + valuesMirror[key] = checkboxModel.checked + + observers[key] = checkboxModel.observe(\.checked, options: [.new]) { [weak self] model, change in + guard let self = self, + let isChecked = change.newValue, + let key = model.fieldKey + else { return } + + self.valuesMirror[key] = isChecked + + // If all are models are in the opposite state of the behavior, then realign. + if self.selectAllIsMisaligned() { + self.realignPageBehavior(asSelectAll: true) + + } else if self.deselectAllIsMisaligned() { + self.realignPageBehavior(asSelectAll: false) + } + } + } + } + } + + //-------------------------------------------------- + // MARK: - Deinit + //-------------------------------------------------- + + deinit { + observers.values.forEach { $0?.invalidate() } + } + //-------------------------------------------------- // MARK: - Custom Action //-------------------------------------------------- @@ -91,25 +137,45 @@ public class SelectAllBoxesBehavior: PageCustomActionHandlerBehavior { // Verify we have the correct action type and necessary values. guard actionType == "selectAllBoxes", let selectableModels: [SelectableMoleculeModel] = delegate?.moleculeDelegate?.getRootMolecules().allMoleculesOfType(), - !selectableModels.isEmpty, - let model = model as? SelectAllBoxesBehaviorModel + !selectableModels.isEmpty else { return false } // Flip the selected state of the behavior. - selectAllState.toggle() + didSelectAllState.toggle() // Iterate through selectable molecules. for selectableModel in selectableModels { if toSelect(model: selectableModel) || toDeselect(model: selectableModel) { - selectableModel.select(as: selectAllState) + selectableModel.select(as: didSelectAllState) } } - // Get title to update the nav button title. - let navButtonTitle: String? = selectAllState ? model.deselectAllTitle : model.selectAllTitle + updatePageNavigationUI() + return true + } + + //-------------------------------------------------- + // MARK: - Methods + //-------------------------------------------------- + + /// In the event that the user manually selects or deselects all `SelectableMoleculeModel` + /// the behavior will need to reflect the inverse of its previously expected action. + /// Initiates the navigation and page behavior realignment + /// - Parameter asSelectAll: The actual value didSelectAllState ought to be. + func realignPageBehavior(asSelectAll: Bool) { + didSelectAllState = asSelectAll + updatePageNavigationUI() + } + + /// Updates the navigation UI to correctly reflect the behavior's state. + func updatePageNavigationUI() { - MVMCoreDispatchUtility.performBlock(onMainThread: { - guard let controller = self.delegate?.moleculeDelegate as? ViewController else { return } + guard let model = model as? SelectAllBoxesBehaviorModel else { return } + + let navButtonTitle: String? = didSelectAllState ? model.deselectAllTitle : model.selectAllTitle + + MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in + guard let controller = self?.delegate?.moleculeDelegate as? ViewController else { return } controller.handleNewDataAndUpdateUI() if MVMCoreUIUtility.getCurrentVisibleController() == controller { @@ -117,21 +183,31 @@ public class SelectAllBoxesBehavior: PageCustomActionHandlerBehavior { controller.manager?.refreshNavigationUI() } }) - - return true + } + + /// Convenience function for readability to confirmt he state of the behavior. + /// - Returns: Boolean indicating that the behavior's `didSelectAllState` is false while all model values are true (selected). + func selectAllIsMisaligned() -> Bool { + !didSelectAllState && valuesMirror.values.allSatisfy { $0 == true } + } + + /// Convenience function for readability to confirmt he state of the behavior. + /// - Returns: Boolean indicating that the behavior's `didSelectAllState` is true while all model values are false (deselected). + func deselectAllIsMisaligned() -> Bool { + didSelectAllState && valuesMirror.values.allSatisfy { $0 == false } } /// Convenience function making it easier to read if a current selectable model should be acted on. /// - Parameter model: A model object assined to the SelectableModel protocol /// - Returns: Boolean determining if the passed model should be selected. func toSelect(model: SelectableMoleculeModel) -> Bool { - selectAllState && !model.selectedValue + didSelectAllState && !model.selectedValue } /// Convenience function making it easier to read if a current selectable model should be acted on. /// - Parameter model: A model object assined to the SelectableModel protocol /// - Returns: Boolean determining if the passed model should be deselected. func toDeselect(model: SelectableMoleculeModel) -> Bool { - !selectAllState && model.selectedValue + !didSelectAllState && model.selectedValue } }