diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index b67f6235..404ae487 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -318,6 +318,7 @@ D224799B231965AD003FCCF9 /* AccordionMoleculeTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D224799A231965AD003FCCF9 /* AccordionMoleculeTableViewCell.swift */; }; D22D8393241C27B100D3DF69 /* TemplateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22D8392241C27B100D3DF69 /* TemplateModel.swift */; }; D22D8395241FB41200D3DF69 /* UIStackView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22D8394241FB41200D3DF69 /* UIStackView+Extension.swift */; }; + D23118B325124E18001C8440 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23118B225124E18001C8440 /* Notification.swift */; }; D2351C7A24A4D433007DF0BC /* ListRightVariableToggleAllTextAndLinksModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2351C7924A4D433007DF0BC /* ListRightVariableToggleAllTextAndLinksModel.swift */; }; D2351C7C24A4D4C3007DF0BC /* ListRightVariableToggleAllTextAndLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2351C7B24A4D4C3007DF0BC /* ListRightVariableToggleAllTextAndLinks.swift */; }; D236E5B4241FEB1000C38625 /* ListTwoColumnPriceDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = D236E5B2241FEB1000C38625 /* ListTwoColumnPriceDescription.swift */; }; @@ -481,8 +482,12 @@ D2E2A99F23E07F8A000B42E6 /* PillButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E2A99E23E07F8A000B42E6 /* PillButton.swift */; }; D2E2A9A123E095AB000B42E6 /* ButtonModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E2A9A023E095AB000B42E6 /* ButtonModelProtocol.swift */; }; D2E2A9A323E096B1000B42E6 /* DisableableModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E2A9A223E096B1000B42E6 /* DisableableModelProtocol.swift */; }; + D2FA83D22513EA6900564112 /* NotificationXButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FA83D12513EA6900564112 /* NotificationXButton.swift */; }; + D2FA83D42514F80C00564112 /* CollapsableNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FA83D32514F80C00564112 /* CollapsableNotification.swift */; }; + D2FA83D62515021F00564112 /* CollapsableNotificationTopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FA83D52515021F00564112 /* CollapsableNotificationTopView.swift */; }; D2FB151B23A2B65B00C20E10 /* MoleculeContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FB151A23A2B65B00C20E10 /* MoleculeContainer.swift */; }; D2FB151D23A40F1500C20E10 /* MoleculeStackItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FB151C23A40F1500C20E10 /* MoleculeStackItem.swift */; }; + D2FD4A4925199BD9000C28A9 /* AccessibilityProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FD4A4825199BD9000C28A9 /* AccessibilityProtocol.swift */; }; DB06250B2293456500B72DD3 /* LeftRightLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB06250A2293456500B72DD3 /* LeftRightLabelView.swift */; }; DBC4391822442197001AB423 /* CaretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC4391622442196001AB423 /* CaretView.swift */; }; DBC4391922442197001AB423 /* DashLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC4391722442197001AB423 /* DashLine.swift */; }; @@ -806,6 +811,7 @@ D224799A231965AD003FCCF9 /* AccordionMoleculeTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccordionMoleculeTableViewCell.swift; sourceTree = ""; }; D22D8392241C27B100D3DF69 /* TemplateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateModel.swift; sourceTree = ""; }; D22D8394241FB41200D3DF69 /* UIStackView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIStackView+Extension.swift"; sourceTree = ""; }; + D23118B225124E18001C8440 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; D2351C7924A4D433007DF0BC /* ListRightVariableToggleAllTextAndLinksModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRightVariableToggleAllTextAndLinksModel.swift; sourceTree = ""; }; D2351C7B24A4D4C3007DF0BC /* ListRightVariableToggleAllTextAndLinks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRightVariableToggleAllTextAndLinks.swift; sourceTree = ""; }; D236E5B2241FEB1000C38625 /* ListTwoColumnPriceDescription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListTwoColumnPriceDescription.swift; sourceTree = ""; }; @@ -970,8 +976,12 @@ D2E2A99E23E07F8A000B42E6 /* PillButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillButton.swift; sourceTree = ""; }; D2E2A9A023E095AB000B42E6 /* ButtonModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonModelProtocol.swift; sourceTree = ""; }; D2E2A9A223E096B1000B42E6 /* DisableableModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisableableModelProtocol.swift; sourceTree = ""; }; + D2FA83D12513EA6900564112 /* NotificationXButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationXButton.swift; sourceTree = ""; }; + D2FA83D32514F80C00564112 /* CollapsableNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsableNotification.swift; sourceTree = ""; }; + D2FA83D52515021F00564112 /* CollapsableNotificationTopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsableNotificationTopView.swift; sourceTree = ""; }; D2FB151A23A2B65B00C20E10 /* MoleculeContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoleculeContainer.swift; sourceTree = ""; }; D2FB151C23A40F1500C20E10 /* MoleculeStackItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoleculeStackItem.swift; sourceTree = ""; }; + D2FD4A4825199BD9000C28A9 /* AccessibilityProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityProtocol.swift; sourceTree = ""; }; DB06250A2293456500B72DD3 /* LeftRightLabelView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LeftRightLabelView.swift; sourceTree = ""; }; DB891E822253FA8500022516 /* Label.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Label.swift; sourceTree = ""; }; DBC4391622442196001AB423 /* CaretView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CaretView.swift; sourceTree = ""; }; @@ -1059,6 +1069,7 @@ children = ( D21B7F72243BAC6800051ABF /* CollectionItemModelProtocol.swift */, 0A5D59C123AD2F5700EFD9E9 /* AppleGuidelinesProtocol.swift */, + D2FD4A4825199BD9000C28A9 /* AccessibilityProtocol.swift */, ); path = Protocols; sourceTree = ""; @@ -2077,9 +2088,13 @@ isa = PBXGroup; children = ( D2CAC7CA251104E100C75681 /* NotificationXButtonModel.swift */, + D2FA83D12513EA6900564112 /* NotificationXButton.swift */, D2CAC7CC251104FE00C75681 /* NotificationModel.swift */, + D23118B225124E18001C8440 /* Notification.swift */, D2CAC7D02511058C00C75681 /* MVMCoreUITopAlertMainView+Extension.swift */, D2CAC7CE2511052300C75681 /* CollapsableNotificationModel.swift */, + D2FA83D32514F80C00564112 /* CollapsableNotification.swift */, + D2FA83D52515021F00564112 /* CollapsableNotificationTopView.swift */, D2CAC7D2251105A700C75681 /* MVMCoreUITopAlertExpandableView+Extension.swift */, ); path = TopNotification; @@ -2388,6 +2403,7 @@ AA633B3124989EC000731E80 /* HeadersH2PricingTwoRowsModel.swift in Sources */, 8DEFA95C243DAC20000D27E5 /* ListThreeColumnDataUsageDividerModel.swift in Sources */, D2092357244FA1EF0044AD09 /* ThreeLayerModelBase.swift in Sources */, + D2FD4A4925199BD9000C28A9 /* AccessibilityProtocol.swift in Sources */, D2CAC7CD251104FE00C75681 /* NotificationModel.swift in Sources */, 0A1B4A96233BB18F005B3FB4 /* CheckboxLabel.swift in Sources */, D20923592450ECE00044AD09 /* TableView.swift in Sources */, @@ -2460,6 +2476,7 @@ C6FA7D5323C77A4A00A3614A /* StringAndMoleculeStack.swift in Sources */, 32F8804624765C6E00C2ACB3 /* ListLeftVariableNumberedListAllTextAndLinksModel.swift in Sources */, 011D958524042432000E3791 /* RulesProtocol.swift in Sources */, + D23118B325124E18001C8440 /* Notification.swift in Sources */, AA9972502475309F00FC7472 /* ListLeftVariableIconAllTextLinksModel.swift in Sources */, AA69AAF62445BF5700AF3D3B /* ListLeftVariableCheckboxBodyText.swift in Sources */, D264FAA3243E632F00D98315 /* ProgrammaticCollectionViewController.swift in Sources */, @@ -2515,6 +2532,7 @@ D2A92882241AAB67004E01C6 /* ScrollingViewController.swift in Sources */, C695A67F23C9830600BFB94E /* UnOrderedListModel.swift in Sources */, 0AE98BB523FF18D2004C5109 /* Arrow.swift in Sources */, + D2FA83D22513EA6900564112 /* NotificationXButton.swift in Sources */, D2D90B442404789000DD6EC9 /* MoleculeContainerProtocol.swift in Sources */, 0A7ECC5F243CEB1200C828E8 /* ColorViewWithLabel.swift in Sources */, 94C0150A24215643005811A9 /* ActionTopAlertModel.swift in Sources */, @@ -2560,6 +2578,7 @@ AAB8549824DC01BD00477C40 /* ListThreeColumnBillHistoryDividerModel.swift in Sources */, D20A9A5E2243D3E300ADE781 /* TwoButtonView.swift in Sources */, D2B1E3E522F37D6A0065F95C /* ImageHeadlineBody.swift in Sources */, + D2FA83D62515021F00564112 /* CollapsableNotificationTopView.swift in Sources */, 0A21DB94235E24ED00C160A2 /* DigitEntryField.swift in Sources */, AA56A211243C5EFC00303286 /* ListTwoColumnSubsectionDivider.swift in Sources */, D264FA8C243BCD8E00D98315 /* CollectionTemplateModel.swift in Sources */, @@ -2605,6 +2624,7 @@ C7192E7D23C301750050C2A0 /* HeadLineBodyCaretLinkImage.swift in Sources */, D29DF13221E6851E003B2FB9 /* MVMCoreUITopAlertBaseView.m in Sources */, BB1D17E2244EAA46001D2002 /* ListDeviceComplexButtonMedium.swift in Sources */, + D2FA83D42514F80C00564112 /* CollapsableNotification.swift in Sources */, 0A7EF86323D8AFA000B2AAD1 /* BaseDropdownEntryFieldModel.swift in Sources */, 0A1214A022C11A18007C7030 /* ActionDetailWithImage.swift in Sources */, D236E5B5241FEB1000C38625 /* ListTwoColumnPriceDescriptionModel.swift in Sources */, diff --git a/MVMCoreUI/Atomic/MoleculeObjectMapping.swift b/MVMCoreUI/Atomic/MoleculeObjectMapping.swift index a598b113..9ebb8cfb 100644 --- a/MVMCoreUI/Atomic/MoleculeObjectMapping.swift +++ b/MVMCoreUI/Atomic/MoleculeObjectMapping.swift @@ -233,8 +233,8 @@ import Foundation MoleculeObjectMapping.shared()?.register(viewClass: LockupsPlanSMLXL.self, viewModelClass: LockupsPlanSMLXLModel.self) // MARK: - Top Notifications - MoleculeObjectMapping.shared()?.register(viewClass: MVMCoreUITopAlertMainView.self, viewModelClass: NotificationModel.self) - MoleculeObjectMapping.shared()?.register(viewClass: MVMCoreUITopAlertExpandableView.self, viewModelClass: CollapsableNotificationModel.self) + MoleculeObjectMapping.shared()?.register(viewClass: NotificationView.self, viewModelClass: NotificationModel.self) + MoleculeObjectMapping.shared()?.register(viewClass: CollapsableNotification.self, viewModelClass: CollapsableNotificationModel.self) // MARK:- Helper models try? ModelRegistry.register(RuleRequiredModel.self) diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotification.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotification.swift new file mode 100644 index 00000000..f5181511 --- /dev/null +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotification.swift @@ -0,0 +1,182 @@ +// +// CollapsableNotification.swift +// MVMCoreUI +// +// Created by Scott Pfeil on 9/18/20. +// Copyright © 2020 Verizon Wireless. All rights reserved. +// + +import Foundation + +@objcMembers open class CollapsableNotification: View { + //-------------------------------------------------- + // MARK: - Outlets + //-------------------------------------------------- + + public let topView = CollapsableNotificationTopView() + public let bottomView = NotificationView() + public var verticalStack: UIStackView! + + //-------------------------------------------------- + // MARK: - Life Cycle + //-------------------------------------------------- + + public override func setupView() { + super.setupView() + + verticalStack = UIStackView(arrangedSubviews: [topView, bottomView]) + verticalStack.translatesAutoresizingMaskIntoConstraints = false + verticalStack.axis = .vertical + verticalStack.alignment = .fill + verticalStack.distribution = .fill + addSubview(verticalStack) + NSLayoutConstraint.constraintPinSubview(verticalStack, pinTop: true, topConstant: 0, pinBottom: true, bottomConstant: 0, pinLeft: true, leftConstant: 0, pinRight: true, rightConstant: 0) + + reset() + } + + open override func updateView(_ size: CGFloat) { + super.updateView(size) + verticalStack.updateView(size) + } + + open override func reset() { + super.reset() + verticalStack.reset() + backgroundColor = .mvmGreen() + } + + //-------------------------------------------------- + // MARK: - Molecule + //-------------------------------------------------- + + open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { + super.set(with: model, delegateObject, additionalData) + guard let model = model as? CollapsableNotificationModel else { return } + + topView.label.set(with: model.topLabel, delegateObject, additionalData) + topView.button.set(with: model.topAction, delegateObject: delegateObject, additionalData: additionalData) + topView.updateAccessibility() + + bottomView.set(with: model, delegateObject, additionalData) + + // Set initial collapse/expand state. + topView.isHidden = !model.alwaysShowTopLabel && !model.initiallyCollapsed + topView.button.isUserInteractionEnabled = model.initiallyCollapsed + bottomView.isHidden = model.initiallyCollapsed + verticalStack.layoutIfNeeded() + + if !model.initiallyCollapsed { + autoCollapse() + } + } + + open func performBlockOperation(with block: @escaping (MVMCoreBlockOperation) -> Void) { + let operation = MVMCoreBlockOperation(block: block)! + MVMCoreNavigationHandler.shared()?.addNavigationOperation(operation) + } + + /// Collapses after a delay + open func autoCollapse() { + let delay: DispatchTimeInterval = DispatchTimeInterval.seconds((model as? CollapsableNotificationModel)?.collapseTime ?? 5) + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in + // If accessibility focused, delay collapse. + guard let self = self else { return } + if MVMCoreUIUtility.viewContainsAccessiblityFocus(self) { + NotificationCenter.default.addObserver(self, selector: #selector(self.accessibilityFocusChanged(notification:)), name: UIAccessibility.elementFocusedNotification, object: nil) + } else { + self.collapse() + } + } + } + + /// Collapses to show just the top view. + open func collapse(animated: Bool = true) { + guard !bottomView.isHidden else { return } + performBlockOperation { [weak self] (operation) in + let strongSelf = self + MVMCoreDispatchUtility.performBlock(onMainThread: { + MVMCoreUITopAlertView.sharedGlobal()?.superview?.layoutIfNeeded() + let animation = { + strongSelf?.topView.isHidden = false + strongSelf?.bottomView.isHidden = true + strongSelf?.verticalStack.layoutIfNeeded() + } + let completion: (Bool) -> Void = { (finished) in + strongSelf?.topView.button.isUserInteractionEnabled = true + MVMCoreUITopAlertView.sharedGlobal()?.superview?.layoutIfNeeded() + UIAccessibility.post(notification: .layoutChanged, argument: strongSelf?.getAccessibilityLayoutChangedArgument()) + operation.markAsFinished() + } + + if animated { + UIView.animate(withDuration: 0.5, animations: animation, completion: completion) + } else { + animation() + completion(true) + } + }) + } + } + + /// Expands to show the bottom view. + open func expand(topViewShowing: Bool = false, animated: Bool = true) { + guard bottomView.isHidden else { return } + performBlockOperation { [weak self] (operation) in + let strongSelf = self + MVMCoreDispatchUtility.performBlock(onMainThread: { + MVMCoreUITopAlertView.sharedGlobal()?.superview?.layoutIfNeeded() + strongSelf?.topView.button.isUserInteractionEnabled = false + let animation = { + strongSelf?.topView.isHidden = !topViewShowing + strongSelf?.bottomView.isHidden = false + strongSelf?.verticalStack.layoutIfNeeded() + } + let completion: (Bool) -> Void = { (finished) in + MVMCoreUITopAlertView.sharedGlobal()?.superview?.layoutIfNeeded() + UIAccessibility.post(notification: .layoutChanged, argument: strongSelf?.getAccessibilityLayoutChangedArgument()) + strongSelf?.autoCollapse() + operation.markAsFinished() + } + + if animated { + UIView.animate(withDuration: 0.5, animations: animation, completion: completion) + } else { + animation() + completion(true) + } + }) + } + } + + open override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { + return 120 + } + + /// Collapse if focus is no longer on this top alert. + @objc func accessibilityFocusChanged(notification: Notification) { + if !MVMCoreUIUtility.viewContainsAccessiblityFocus(self) { + NotificationCenter.default.removeObserver(self, name: UIAccessibility.elementFocusedNotification, object: nil) + collapse() + } + } +} + +extension CollapsableNotification: StatusBarUI { + func getStatusBarUI() -> (color: UIColor, style: UIStatusBarStyle) { + let color = backgroundColor ?? UIColor.mvmGreen + var greyScale: CGFloat = 0 + topView.label.textColor.getWhite(&greyScale, alpha: nil) + return (color, greyScale > 0.5 ? .lightContent : .default) + } +} + +extension CollapsableNotification: AccessibilityProtocol { + public func getAccessibilityLayoutChangedArgument() -> Any? { + if !topView.isHidden { + return topView + } else { + return bottomView.headline + } + } +} diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotificationModel.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotificationModel.swift index 15db1009..804bfeb6 100644 --- a/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotificationModel.swift +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotificationModel.swift @@ -8,16 +8,12 @@ import Foundation -public class CollapsableNotificationModel: MoleculeModelProtocol { - public static var identifier: String = "collapsableNotification" - public var moleculeName: String = CollapsableNotificationModel.identifier - public var backgroundColor: Color? +public class CollapsableNotificationModel: NotificationModel { + public class override var identifier: String { + return "collapsableNotification" + } public var topLabel: LabelModel public var topAction: ActionModelProtocol? - public var headline: LabelModel - public var body: LabelModel? - public var button: ButtonModel? - public var closeButton: NotificationXButtonModel? public var alwaysShowTopLabel = false public var collapseTime: Int = 5 public var initiallyCollapsed = false @@ -25,18 +21,23 @@ public class CollapsableNotificationModel: MoleculeModelProtocol { init(with topLabel: LabelModel, headline: LabelModel) { self.topLabel = topLabel - self.headline = headline + super.init(with: headline) + } + + override func setDefault() { + super.setDefault() + if topLabel.textColor == nil { + topLabel.textColor = Color(uiColor: .white) + } + if topLabel.textAlignment == nil { + topLabel.textAlignment = .center + } } private enum CodingKeys: String, CodingKey { case moleculeName - case backgroundColor case topLabel case topAction - case headline - case body - case button - case closeButton case alwaysShowTopLabel case collapseTime case initiallyCollapsed @@ -46,12 +47,7 @@ public class CollapsableNotificationModel: MoleculeModelProtocol { required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) topLabel = try typeContainer.decode(LabelModel.self, forKey: .topLabel) - backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) topAction = try typeContainer.decodeModelIfPresent(codingKey: .topAction) - headline = try typeContainer.decode(LabelModel.self, forKey: .headline) - 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 alwaysShowTopLabel = try typeContainer.decodeIfPresent(Bool.self, forKey: .alwaysShowTopLabel) { self.alwaysShowTopLabel = alwaysShowTopLabel } @@ -62,18 +58,15 @@ public class CollapsableNotificationModel: MoleculeModelProtocol { self.initiallyCollapsed = initiallyCollapsed } pages = try typeContainer.decodeIfPresent([String].self, forKey: .pages) + try super.init(from: decoder) } - public func encode(to encoder: Encoder) throws { + public override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(moleculeName, forKey: .moleculeName) - try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encode(topLabel, forKey: .topLabel) try container.encodeModelIfPresent(topAction, forKey: .topAction) - try container.encode(headline, forKey: .headline) - try container.encodeIfPresent(body, forKey: .body) - try container.encodeIfPresent(button, forKey: .button) - try container.encodeIfPresent(closeButton, forKey: .closeButton) try container.encode(alwaysShowTopLabel, forKey: .alwaysShowTopLabel) try container.encode(collapseTime, forKey: .collapseTime) try container.encode(initiallyCollapsed, forKey: .initiallyCollapsed) diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotificationTopView.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotificationTopView.swift new file mode 100644 index 00000000..36ddc8a6 --- /dev/null +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotificationTopView.swift @@ -0,0 +1,63 @@ +// +// CollapsableNotificationTopView.swift +// MVMCoreUI +// +// Created by Scott Pfeil on 9/18/20. +// Copyright © 2020 Verizon Wireless. All rights reserved. +// + +import Foundation + +@objcMembers open class CollapsableNotificationTopView: View { + public let label: Label = { + let label = Label(fontStyle: .BoldBodySmall) + label.numberOfLines = 1 + label.textAlignment = .center + label.setContentHuggingPriority(.defaultHigh, for: .vertical) + return label + }() + + public let button: Button = { + let button = Button(type: .custom) + button.backgroundColor = .clear + button.setContentCompressionResistancePriority(.defaultLow, for: .vertical) + button.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + return button + }() + + public override func setupView() { + super.setupView() + + addSubview(label) + NSLayoutConstraint.constraintPinSubview(label, pinTop: true, topConstant: 0, pinBottom: true, bottomConstant: 0, pinLeft: true, leftConstant: PaddingThree, pinRight: true, rightConstant: PaddingThree) + + addSubview(button) + NSLayoutConstraint.constraintPinSubview(button, pinTop: true, topConstant: 0, pinBottom: true, bottomConstant: -5, pinLeft: true, leftConstant: 0, pinRight: true, rightConstant: 0) + + // Listen for status bar touches. + NotificationCenter.default.addObserver(self, selector: #selector(pressed(_:)), name: NSNotification.Name(rawValue: NotificationStatusBarTouched), object: nil) + } + + open override func updateView(_ size: CGFloat) { + super.updateView(size) + label.updateView(size) + } + + open override func reset() { + super.reset() + label.setFontStyle(.BoldBodySmall) + label.textColor = .white + label.textAlignment = .center + } + + open func updateAccessibility() { + isAccessibilityElement = true + accessibilityLabel = label.text + accessibilityTraits = (button.isUserInteractionEnabled && button.actionModel != nil) ? .button : .none + MVMCoreUITopAlertBaseView.amendAccesibilityLabel(for: self) + } + + @objc func pressed(_ sender: Notification) { + button.callActionBlock(button) + } +} diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/Notification.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/Notification.swift new file mode 100644 index 00000000..44ce254b --- /dev/null +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/Notification.swift @@ -0,0 +1,83 @@ +// +// Notification.swift +// MVMCoreUI +// +// Created by Scott Pfeil on 9/16/20. +// Copyright © 2020 Verizon Wireless. All rights reserved. +// + +import Foundation + +@objcMembers open class NotificationView: View { + //-------------------------------------------------- + // MARK: - Outlets + //-------------------------------------------------- + + public let headline = Label(fontStyle: .BoldBodySmall) + public let body = Label(fontStyle: .RegularBodySmall) + public let button = PillButton(asPrimaryButton: false, makeTiny: true) + public let closeButton = NotificationXButton() + public var labelStack: Stack! + public var horizontalStack: Stack! + + // Legacy constant + private static let viewHeight: CGFloat = 96.0 + + //-------------------------------------------------- + // MARK: - Life Cycle + //-------------------------------------------------- + + public override func setupView() { + super.setupView() + reset() + + labelStack = Stack.createStack(with: [headline, body], spacing: 0) + horizontalStack = Stack.createStack(with: [(view: labelStack, model: StackItemModel()),(view: button, model: StackItemModel(horizontalAlignment: .fill)),(view: closeButton, model: StackItemModel(horizontalAlignment: .fill))], axis: .horizontal) + addSubview(horizontalStack) + NSLayoutConstraint.constraintPinSubview(horizontalStack, pinTop: true, topConstant: PaddingTwo, pinBottom: true, bottomConstant: PaddingTwo, pinLeft: true, leftConstant: PaddingThree, pinRight: true, rightConstant: PaddingThree) + labelStack.restack() + horizontalStack.restack() + + heightAnchor.constraint(equalToConstant: Self.viewHeight).isActive = true + } + + open override func updateView(_ size: CGFloat) { + super.updateView(size) + horizontalStack.updateView(size) + } + + open override func reset() { + super.reset() + backgroundColor = .mvmGreen() + headline.textColor = .white + body.textColor = .white + } + + //-------------------------------------------------- + // MARK: - Molecule + //-------------------------------------------------- + + open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { + super.set(with: model, delegateObject, additionalData) + guard let model = model as? NotificationModel else { return } + labelStack.updateContainedMolecules(with: [model.headline, model.body], delegateObject, nil) + horizontalStack.updateContainedMolecules(with: [labelStack.stackModel, model.button, model.closeButton], delegateObject, nil) + updateAccessibility() + } + + open override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { + return viewHeight + } + + open func updateAccessibility() { + MVMCoreUITopAlertBaseView.amendAccesibilityLabel(for: headline) + MVMCoreUITopAlertBaseView.amendAccesibilityLabel(for: body) + MVMCoreUITopAlertBaseView.amendAccesibilityLabel(for: button) + } +} + +extension NotificationView: AccessibilityProtocol { + public func getAccessibilityLayoutChangedArgument() -> Any? { + return headline + } +} diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationModel.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationModel.swift index ac74abb6..e2f94326 100644 --- a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationModel.swift +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationModel.swift @@ -9,8 +9,9 @@ import Foundation public class NotificationModel: MoleculeModelProtocol { - public static var identifier: String = "notification" - public var moleculeName: String = NotificationModel.identifier + public class var identifier: String { + return "notification" + } public var backgroundColor: Color? public var headline: LabelModel public var body: LabelModel? @@ -20,4 +21,51 @@ public class NotificationModel: MoleculeModelProtocol { init(with headline: LabelModel) { self.headline = headline } + + func setDefault() { + if backgroundColor == nil { + backgroundColor = Color(uiColor: .mvmGreen()) + } + if headline.textColor == nil { + headline.textColor = Color(uiColor: .white) + } + if body?.textColor == nil { + body?.textColor = Color(uiColor: .white) + } + if button?.style == nil { + button?.style = .secondary + } + button?.size = .tiny + button?.enabledTextColor = Color(uiColor: .white) + button?.enabledBorderColor = Color(uiColor: .white) + } + + private enum CodingKeys: String, CodingKey { + case moleculeName + case backgroundColor + case headline + case body + case button + case closeButton + } + + required public init(from decoder: Decoder) throws { + let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) + headline = try typeContainer.decode(LabelModel.self, forKey: .headline) + body = try typeContainer.decodeIfPresent(LabelModel.self, forKey: .body) + button = try typeContainer.decodeIfPresent(ButtonModel.self, forKey: .button) + closeButton = try typeContainer.decodeIfPresent(NotificationXButtonModel.self, forKey: .closeButton) + setDefault() + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(moleculeName, forKey: .moleculeName) + try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) + try container.encode(headline, forKey: .headline) + try container.encodeIfPresent(body, forKey: .body) + try container.encodeIfPresent(button, forKey: .button) + try container.encodeIfPresent(closeButton, forKey: .closeButton) + } } diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationXButton.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationXButton.swift new file mode 100644 index 00000000..84b1abbf --- /dev/null +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationXButton.swift @@ -0,0 +1,42 @@ +// +// NotificationXButton.swift +// MVMCoreUI +// +// Created by Scott Pfeil on 9/17/20. +// Copyright © 2020 Verizon Wireless. All rights reserved. +// + +import Foundation + +@objcMembers open class NotificationXButton: Button { + + open func closeTopAlert() { + if let delegate = MVMCoreUITopAlertView.sharedGlobal()?.animationDelegate { + delegate.topAlertCloseButtonPressed() + } else { + MVMCoreUISession.sharedGlobal()?.topAlertView?.hideAlertView(true, completionHandler: nil) + } + } + + open override func setupView() { + if let image = MVMCoreUIUtility.imageNamed("nav_close")?.withRenderingMode(.alwaysTemplate) { + setImage(image, for: .normal) + } + tintColor = .white + adjustsImageWhenHighlighted = false + accessibilityLabel = MVMCoreUIUtility.hardcodedString(withKey: "AccCloseButton") + } + + 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 + + // TODO: Temporary, consider action for dismissing top alert + if model.action.actionType == ActionNoopModel.identifier { + addActionBlock(event: .touchUpInside) { (button) in + (button as? NotificationXButton)?.closeTopAlert() + } + } + } +} diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationXButtonModel.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationXButtonModel.swift index 368ec1cf..81dae17e 100644 --- a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationXButtonModel.swift +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationXButtonModel.swift @@ -8,27 +8,32 @@ import Foundation -public class NotificationXButtonModel: Codable { - public var color: Color? - public var action: ActionModelProtocol? +public class NotificationXButtonModel: ButtonModelProtocol, MoleculeModelProtocol { + public static var identifier: String = "notificationXButton" + public var backgroundColor: Color? + public var color = Color(uiColor: .white) + public var action: ActionModelProtocol = ActionNoopModel() private enum CodingKeys: String, CodingKey { + case moleculeName case color case action } - public init() { - } + public init() {} public required init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) - color = try typeContainer.decodeIfPresent(Color.self, forKey: .color) - action = try typeContainer.decodeModelIfPresent(codingKey: .action) + color = try typeContainer.decodeIfPresent(Color.self, forKey: .color) ?? Color(uiColor: .white) + if let action: ActionModelProtocol = try typeContainer.decodeModelIfPresent(codingKey: .action) { + self.action = action + } } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encodeIfPresent(color, forKey: .color) - try container.encodeModelIfPresent(action, forKey: .action) + try container.encode(moleculeName, forKey: .moleculeName) + try container.encode(color, forKey: .color) + try container.encodeModel(action, forKey: .action) } } diff --git a/MVMCoreUI/BaseClasses/Button.swift b/MVMCoreUI/BaseClasses/Button.swift index 599fc48c..b486c2c5 100644 --- a/MVMCoreUI/BaseClasses/Button.swift +++ b/MVMCoreUI/BaseClasses/Button.swift @@ -72,12 +72,12 @@ public typealias ButtonAction = (Button) -> () buttonAction?(self) } - open func set(with actionModel: ActionModelProtocol, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) { + open func set(with actionModel: ActionModelProtocol?, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) { self.actionModel = actionModel buttonDelegate = delegateObject?.buttonDelegate addActionBlock(event: .touchUpInside) { [weak self] sender in - guard let self = self else { return } + guard let self = self, let actionModel = actionModel else { return } Self.performButtonAction(with: actionModel, button: self, delegateObject: delegateObject, additionalData: additionalData) } } diff --git a/MVMCoreUI/BaseClasses/Protocols/AccessibilityProtocol.swift b/MVMCoreUI/BaseClasses/Protocols/AccessibilityProtocol.swift new file mode 100644 index 00000000..6c7ada50 --- /dev/null +++ b/MVMCoreUI/BaseClasses/Protocols/AccessibilityProtocol.swift @@ -0,0 +1,14 @@ +// +// AccessibilityProtocol.swift +// MVMCoreUI +// +// Created by Scott Pfeil on 9/21/20. +// Copyright © 2020 Verizon Wireless. All rights reserved. +// + +import Foundation + +@objc public protocol AccessibilityProtocol { + /// Should return the argument to use for posting a layout change. + func getAccessibilityLayoutChangedArgument() -> Any? +} diff --git a/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.m b/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.m index 06e44c73..288878b8 100644 --- a/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.m +++ b/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.m @@ -1073,10 +1073,13 @@ CGFloat const PanelAnimationDuration = 0.2; } - (UIViewController *)getCurrentDetailViewController { - UIViewController *viewController = self.navigationController.topViewController; - if ([viewController conformsToProtocol:@protocol(MVMCoreViewManagerProtocol)]) { - viewController = [viewController performSelector:@selector(getCurrentViewController)]; - } + __block UIViewController *viewController = nil; + [MVMCoreDispatchUtility performSyncBlockOnMainThread:^{ + viewController = self.navigationController.topViewController; + if ([viewController conformsToProtocol:@protocol(MVMCoreViewManagerProtocol)]) { + viewController = [viewController performSelector:@selector(getCurrentViewController)]; + } + }]; return viewController; } diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertView+Extension.swift b/MVMCoreUI/TopAlert/MVMCoreUITopAlertView+Extension.swift index e6df7178..8bf4860f 100644 --- a/MVMCoreUI/TopAlert/MVMCoreUITopAlertView+Extension.swift +++ b/MVMCoreUI/TopAlert/MVMCoreUITopAlertView+Extension.swift @@ -56,21 +56,22 @@ public extension MVMCoreUITopAlertView { } /// Returns the top alert molecule to use and status bar color legacy style. - @objc func molecule(for topAlertObject: MVMCoreTopAlertObject, statusBarColor: AutoreleasingUnsafeMutablePointer?, statusBarStyle: UnsafeMutablePointer?) -> MVMCoreUITopAlertBaseView? { + @objc func molecule(for topAlertObject: MVMCoreTopAlertObject, statusBarColor: AutoreleasingUnsafeMutablePointer?, statusBarStyle: UnsafeMutablePointer?) -> UIView? { do { let delegateObject = MVMCoreUIDelegateObject.create(withDelegateForAll: self) guard let json = topAlertObject.json else { return nil } let model = try TopNotificationModel.decode(json: json, delegateObject: delegateObject) - guard let molecule = MoleculeObjectMapping.shared()?.createMolecule(model.molecule, delegateObject: delegateObject, additionalData: nil), - let view = molecule as? MVMCoreUITopAlertBaseView else { - throw ModelRegistry.Error.decoderOther(message: "Molecule not a top alert") + guard let molecule = MoleculeObjectMapping.shared()?.createMolecule(model.molecule, delegateObject: delegateObject, additionalData: nil) else { + throw ModelRegistry.Error.decoderOther(message: "Molecule not mapped") } - if let castView = view as? StatusBarUI { + if let castView = molecule as? StatusBarUI { let (color, style) = castView.getStatusBarUI() statusBarColor?.pointee = color statusBarStyle?.pointee = style } - return view + // TODO: Temporary, waiting for actual restriction from design. + molecule.heightAnchor.constraint(lessThanOrEqualToConstant: 140).isActive = true + return molecule } catch { if let errorObject = MVMCoreErrorObject.createErrorObject(for: error, location: "\(self)") { MVMCoreUILoggingHandler.shared()?.addError(toLog: errorObject) diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.h b/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.h index 4b757bf7..82c0a7e9 100644 --- a/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.h +++ b/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.h @@ -16,7 +16,6 @@ #import @class MVMCoreTopAlertObject; -@class MVMCoreUITopAlertBaseView; @interface MVMCoreUITopAlertView : UIView @@ -45,7 +44,7 @@ - (void)resetDefaultBackgroundColor:(nullable UIColor *)backgroundColor basedOnStatusBarStyle:(UIStatusBarStyle)style; // Can be subclassed for custom views. -- (nonnull MVMCoreUITopAlertBaseView *)topAlertViewForTopAlertObject:(nullable MVMCoreTopAlertObject *)topAlertObject animationDelegate:(nonnull id )animationDelegate statusBarColor:(UIColor *_Nullable *_Nullable)statusBarColor statusBarStyle:(UIStatusBarStyle *_Nullable)statusBarStyle; +- (nonnull UIView *)topAlertViewForTopAlertObject:(nullable MVMCoreTopAlertObject *)topAlertObject animationDelegate:(nonnull id )animationDelegate statusBarColor:(UIColor *_Nullable *_Nullable)statusBarColor statusBarStyle:(UIStatusBarStyle *_Nullable)statusBarStyle; /// Get the background color based on the type - (nonnull UIColor *)getBackgroundColorForType:(nullable NSString *)type; diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.m b/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.m index 903a24d3..31899d3f 100644 --- a/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.m +++ b/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.m @@ -124,7 +124,7 @@ NSString * const MFAccTopAlertClosed = @"Top alert notification is closed."; } } -- (nonnull MVMCoreUITopAlertBaseView *)topAlertViewForTopAlertObject:(nullable MVMCoreTopAlertObject *)topAlertObject animationDelegate:(nonnull id )animationDelegate statusBarColor:(UIColor *_Nullable *_Nullable)statusBarColor statusBarStyle:(UIStatusBarStyle *_Nullable)statusBarStyle { +- (nonnull UIView *)topAlertViewForTopAlertObject:(nullable MVMCoreTopAlertObject *)topAlertObject animationDelegate:(nonnull id )animationDelegate statusBarColor:(UIColor *_Nullable *_Nullable)statusBarColor statusBarStyle:(UIStatusBarStyle *_Nullable)statusBarStyle { if (topAlertObject.json) { return [self moleculeFor:topAlertObject statusBarColor:statusBarColor statusBarStyle:statusBarStyle]; } else { @@ -169,8 +169,10 @@ NSString * const MFAccTopAlertClosed = @"Top alert notification is closed."; UIColor *statusBarColor = nil; UIStatusBarStyle statusBarStyle = UIStatusBarStyleDefault; - MVMCoreUITopAlertBaseView *view = [self topAlertViewForTopAlertObject:topAlertObject animationDelegate:animationDelegate statusBarColor:&statusBarColor statusBarStyle:&statusBarStyle]; - [view updateView:CGRectGetWidth(self.bounds)]; + UIView *view = [self topAlertViewForTopAlertObject:topAlertObject animationDelegate:animationDelegate statusBarColor:&statusBarColor statusBarStyle:&statusBarStyle]; + if ([view conformsToProtocol:@protocol(MVMCoreViewProtocol)]) { + [((UIView *)view) updateView:CGRectGetWidth(self.bounds)]; + } if (!statusBarColor) { statusBarColor = [UIColor whiteColor]; } @@ -185,7 +187,20 @@ NSString * const MFAccTopAlertClosed = @"Top alert notification is closed."; [[MVMCoreUISession sharedGlobal].splitViewController.parentViewController setNeedsStatusBarAppearanceUpdate]; } -- (void)showAlertView:(nullable MVMCoreUITopAlertBaseView *)view topAlertObject:(nonnull MVMCoreTopAlertObject *)topAlertObject completionHandler:(void (^ __nullable)(BOOL finished))completionHandler { +- (void)updateAccessibilityForTopAlert:(nullable UIView *)view { + // Update accessibility with top alert + if ([view isKindOfClass:[MVMCoreUITopAlertBaseView class]]) { + [((MVMCoreUITopAlertBaseView *)view) handleAccessibility]; + } else { + id accessibilityArgument = view; + if ([view conformsToProtocol:@protocol(AccessibilityProtocol)]) { + accessibilityArgument = [((id )view) getAccessibilityLayoutChangedArgument]; + } + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, accessibilityArgument); + } +} + +- (void)showAlertView:(nullable UIView *)view topAlertObject:(nonnull MVMCoreTopAlertObject *)topAlertObject completionHandler:(void (^ __nullable)(BOOL finished))completionHandler { __weak typeof(self) weakSelf = self; MVMCoreBlockOperation *operation = [MVMCoreBlockOperation blockOperationWithBlock:^(MVMCoreBlockOperation * _Nonnull operation) { @@ -205,8 +220,9 @@ NSString * const MFAccTopAlertClosed = @"Top alert notification is closed."; } completion:^(BOOL finished) { [weakSelf.superview layoutIfNeeded]; [weakSelf.animationDelegate topAlertViewFinishAnimation]; - [view handleAccessibility]; + [weakSelf updateAccessibilityForTopAlert:view]; + [MVMCoreDispatchUtility performBlockInBackground:^{ if ([weakSelf.topAlertObject.delegate respondsToSelector:@selector(topAlertViewShown:topAlertObject:)]) { [weakSelf.topAlertObject.delegate topAlertViewShown:view topAlertObject:topAlertObject]; diff --git a/MVMCoreUI/Utility/MVMCoreUIUtility.m b/MVMCoreUI/Utility/MVMCoreUIUtility.m index 60ff4872..fed0bb30 100644 --- a/MVMCoreUI/Utility/MVMCoreUIUtility.m +++ b/MVMCoreUI/Utility/MVMCoreUIUtility.m @@ -89,7 +89,11 @@ if (![focusedElement isKindOfClass:[UIView class]]) { return NO; } - return [(UIView *)focusedElement isDescendantOfView:view]; + __block BOOL containsFocus; + [MVMCoreDispatchUtility performSyncBlockOnMainThread:^{ + containsFocus = [(UIView *)focusedElement isDescendantOfView:view]; + }]; + return containsFocus; } #pragma mark - Setters