diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotification.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotification.swift index 8539c817..82983f4d 100644 --- a/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotification.swift +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotification.swift @@ -160,7 +160,7 @@ import Foundation /// Collapse if focus is no longer on this top alert. @objc func accessibilityFocusChanged(notification: Notification) { - if !MVMCoreUIUtility.viewContainsAccessiblityFocus(self) { + if (notification.userInfo?[UIAccessibility.focusedElementUserInfoKey] != nil) && !MVMCoreUIUtility.viewContainsAccessiblityFocus(self) { NotificationCenter.default.removeObserver(self, name: UIAccessibility.elementFocusedNotification, object: nil) collapse() } diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationModel.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationModel.swift index 1e77e61d..bd84b7fd 100644 --- a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationModel.swift +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationModel.swift @@ -8,6 +8,21 @@ open class NotificationModel: ContainerModel, MoleculeModelProtocol { + + /** + The style of the notification: + - success, green background, white content + - error, orange background, black content + - \warning, yellow background, black content + - information, blue background, white content + */ + public enum Style: String, Codable { + case success + case error + case warning + case information + } + //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -19,6 +34,7 @@ open class NotificationModel: ContainerModel, MoleculeModelProtocol { public var body: LabelModel? public var button: ButtonModel? public var closeButton: NotificationXButtonModel? + public var style: NotificationModel.Style = .success //-------------------------------------------------- // MARK: - Initializer @@ -40,20 +56,63 @@ open class NotificationModel: ContainerModel, MoleculeModelProtocol { bottomPadding = PaddingTwo if backgroundColor == nil { - backgroundColor = Color(uiColor: .mvmGreen) + switch style { + case .error: + backgroundColor = Color(uiColor: .mvmOrange) + case .warning: + backgroundColor = Color(uiColor: .mvmYellow) + case .information: + backgroundColor = Color(uiColor: .mvmBlue) + default: + backgroundColor = Color(uiColor: .mvmGreen) + } } if headline.textColor == nil { - headline.textColor = Color(uiColor: .mvmWhite) + switch style { + case .error, .warning: + headline.textColor = Color(uiColor: .mvmBlack) + default: + headline.textColor = Color(uiColor: .mvmWhite) + } } if body?.textColor == nil { - body?.textColor = Color(uiColor: .mvmWhite) + switch style { + case .error, .warning: + body?.textColor = Color(uiColor: .mvmBlack) + default: + body?.textColor = Color(uiColor: .mvmWhite) + } + } + + button?.size = .tiny + if button?.enabledTextColor == nil { + switch style { + case .error, .warning: + button?.enabledTextColor = Color(uiColor: .mvmBlack) + default: + button?.enabledTextColor = Color(uiColor: .mvmWhite) + } + } + if button?.enabledBorderColor == nil { + switch style { + case .error, .warning: + button?.enabledBorderColor = Color(uiColor: .mvmBlack) + default: + button?.enabledBorderColor = Color(uiColor: .mvmWhite) + } } if button?.style == nil { button?.style = .secondary } - button?.size = .tiny - button?.enabledTextColor = Color(uiColor: .mvmWhite) - button?.enabledBorderColor = Color(uiColor: .mvmWhite) + + if closeButton?.color == nil { + switch style { + case .error, .warning: + closeButton?.color = Color(uiColor: .mvmBlack) + default: + closeButton?.color = Color(uiColor: .mvmWhite) + } + } } //-------------------------------------------------- @@ -68,6 +127,7 @@ open class NotificationModel: ContainerModel, MoleculeModelProtocol { case body case button case closeButton + case style } //-------------------------------------------------- @@ -82,9 +142,12 @@ open class NotificationModel: ContainerModel, MoleculeModelProtocol { body = try typeContainer.decodeIfPresent(LabelModel.self, forKey: .body) button = try typeContainer.decodeIfPresent(ButtonModel.self, forKey: .button) closeButton = try typeContainer.decodeIfPresent(NotificationXButtonModel.self, forKey: .closeButton) + if let style = try typeContainer.decodeIfPresent(NotificationModel.Style.self, forKey: .style) { + self.style = style + } super.init() } - + open override func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(moleculeName, forKey: .moleculeName) @@ -94,5 +157,6 @@ open class NotificationModel: ContainerModel, MoleculeModelProtocol { try container.encodeIfPresent(body, forKey: .body) try container.encodeIfPresent(button, forKey: .button) try container.encodeIfPresent(closeButton, forKey: .closeButton) + try container.encode(style, forKey: .style) } } diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationXButton.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationXButton.swift index d2696722..204785cb 100644 --- a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationXButton.swift +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationXButton.swift @@ -33,7 +33,7 @@ import Foundation open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable : Any]?) { super.set(with: model, delegateObject, additionalData) guard let model = model as? NotificationXButtonModel else { return } - tintColor = model.color.uiColor + tintColor = model.color?.uiColor ?? .white // TODO: Temporary, consider action for dismissing top alert if model.action.actionType == ActionNoopModel.identifier { diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationXButtonModel.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationXButtonModel.swift index 81dae17e..59da1868 100644 --- a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationXButtonModel.swift +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationXButtonModel.swift @@ -11,7 +11,7 @@ import Foundation public class NotificationXButtonModel: ButtonModelProtocol, MoleculeModelProtocol { public static var identifier: String = "notificationXButton" public var backgroundColor: Color? - public var color = Color(uiColor: .white) + public var color: Color? public var action: ActionModelProtocol = ActionNoopModel() private enum CodingKeys: String, CodingKey { @@ -24,7 +24,7 @@ public class NotificationXButtonModel: ButtonModelProtocol, MoleculeModelProtoco public required init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) - color = try typeContainer.decodeIfPresent(Color.self, forKey: .color) ?? Color(uiColor: .white) + color = try typeContainer.decodeIfPresent(Color.self, forKey: .color) if let action: ActionModelProtocol = try typeContainer.decodeModelIfPresent(codingKey: .action) { self.action = action } @@ -33,7 +33,7 @@ public class NotificationXButtonModel: ButtonModelProtocol, MoleculeModelProtoco public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(moleculeName, forKey: .moleculeName) - try container.encode(color, forKey: .color) + try container.encodeIfPresent(color, forKey: .color) try container.encodeModel(action, forKey: .action) } } diff --git a/MVMCoreUI/FormUIHelpers/FormValidator.swift b/MVMCoreUI/FormUIHelpers/FormValidator.swift index a0785c41..bed97391 100644 --- a/MVMCoreUI/FormUIHelpers/FormValidator.swift +++ b/MVMCoreUI/FormUIHelpers/FormValidator.swift @@ -96,8 +96,13 @@ import MVMCore public func validateGroup(_ group: FormGroupRule) -> Bool { // Validate each rule. var valid = true + var previousValidity: [String: Bool] = [:] for rule in group.rules { - valid = valid && rule.validate(fields) + let tuple = rule.validate(fields, previousValidity) + let isValidRule = tuple.valid + let returnedValidity = tuple.fieldValidity + previousValidity = previousValidity.merging(returnedValidity) { (_, new) in new } + valid = valid && isValidRule } // Notify the group watchers of validity. diff --git a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAnyRequiredModel.swift b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAnyRequiredModel.swift index 7f153e83..3a9b3871 100644 --- a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAnyRequiredModel.swift +++ b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAnyRequiredModel.swift @@ -36,15 +36,23 @@ public class RuleAnyRequiredModel: RulesProtocol { return false } - public func validate(_ fieldMolecules: [String: FormFieldProtocol]) -> Bool { - + public func validate(_ fieldMolecules: [String: FormFieldProtocol],_ previousFieldValidity: [String: Bool]) -> (valid: Bool, fieldValidity: [String: Bool]) { + + var previousValidity: [String: Bool] = [:] for formKey in fields { guard let formField = fieldMolecules[formKey] else { continue } - - if isValid(formField) { - return true + + var fieldValidity = isValid(formField) + // If past rule is invalid for a field, the current rule should not flip the validity of a field + if let validity = previousFieldValidity[formKey], !validity, fieldValidity { + fieldValidity = false } + + if fieldValidity { + return (fieldValidity, previousValidity) + } + previousValidity[formKey] = false } - return false + return (valid: false, fieldValidity: previousValidity) } } diff --git a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAnyValueChangedModel.swift b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAnyValueChangedModel.swift index 07cf451f..2a982e45 100644 --- a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAnyValueChangedModel.swift +++ b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAnyValueChangedModel.swift @@ -27,13 +27,21 @@ public class RuleAnyValueChangedModel: RulesProtocol { return formField.baseValue != formField.formFieldValue() } - public func validate(_ fieldMolecules: [String: FormFieldProtocol]) -> Bool { + public func validate(_ fieldMolecules: [String: FormFieldProtocol],_ previousFieldValidity: [String: Bool]) -> (valid: Bool, fieldValidity: [String: Bool]) { + var previousValidity: [String: Bool] = [:] for formKey in fields { guard let formField = fieldMolecules[formKey] else { continue } - if isValid(formField) { - return true + var fieldValidity = isValid(formField) + // If past rule is invalid forr a field, the current rule should not flip the validity of a field + if let validity = previousFieldValidity[formKey], !validity, fieldValidity { + fieldValidity = false } - } - return false + + if fieldValidity { + return (true, previousValidity) + } + previousValidity[formKey] = false + } + return (valid: false, fieldValidity: previousValidity) } } diff --git a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleEqualsIgnoreCaseModel.swift b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleEqualsIgnoreCaseModel.swift index 5e014cff..8326cf4f 100644 --- a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleEqualsIgnoreCaseModel.swift +++ b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleEqualsIgnoreCaseModel.swift @@ -27,10 +27,11 @@ public class RuleEqualsIgnoreCaseModel: RulesProtocol { return false } - public func validate(_ fieldMolecules: [String: FormFieldProtocol]) -> Bool { - var valid = false - var compareText: String? + public func validate(_ fieldMolecules: [String: FormFieldProtocol],_ previousFieldValidity: [String: Bool]) -> (valid: Bool, fieldValidity: [String: Bool]) { + var valid = false + var compareText: String? + var previousValidity: [String: Bool] = [:] for formKey in fields { guard let formField = fieldMolecules[formKey] else { continue } @@ -42,16 +43,23 @@ public class RuleEqualsIgnoreCaseModel: RulesProtocol { if let fieldValue = formField.formFieldValue() as? String, compareString.caseInsensitiveCompare(fieldValue) == .orderedSame { valid = true + + var fieldValidity = valid + // If past rule is invalid for a field, the current rule should not flip the validity of a field + if let validity = previousFieldValidity[formKey], !validity, fieldValidity { + fieldValidity = false + } + for formKey in fields { guard let formField = fieldMolecules[formKey] else { continue } (formField as? FormRuleWatcherFieldProtocol)?.setValidity(true, rule: self) } break } - + + previousValidity[formKey] = valid (formField as? FormRuleWatcherFieldProtocol)?.setValidity(valid, rule: self) } - - return valid + return (valid: valid, fieldValidity: previousValidity) } } diff --git a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleEqualsModel.swift b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleEqualsModel.swift index fa73ed51..a405e2e7 100644 --- a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleEqualsModel.swift +++ b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleEqualsModel.swift @@ -27,9 +27,10 @@ public class RuleEqualsModel: RulesProtocol { return false } - public func validate(_ fieldMolecules: [String: FormFieldProtocol]) -> Bool { - var valid = true - var compareValue: AnyHashable? + public func validate(_ fieldMolecules: [String: FormFieldProtocol],_ previousFieldValidity: [String: Bool]) -> (valid: Bool, fieldValidity: [String: Bool]) { + var valid = true + var compareValue: AnyHashable? + var previousValidity: [String: Bool] = [:] for formKey in fields { guard let formField = fieldMolecules[formKey] else { continue } @@ -41,13 +42,18 @@ public class RuleEqualsModel: RulesProtocol { if compareValue != formField.formFieldValue() { valid = false + previousValidity[formKey] = valid (formField as? FormRuleWatcherFieldProtocol)?.setValidity(valid, rule: self) break } else { - (formField as? FormRuleWatcherFieldProtocol)?.setValidity(valid, rule: self) + var fieldValidity = valid + // If past rule is invalid for a field, the current rule should not flip the validity of a field + if let validity = previousFieldValidity[formKey], !validity, fieldValidity { + fieldValidity = false + } + (formField as? FormRuleWatcherFieldProtocol)?.setValidity(fieldValidity, rule: self) } } - - return valid + return (valid: valid, fieldValidity: previousValidity) } } diff --git a/MVMCoreUI/FormUIHelpers/Rules/Rules/RulesProtocol.swift b/MVMCoreUI/FormUIHelpers/Rules/Rules/RulesProtocol.swift index 5a8b47a8..db8614ac 100644 --- a/MVMCoreUI/FormUIHelpers/Rules/Rules/RulesProtocol.swift +++ b/MVMCoreUI/FormUIHelpers/Rules/Rules/RulesProtocol.swift @@ -26,7 +26,7 @@ public protocol RulesProtocol: ModelProtocol { func isValid(_ formField: FormFieldProtocol) -> Bool // Validates the rule and returns the result. - func validate(_ fieldMolecules: [String: FormFieldProtocol]) -> Bool + func validate(_ fieldMolecules: [String: FormFieldProtocol],_ previousFieldValidity: [String: Bool]) -> (valid: Bool, fieldValidity: [String: Bool]) } public extension RulesProtocol { @@ -38,14 +38,21 @@ public extension RulesProtocol { static var categoryName: String { "\(RulesProtocol.self)" } // Individual rule can override the function to validate based on the rule type. - func validate(_ fieldMolecules: [String: FormFieldProtocol]) -> Bool { - var valid = true - for formKey in fields { - guard let formField = fieldMolecules[formKey] else { continue } - let fieldValidity = isValid(formField) - (formField as? FormRuleWatcherFieldProtocol)?.setValidity(fieldValidity, rule: self) - valid = valid && fieldValidity - } - return valid + func validate(_ fieldMolecules: [String: FormFieldProtocol],_ previousFieldValidity: [String: Bool]) -> (valid: Bool, fieldValidity: [String: Bool]) { + var valid = true + var previousValidity: [String: Bool] = [:] + for formKey in fields { + guard let formField = fieldMolecules[formKey] else { continue } + + var fieldValidity = isValid(formField) + // If past rule is invalid for a field, the current rule should not flip the validity of a field + if let validity = previousFieldValidity[formKey], !validity, fieldValidity { + fieldValidity = false + } + (formField as? FormRuleWatcherFieldProtocol)?.setValidity(fieldValidity, rule: self) + valid = valid && fieldValidity + previousValidity[formKey] = fieldValidity + } + return (valid: valid, fieldValidity: previousValidity) } } diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertExpandableView.m b/MVMCoreUI/TopAlert/MVMCoreUITopAlertExpandableView.m index 46ecd793..405dd781 100644 --- a/MVMCoreUI/TopAlert/MVMCoreUITopAlertExpandableView.m +++ b/MVMCoreUI/TopAlert/MVMCoreUITopAlertExpandableView.m @@ -423,7 +423,7 @@ } - (void)accessibilityFocusChanged:(NSNotification *)notification { - if (![MVMCoreUIUtility viewContainsAccessiblityFocus:self]) { + if (notification.userInfo[UIAccessibilityFocusedElementKey] && ![MVMCoreUIUtility viewContainsAccessiblityFocus:self]) { [[NSNotificationCenter defaultCenter] removeObserver:self name:UIAccessibilityElementFocusedNotification object:nil]; [self collapse]; } diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.m b/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.m index ad0a833f..eba4cfcc 100644 --- a/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.m +++ b/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.m @@ -335,7 +335,7 @@ NSString * const MFAccTopAlertClosed = @"Top alert notification is closed."; /// If the voice over user leaves top alert focus, hide. - (void)accessibilityFocusChanged:(NSNotification *)notification { - if (![MVMCoreUIUtility viewContainsAccessiblityFocus:self]) { + if (notification.userInfo[UIAccessibilityFocusedElementKey] && ![MVMCoreUIUtility viewContainsAccessiblityFocus:self]) { [[NSNotificationCenter defaultCenter] removeObserver:self name:UIAccessibilityElementFocusedNotification object:nil]; [self hideAlertView:YES completionHandler:self.hideCompletionHandler]; self.hideCompletionHandler = nil;