// // Notification.swift // VDS // // Created by Nadigadda, Sumanth on 14/03/23. // import Foundation import UIKit import VDSColorTokens import Combine @objc(VDSNotification) /// A VDS Component that will render a view with information open class Notification: View { //-------------------------------------------------- // MARK: - Enums //-------------------------------------------------- public enum Style: String, CaseIterable { case info, success, warning, error func styleIconName() -> Icon.Name { switch self { case .info: return .infoBold case .success: return .checkmarkAltBold case .warning: return .warningBold case .error: return .errorBold } } } public enum Layout: String, CaseIterable { case vertical, horizontal } //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- private var mainStackView = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.alignment = .top $0.axis = .horizontal $0.spacing = VDSLayout.Spacing.space2X.value } private var labelsView = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.alignment = .top $0.axis = .vertical } private var labelButtonView = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.alignment = .top $0.distribution = .fillEqually $0.axis = .vertical $0.spacing = VDSLayout.Spacing.space2X.value } private var edgeSpacing: CGFloat { return UIDevice.isIPad ? VDSLayout.Spacing.space5X.value : VDSLayout.Spacing.space4X.value } private var minViewHeight: CGFloat { return UIDevice.isIPad ? VDSLayout.Spacing.space16X.value : VDSLayout.Spacing.space12X.value } private var minContentHeight: CGFloat { return UIDevice.isIPad ? VDSLayout.Spacing.space5X.value : VDSLayout.Spacing.space4X.value } private var minViewWidth: CGFloat { return fullBleed ? 320 : 288 } ///Max view width is for Tablet private var maxViewWidth: CGFloat { return fullBleed ? 1272 : 1232 } private var maxWidthConstraint: NSLayoutConstraint? open var leadingConstraint: NSLayoutConstraint? open var trailingConstraint: NSLayoutConstraint? //-------------------------------------------------- // MARK: - View Properties //-------------------------------------------------- open var typeIcon = Icon().with { $0.name = .infoBold $0.size = UIDevice.isIPad ? .medium : .small } open var closeButton = Icon().with { $0.name = .close $0.size = UIDevice.isIPad ? .medium : .small } open var titleLabel = Label().with { $0.textStyle = UIDevice.isIPad ? .boldBodyLarge : .boldBodySmall } open var subTitleLabel = Label().with { $0.textStyle = UIDevice.isIPad ? .bodyLarge : .bodySmall } open var buttonsView = ButtonGroup().with { $0.buttonPosition = .left } //Text open var title: String = "" { didSet{setNeedsUpdate()}} open var subTitle: String? { didSet{setNeedsUpdate()}} //Buttons open var primaryButtonModel: ButtonModel? { didSet{setNeedsUpdate()}} open var primaryButton = Button().with { $0.size = .small $0.use = .secondary } open var secondaryButtonModel: ButtonModel? { didSet{setNeedsUpdate()}} open var secondaryButton = Button().with { $0.size = .small $0.use = .secondary } open var onCloseClick: ((Notification)->())? { didSet { if let onCloseClick { onCloseSubscriber = closeButton .publisher(for: UITapGestureRecognizer()) .sink { _ in onCloseClick(self) } } else { onCloseSubscriber = nil } } } internal var onCloseSubscriber: AnyCancellable? //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- open var hideCloseButton: Bool = false { didSet{setNeedsUpdate()}} open var type: Style = .info { didSet{setNeedsUpdate()}} open var fullBleed: Bool = false { didSet {setNeedsUpdate()}} var _layout: Layout = .vertical open var layout: Layout { set { if !UIDevice.isIPad, newValue == .horizontal { return } _layout = newValue buttonsView.buttonPosition = _layout == .horizontal ? .center : .left setNeedsUpdate() } get { _layout } } //-------------------------------------------------- // MARK: - Configuration //-------------------------------------------------- private var backgroundColorConfiguration: AnyColorable = { let config = KeyedColorConfiguration(keyPath: \.type) config.setSurfaceColors(VDSColor.feedbackInformationBackgroundOnlight, VDSColor.feedbackInformationBackgroundOndark, forKey: .info) config.setSurfaceColors(VDSColor.feedbackWarningBackgroundOnlight, VDSColor.feedbackWarningBackgroundOndark, forKey: .warning) config.setSurfaceColors(VDSColor.feedbackSuccessBackgroundOnlight, VDSColor.feedbackSuccessBackgroundOndark, forKey: .success) config.setSurfaceColors(VDSColor.feedbackErrorBackgroundOnlight, VDSColor.feedbackErrorBackgroundOndark, forKey: .error) return config.eraseToAnyColorable() }() private var textColorConfiguration = ViewColorConfiguration().with { $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forDisabled: false) $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forDisabled: true) } //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- required public init() { super.init(frame: .zero) } public override init(frame: CGRect) { super.init(frame: .zero) } public required init?(coder: NSCoder) { super.init(coder: coder) } //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- open override func setup() { super.setup() addSubview(mainStackView) mainStackView.pinToSuperView(.init(top: edgeSpacing, left: edgeSpacing, bottom: edgeSpacing, right: edgeSpacing)) NSLayoutConstraint.activate([ heightAnchor.constraint(greaterThanOrEqualToConstant: minViewHeight), mainStackView.heightAnchor.constraint(greaterThanOrEqualToConstant: minContentHeight), widthAnchor.constraint(greaterThanOrEqualToConstant: minViewWidth) ]) maxWidthConstraint = widthAnchor.constraint(lessThanOrEqualToConstant: maxViewWidth) labelButtonView.addArrangedSubview(labelsView) mainStackView.addArrangedSubview(typeIcon) mainStackView.addArrangedSubview(labelButtonView) mainStackView.addArrangedSubview(closeButton) //labels titleLabel.textColorConfiguration = textColorConfiguration.eraseToAnyColorable() subTitleLabel.textColorConfiguration = textColorConfiguration.eraseToAnyColorable() } /// Resets back to this objects default settings. open override func reset() { super.reset() shouldUpdateView = false titleLabel.reset() titleLabel.text = "" titleLabel.textStyle = UIDevice.isIPad ? .boldBodyLarge : .boldBodySmall subTitleLabel.reset() subTitleLabel.textStyle = UIDevice.isIPad ? .bodyLarge : .bodySmall buttonsView.reset() buttonsView.buttonPosition = .left primaryButtonModel = nil secondaryButtonModel = nil type = .info typeIcon.size = UIDevice.isIPad ? .medium : .small typeIcon.name = .infoBold onCloseClick = nil closeButton.size = UIDevice.isIPad ? .medium : .small closeButton.name = .close layout = .vertical hideCloseButton = false fullBleed = false shouldUpdateView = true setNeedsUpdate() } //-------------------------------------------------- // MARK: - State //-------------------------------------------------- open override func updateView() { backgroundColor = backgroundColorConfiguration.getColor(self) updateIcons() updateLabels() updateButtons() setConstraints() } private func updateIcons() { let iconColor = surface == .dark ? VDSColor.paletteWhite : VDSColor.paletteBlack typeIcon.name = type.styleIconName() typeIcon.color = iconColor closeButton.color = iconColor closeButton.isHidden = hideCloseButton } private func updateLabels() { titleLabel.surface = surface subTitleLabel.surface = surface if !title.isEmpty { titleLabel.text = title labelsView.addArrangedSubview(titleLabel) } else { titleLabel.removeFromSuperview() } if let subTitle { subTitleLabel.text = subTitle labelsView.addArrangedSubview(subTitleLabel) } else { subTitleLabel.removeFromSuperview() } } private func updateButtons() { var buttons: [Button] = [] if let primaryButtonModel { primaryButton.text = primaryButtonModel.text primaryButton.surface = surface primaryButton.onClick = primaryButtonModel.onClick buttons.append(primaryButton) } if let secondaryButtonModel { secondaryButton.text = secondaryButtonModel.text secondaryButton.surface = surface secondaryButton.onClick = secondaryButtonModel.onClick buttons.append(secondaryButton) } if buttons.isEmpty { labelsView.setCustomSpacing(0, after: subTitleLabel) buttonsView.removeFromSuperview() } else { labelsView.setCustomSpacing(VDSLayout.Spacing.space3X.value, after: subTitleLabel) buttonsView.buttons = buttons labelButtonView.axis = layout == .vertical ? .vertical : .horizontal labelButtonView.addArrangedSubview(buttonsView) buttonsView .pinLeading() .pinTrailing() } } private func setConstraints() { maxWidthConstraint?.constant = maxViewWidth maxWidthConstraint?.isActive = UIDevice.isIPad if leadingConstraint == nil, let superview { leadingConstraint = NSLayoutConstraint(item: self, attribute: .leading, relatedBy: .equal, toItem: superview, attribute: .leading, multiplier: 1, constant: 0) } if trailingConstraint == nil, let superview { trailingConstraint = NSLayoutConstraint(item: superview, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1, constant: 0) } leadingConstraint?.isActive = fullBleed trailingConstraint?.isActive = fullBleed } }