diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index b67f6235..de7e8645 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,6 +482,9 @@ 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 /* NotificationStatusBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FA83D52515021F00564112 /* NotificationStatusBar.swift */; }; D2FB151B23A2B65B00C20E10 /* MoleculeContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FB151A23A2B65B00C20E10 /* MoleculeContainer.swift */; }; D2FB151D23A40F1500C20E10 /* MoleculeStackItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FB151C23A40F1500C20E10 /* MoleculeStackItem.swift */; }; DB06250B2293456500B72DD3 /* LeftRightLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB06250A2293456500B72DD3 /* LeftRightLabelView.swift */; }; @@ -806,6 +810,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,6 +975,9 @@ 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 /* NotificationStatusBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationStatusBar.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 = ""; }; DB06250A2293456500B72DD3 /* LeftRightLabelView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LeftRightLabelView.swift; sourceTree = ""; }; @@ -2077,9 +2085,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 /* NotificationStatusBar.swift */, D2CAC7D2251105A700C75681 /* MVMCoreUITopAlertExpandableView+Extension.swift */, ); path = TopNotification; @@ -2460,6 +2472,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 +2528,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 +2574,7 @@ AAB8549824DC01BD00477C40 /* ListThreeColumnBillHistoryDividerModel.swift in Sources */, D20A9A5E2243D3E300ADE781 /* TwoButtonView.swift in Sources */, D2B1E3E522F37D6A0065F95C /* ImageHeadlineBody.swift in Sources */, + D2FA83D62515021F00564112 /* NotificationStatusBar.swift in Sources */, 0A21DB94235E24ED00C160A2 /* DigitEntryField.swift in Sources */, AA56A211243C5EFC00303286 /* ListTwoColumnSubsectionDivider.swift in Sources */, D264FA8C243BCD8E00D98315 /* CollectionTemplateModel.swift in Sources */, @@ -2605,6 +2620,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..54eeb51a --- /dev/null +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotification.swift @@ -0,0 +1,168 @@ +// +// 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 = NotificationStatusBar() + 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) + bottomView.set(with: model, delegateObject, additionalData) + updateAccessibilityLabel() + + topView.isHidden = !model.alwaysShowTopLabel && !model.initiallyCollapsed + topView.button.isUserInteractionEnabled = model.initiallyCollapsed + bottomView.isHidden = model.initiallyCollapsed + verticalStack.layoutIfNeeded() + + if !model.initiallyCollapsed { + collapse(with: .now() + DispatchTimeInterval.seconds(model.collapseTime)) + } + } + + open func collapse(with delay: DispatchTime) { + DispatchQueue.main.asyncAfter(deadline: delay) { [weak self] in + self?.collapse() + } + } + + open func collapse(animated: Bool = true) { + let animation = { [weak self] in + self?.topView.isHidden = false + self?.bottomView.isHidden = true + self?.verticalStack.layoutIfNeeded() + } + if animated { + UIView.animate(withDuration: 0.5, animations: animation) { [weak self] (finished) in + self?.topView.button.isUserInteractionEnabled = true + } + } else { + animation() + } + } + + open func expand(topViewShowing: Bool = false, animated: Bool = true) { + topView.button.isUserInteractionEnabled = false + let animation = { [weak self] in + self?.topView.isHidden = !topViewShowing + self?.bottomView.isHidden = false + self?.verticalStack.layoutIfNeeded() + } + if animated { + UIView.animate(withDuration: 0.5, animations: animation) { [weak self] (finished) in + + } + } else { + animation() + } + } + + open override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { + return 96 + } + /* + func getAccessibilityMessage() -> String? { + + guard let leftImageLabel = leftImage.imageView.accessibilityLabel else { + return eyebrowHeadlineBodyLink.getAccessibilityMessage() + } + + guard let label = eyebrowHeadlineBodyLink.getAccessibilityMessage() else { + return leftImageLabel + } + + return leftImageLabel + ", " + label + }*/ + + func updateAccessibilityLabel() { + /*headline.accessibilityLabel = headline.text + MVMCoreUITopAlertBaseView.amendAccesibilityLabel(for: headline) + + body.accessibilityLabel = body.text + MVMCoreUITopAlertBaseView.amendAccesibilityLabel(for: body) + + + let linkShowing = eyebrowHeadlineBodyLink.link.titleLabel?.text?.count ?? 0 > 0 + isAccessibilityElement = !linkShowing + accessibilityTraits = (isAccessibilityElement && accessoryView != nil) ? .button : .none + + if !linkShowing { + // Make whole cell focusable if no link. + accessibilityLabel = getAccessibilityMessage() + } else if let accessoryView = accessoryView { + // Both caret and link. Read all content on caret. + accessoryView.accessibilityLabel = getAccessibilityMessage() + accessibilityElements = [accessoryView, eyebrowHeadlineBodyLink.link] + } else { + // Only link. Manually add accessibility elements to ensure they are read in the right order. + var elements: [Any] = [] + + if let leftImageLabel = leftImage.imageView.accessibilityLabel, !leftImageLabel.isEmpty { + elements.append(leftImage.imageView) + } + + if let otherElements = eyebrowHeadlineBodyLink.getAccessibilityElements() { + elements.append(otherElements) + } + + accessibilityElements = elements + }*/ + } +} + +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) + } +} 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/Notification.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/Notification.swift new file mode 100644 index 00000000..823158fc --- /dev/null +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/Notification.swift @@ -0,0 +1,117 @@ +// +// 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! + + //-------------------------------------------------- + // 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: 96).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) + + updateAccessibilityLabel() + } + + open override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { + return 96 + } + /* + func getAccessibilityMessage() -> String? { + + guard let leftImageLabel = leftImage.imageView.accessibilityLabel else { + return eyebrowHeadlineBodyLink.getAccessibilityMessage() + } + + guard let label = eyebrowHeadlineBodyLink.getAccessibilityMessage() else { + return leftImageLabel + } + + return leftImageLabel + ", " + label + }*/ + + func updateAccessibilityLabel() { + /*headline.accessibilityLabel = headline.text + MVMCoreUITopAlertBaseView.amendAccesibilityLabel(for: headline) + + body.accessibilityLabel = body.text + MVMCoreUITopAlertBaseView.amendAccesibilityLabel(for: body) + + + let linkShowing = eyebrowHeadlineBodyLink.link.titleLabel?.text?.count ?? 0 > 0 + isAccessibilityElement = !linkShowing + accessibilityTraits = (isAccessibilityElement && accessoryView != nil) ? .button : .none + + if !linkShowing { + // Make whole cell focusable if no link. + accessibilityLabel = getAccessibilityMessage() + } else if let accessoryView = accessoryView { + // Both caret and link. Read all content on caret. + accessoryView.accessibilityLabel = getAccessibilityMessage() + accessibilityElements = [accessoryView, eyebrowHeadlineBodyLink.link] + } else { + // Only link. Manually add accessibility elements to ensure they are read in the right order. + var elements: [Any] = [] + + if let leftImageLabel = leftImage.imageView.accessibilityLabel, !leftImageLabel.isEmpty { + elements.append(leftImage.imageView) + } + + if let otherElements = eyebrowHeadlineBodyLink.getAccessibilityElements() { + elements.append(otherElements) + } + + accessibilityElements = elements + }*/ + } +} 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/NotificationStatusBar.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationStatusBar.swift new file mode 100644 index 00000000..73bde3f1 --- /dev/null +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationStatusBar.swift @@ -0,0 +1,56 @@ +// +// NotificationStatusBar.swift +// MVMCoreUI +// +// Created by Scott Pfeil on 9/18/20. +// Copyright © 2020 Verizon Wireless. All rights reserved. +// + +import Foundation + +@objcMembers open class NotificationStatusBar: 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 + } + + @objc func pressed(_ sender: Notification) { + button.callActionBlock(button) + } +} diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationXButton.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationXButton.swift new file mode 100644 index 00000000..434ddc3d --- /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 + + // temporary + 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 757855ed..535d3830 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/TopAlert/MVMCoreUITopAlertView+Extension.swift b/MVMCoreUI/TopAlert/MVMCoreUITopAlertView+Extension.swift index e6df7178..09cef092 100644 --- a/MVMCoreUI/TopAlert/MVMCoreUITopAlertView+Extension.swift +++ b/MVMCoreUI/TopAlert/MVMCoreUITopAlertView+Extension.swift @@ -56,21 +56,23 @@ 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 { + 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") } - 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 + // Temporary + molecule.heightAnchor.constraint(lessThanOrEqualToConstant: 150).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..570b9297 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,7 @@ 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)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 +207,10 @@ NSString * const MFAccTopAlertClosed = @"Top alert notification is closed."; } completion:^(BOOL finished) { [weakSelf.superview layoutIfNeeded]; [weakSelf.animationDelegate topAlertViewFinishAnimation]; - [view handleAccessibility]; - + if ([view isKindOfClass:[MVMCoreUITopAlertBaseView class]]) { + [((MVMCoreUITopAlertBaseView *)view) handleAccessibility]; + } + [MVMCoreDispatchUtility performBlockInBackground:^{ if ([weakSelf.topAlertObject.delegate respondsToSelector:@selector(topAlertViewShown:topAlertObject:)]) { [weakSelf.topAlertObject.delegate topAlertViewShown:view topAlertObject:topAlertObject];