418 lines
16 KiB
Swift
418 lines
16 KiB
Swift
//
|
|
// Notification.swift
|
|
// VDS
|
|
//
|
|
// Created by Nadigadda, Sumanth on 14/03/23.
|
|
//
|
|
|
|
import Foundation
|
|
import UIKit
|
|
import VDSColorTokens
|
|
import Combine
|
|
|
|
/// Notifications are prominent, attention-getting banners that provide information
|
|
/// in context. There are four types: information, success, warning and error; each
|
|
/// with different color and content. They may be screen-specific, flow-specific or
|
|
/// experience-wide.
|
|
@objc(VDSNotification)
|
|
open class Notification: View {
|
|
|
|
//--------------------------------------------------
|
|
// 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: - Enums
|
|
//--------------------------------------------------
|
|
/// Enum used to describe the type of Notification.
|
|
public enum Style: String, CaseIterable {
|
|
case info, success, warning, error
|
|
|
|
func iconName() -> Icon.Name {
|
|
switch self {
|
|
case .info:
|
|
return .infoBold
|
|
case .success:
|
|
return .checkmarkAltBold
|
|
case .warning:
|
|
return .warningBold
|
|
case .error:
|
|
return .errorBold
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Enum used to describe the orientation of Notification.
|
|
/// Only horizontal orientation is supported for iPhone & iPad devices only.
|
|
private 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 = UIDevice.isIPad ? VDSLayout.Spacing.space3X.value : VDSLayout.Spacing.space2X.value
|
|
}
|
|
|
|
private var labelsView = UIStackView().with {
|
|
$0.spacing = VDSLayout.Spacing.space1X.value
|
|
$0.translatesAutoresizingMaskIntoConstraints = false
|
|
$0.alignment = .fill
|
|
$0.distribution = .equalSpacing
|
|
$0.axis = .vertical
|
|
}
|
|
|
|
private var labelButtonView = View().with {
|
|
$0.translatesAutoresizingMaskIntoConstraints = false
|
|
}
|
|
|
|
private var labelButtonViewSpacing: CGFloat {
|
|
let spacing: CGFloat = UIDevice.isIPad ? 20 : 16
|
|
return switch layout {
|
|
case .vertical:
|
|
0
|
|
case .horizontal:
|
|
spacing
|
|
}
|
|
}
|
|
|
|
internal var onCloseSubscriber: AnyCancellable?
|
|
|
|
private var maxWidthConstraint: NSLayoutConstraint?
|
|
|
|
private var leadingConstraint: NSLayoutConstraint?
|
|
|
|
private var trailingConstraint: NSLayoutConstraint?
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Public Properties
|
|
//--------------------------------------------------
|
|
/// Icon used for denoting type.
|
|
open var typeIcon = Icon().with {
|
|
$0.name = .infoBold
|
|
$0.size = UIDevice.isIPad ? .medium : .small
|
|
}
|
|
|
|
/// Icon used for the close.
|
|
open var closeButton = Icon().with {
|
|
$0.name = .close
|
|
$0.size = UIDevice.isIPad ? .medium : .small
|
|
}
|
|
|
|
/// Label used to show title.
|
|
open var titleLabel = Label().with {
|
|
$0.textStyle = UIDevice.isIPad ? .boldBodyLarge : .boldBodySmall
|
|
}
|
|
|
|
/// Label used to show subTitle.
|
|
open var subTitleLabel = Label().with {
|
|
$0.textStyle = UIDevice.isIPad ? .bodyLarge : .bodySmall
|
|
}
|
|
|
|
/// ButtonGroup to house the 2 optional buttons, primaryButton and secondaryButotn
|
|
open var buttonGroup = ButtonGroup().with {
|
|
$0.alignment = .left
|
|
}
|
|
|
|
/// Text that will go into the titleLabel.
|
|
open var title: String = "" { didSet { setNeedsUpdate()}}
|
|
|
|
/// Text that will go into the subTitleLabel.
|
|
open var subTitle: String? { didSet { setNeedsUpdate()}}
|
|
|
|
/// Button model representing the primaryButton.
|
|
open var primaryButtonModel: ButtonModel? { didSet { setNeedsUpdate()}}
|
|
|
|
/// Button used for the primary action.
|
|
open var primaryButton = Button().with {
|
|
$0.size = .small
|
|
$0.use = .secondary
|
|
}
|
|
|
|
/// Button model representing the secondaryButton.
|
|
open var secondaryButtonModel: ButtonModel? { didSet { setNeedsUpdate()}}
|
|
|
|
/// Button used for the secondary action.
|
|
open var secondaryButton = Button().with {
|
|
$0.size = .small
|
|
$0.use = .secondary
|
|
}
|
|
|
|
/// Completion Block for a close click event.
|
|
open var onCloseClick: ((Notification)->())? {
|
|
didSet {
|
|
if let onCloseClick {
|
|
onCloseSubscriber = closeButton
|
|
.publisher(for: UITapGestureRecognizer())
|
|
.sink { _ in
|
|
onCloseClick(self)
|
|
}
|
|
} else {
|
|
onCloseSubscriber = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
/// If true, will hide the close button.
|
|
open var hideCloseButton: Bool = false { didSet { setNeedsUpdate()}}
|
|
|
|
/// Add this attribute determine your type of Notification.
|
|
open var style: Style = .info { didSet { setNeedsUpdate()}}
|
|
|
|
private var _layout: Layout = .vertical
|
|
|
|
/// Determines the orientation of buttons and text in the Notification.
|
|
/// Only horizontal orientation is supported for iPhone & iPad devices only.
|
|
private var layout: Layout {
|
|
set {
|
|
if !UIDevice.isIPad, newValue == .horizontal { return }
|
|
_layout = newValue
|
|
buttonGroup.alignment = _layout == .horizontal ? .center : .left
|
|
setNeedsUpdate()
|
|
}
|
|
get { _layout }
|
|
}
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Configuration
|
|
//--------------------------------------------------
|
|
private var backgroundColorConfiguration: AnyColorable = {
|
|
let config = KeyedColorConfiguration<Notification, Style>(keyPath: \.style)
|
|
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)
|
|
}
|
|
|
|
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 288
|
|
}
|
|
|
|
private var maxViewWidth: CGFloat {
|
|
return 1232
|
|
}
|
|
|
|
private var labelViewWidthConstraint: NSLayoutConstraint?
|
|
private var labelViewBottomConstraint: NSLayoutConstraint?
|
|
private var labelViewAndButtonViewConstraint: NSLayoutConstraint?
|
|
private var buttonViewTopConstraint: NSLayoutConstraint?
|
|
private var typeIconWidthConstraint: NSLayoutConstraint?
|
|
private var closeIconWidthConstraint: NSLayoutConstraint?
|
|
private var buttonGroupCenterYConstraint: NSLayoutConstraint?
|
|
private var buttonGroupBottomConstraint: NSLayoutConstraint?
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Overrides
|
|
//--------------------------------------------------
|
|
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
|
|
open override func setup() {
|
|
super.setup()
|
|
|
|
let layoutGuide = UILayoutGuide()
|
|
addLayoutGuide(layoutGuide)
|
|
layoutGuide
|
|
.pinTop(0)
|
|
.pinLeading(0)
|
|
.pinTrailing(0, .defaultHigh)
|
|
.pinBottom(0, .defaultHigh)
|
|
|
|
addSubview(mainStackView)
|
|
mainStackView.pin(layoutGuide, with: .uniform(edgeSpacing))
|
|
|
|
NSLayoutConstraint.activate([
|
|
layoutGuide.heightAnchor.constraint(greaterThanOrEqualToConstant: minViewHeight),
|
|
mainStackView.heightAnchor.constraint(greaterThanOrEqualToConstant: minContentHeight),
|
|
layoutGuide.widthAnchor.constraint(greaterThanOrEqualToConstant: minViewWidth)
|
|
])
|
|
maxWidthConstraint = layoutGuide.widthAnchor.constraint(lessThanOrEqualToConstant: maxViewWidth)
|
|
|
|
labelButtonView.addSubview(labelsView)
|
|
labelsView
|
|
.pinTop()
|
|
.pinLeading()
|
|
labelViewWidthConstraint = labelsView.widthAnchor.constraint(equalTo: labelButtonView.widthAnchor, multiplier: 1.0)
|
|
labelViewWidthConstraint?.activate()
|
|
labelViewBottomConstraint = labelButtonView.bottomAnchor.constraint(equalTo: labelsView.bottomAnchor)
|
|
|
|
labelButtonView.addSubview(buttonGroup)
|
|
buttonGroup
|
|
.pinTrailing()
|
|
buttonGroupBottomConstraint = labelButtonView.bottomAnchor.constraint(equalTo: buttonGroup.bottomAnchor)
|
|
buttonGroupCenterYConstraint = buttonGroup.centerYAnchor.constraint(equalTo: labelButtonView.centerYAnchor)
|
|
labelViewAndButtonViewConstraint = buttonGroup.topAnchor.constraint(equalTo: labelsView.bottomAnchor, constant: VDSLayout.Spacing.space3X.value)
|
|
buttonGroup.widthAnchor.constraint(equalTo: labelsView.widthAnchor).activate()
|
|
|
|
mainStackView.addArrangedSubview(typeIcon)
|
|
mainStackView.addArrangedSubview(labelButtonView)
|
|
mainStackView.addArrangedSubview(closeButton)
|
|
|
|
typeIconWidthConstraint = typeIcon.width(constant: typeIcon.size.dimensions.width)
|
|
closeIconWidthConstraint = closeButton.width(constant: closeButton.size.dimensions.width)
|
|
|
|
//labels
|
|
titleLabel.textColorConfiguration = textColorConfiguration.eraseToAnyColorable()
|
|
subTitleLabel.textColorConfiguration = textColorConfiguration.eraseToAnyColorable()
|
|
|
|
//TODO: Need to add setup animation for displaying the Notification view for iOS.
|
|
}
|
|
|
|
/// Resets to 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
|
|
|
|
buttonGroup.reset()
|
|
buttonGroup.alignment = .left
|
|
|
|
primaryButtonModel = nil
|
|
secondaryButtonModel = nil
|
|
|
|
style = .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
|
|
|
|
shouldUpdateView = true
|
|
setNeedsUpdate()
|
|
}
|
|
|
|
/// Used to make changes to the View based off a change events or from local properties.
|
|
open override func updateView() {
|
|
super.updateView()
|
|
|
|
backgroundColor = backgroundColorConfiguration.getColor(self)
|
|
updateIcons()
|
|
updateLabels()
|
|
updateButtons()
|
|
setConstraints()
|
|
}
|
|
|
|
/// Override to check the screen width to determine cornerRadius
|
|
open override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
layer.cornerRadius = UIScreen.main.bounds.width == bounds.width ? 0 : 4.0
|
|
}
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Private Methods
|
|
//--------------------------------------------------
|
|
private func updateIcons() {
|
|
let iconColor = surface == .dark ? VDSColor.paletteWhite : VDSColor.paletteBlack
|
|
typeIcon.name = style.iconName()
|
|
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)
|
|
}
|
|
labelViewWidthConstraint?.deactivate()
|
|
if buttons.isEmpty {
|
|
buttonGroup.isHidden = true
|
|
labelViewWidthConstraint = labelsView.widthAnchor.constraint(equalTo: labelButtonView.widthAnchor)
|
|
buttonGroup.buttons.removeAll()
|
|
} else {
|
|
labelsView.setCustomSpacing(VDSLayout.Spacing.space3X.value, after: subTitleLabel)
|
|
buttonGroup.buttons = buttons
|
|
buttonGroup.isHidden = false
|
|
labelViewWidthConstraint = labelsView.widthAnchor.constraint(equalTo: labelButtonView.widthAnchor, multiplier: layout == .vertical ? 1.0 : 0.5, constant: layout == .vertical ? 0 : -labelButtonViewSpacing)
|
|
}
|
|
labelViewWidthConstraint?.activate()
|
|
}
|
|
|
|
private func setConstraints() {
|
|
labelViewAndButtonViewConstraint?.deactivate()
|
|
labelViewBottomConstraint?.deactivate()
|
|
buttonGroupCenterYConstraint?.deactivate()
|
|
buttonGroupBottomConstraint?.deactivate()
|
|
labelViewAndButtonViewConstraint?.isActive = layout == .vertical && !buttonGroup.buttons.isEmpty
|
|
labelViewBottomConstraint?.isActive = layout == .horizontal || buttonGroup.buttons.isEmpty
|
|
buttonGroupCenterYConstraint?.isActive = layout == .horizontal
|
|
buttonGroupBottomConstraint?.isActive = layout == .vertical
|
|
typeIconWidthConstraint?.constant = typeIcon.size.dimensions.width
|
|
closeIconWidthConstraint?.constant = closeButton.size.dimensions.width
|
|
//iPad 12.9 inches is more than the configured maxViewWidth(1232), verified with designer on the same. Suggested to remove max width constraint
|
|
//maxWidthConstraint?.constant = maxViewWidth
|
|
//maxWidthConstraint?.isActive = UIDevice.isIPad
|
|
}
|
|
}
|