vds_ios/VDS/Components/Notification/Notification.swift
Krishna Kishore Bandaru 3dc55db8ea added spacing constants
2024-02-13 22:18:03 +05:30

403 lines
15 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.
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 = UIDevice.isIPad ? VDSLayout.Spacing.space3X.value : VDSLayout.Spacing.space2X.value
}
private var labelsView = UIStackView().with {
$0.spacing = 1.0
$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()}}
var _layout: Layout = .vertical
/// Determines the orientation of buttons and text in the Notification.
open 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()
}
/// 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()
}
//--------------------------------------------------
// 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() {
maxWidthConstraint?.constant = maxViewWidth
maxWidthConstraint?.isActive = UIDevice.isIPad
labelViewAndButtonViewConstraint?.isActive = layout == .vertical && !buttonGroup.buttons.isEmpty
typeIconWidthConstraint?.constant = typeIcon.size.dimensions.width
closeIconWidthConstraint?.constant = closeButton.size.dimensions.width
labelViewBottomConstraint?.isActive = layout == .horizontal || buttonGroup.buttons.isEmpty
buttonGroupCenterYConstraint?.isActive = layout == .horizontal
buttonGroupBottomConstraint?.isActive = layout == .vertical
}
}