// // ModalDialog.swift // VDS // // Created by Kanamarlapudi, Vasavi on 09/09/24. // import Foundation import UIKit import VDSCoreTokens @objc(VDSModalDialog) open class ModalDialog: View, UIScrollViewDelegate, ParentViewProtocol { //-------------------------------------------------- // 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: - Public Properties //-------------------------------------------------- open var children: [any ViewProtocol] { [closeCrossButton, titleLabel, contentLabel, buttonGroupData] } open var modalModel = Modal.ModalModel() { didSet { setNeedsUpdate() } } open var titleLabel = Label().with { label in label.isAccessibilityElement = true label.textStyle = .boldTitleLarge } open var contentLabel = Label().with { label in label.isAccessibilityElement = true label.textStyle = .bodyLarge } open lazy var closeCrossButton = ButtonIcon().with { $0.kind = .ghost $0.surfaceType = .colorFill $0.iconName = .close $0.size = .small $0.customContainerSize = UIDevice.isIPad ? 48 : 48 $0.customIconSize = UIDevice.isIPad ? 32 : 32 } open var buttonGroupData = ButtonGroup().with { $0.alignment = .left } //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- private var scrollView = UIScrollView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.backgroundColor = .clear } private var contentStackView = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical $0.alignment = .leading $0.distribution = .fillProportionally $0.spacing = 0 } lazy var primaryAccessibilityElement = UIAccessibilityElement(accessibilityContainer: self).with { $0.accessibilityLabel = "Modal" } // close button with the 48 x 48 px private var closeCrossButtonSize = 48.0 private let containerViewInset = UIDevice.isIPad ? VDSLayout.space12X : VDSLayout.space4X private let contentLabelTopSpace = UIDevice.isIPad ? VDSLayout.space8X : VDSLayout.space6X private let contentLabelBottomSpace = UIDevice.isIPad ? VDSLayout.space8X : VDSLayout.space12X private let gapBetweenButtonItems = VDSLayout.space3X //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- private let backgroundColorConfiguration = SurfaceColorConfiguration(VDSColor.backgroundPrimaryLight, VDSColor.backgroundPrimaryDark) private let textColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark) //-------------------------------------------------- // MARK: - Constraints //-------------------------------------------------- private var contentStackViewBottomConstraint: 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() titleLabel.accessibilityTraits = .header layer.cornerRadius = 12 // Add titleLabel, contentLabel to contentStack. contentStackView.addArrangedSubview(titleLabel) contentStackView.addArrangedSubview(contentLabel) contentStackView.setCustomSpacing(contentLabelTopSpace, after: titleLabel) scrollView.addSubview(contentStackView) // Add crossButon, scrollView, buttonsData. addSubview(closeCrossButton) addSubview(scrollView) addSubview(buttonGroupData) self.bringSubviewToFront(closeCrossButton) let crossTopSpace = UIDevice.isIPad && !modalModel.fullScreenDialog ? 0 : VDSLayout.space12X let scrollTopSpace = UIDevice.isIPad && !modalModel.fullScreenDialog ? containerViewInset : (crossTopSpace + closeCrossButtonSize) let contentTrailingSpace = UIDevice.isIPad ? (containerViewInset/2) - 6 : containerViewInset // Activate constraints NSLayoutConstraint.activate([ // Constraints for the closeCrossButton closeCrossButton.topAnchor.constraint(equalTo: topAnchor, constant: crossTopSpace), closeCrossButton.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor), closeCrossButton.trailingAnchor.constraint(equalTo: trailingAnchor), closeCrossButton.heightAnchor.constraint(equalToConstant: closeCrossButtonSize), // Constraints for the bottom button view buttonGroupData.leadingAnchor.constraint(equalTo: leadingAnchor, constant:containerViewInset), buttonGroupData.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -containerViewInset), buttonGroupData.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -containerViewInset), // Constraints for the scrollView scrollView.topAnchor.constraint(equalTo: topAnchor, constant: scrollTopSpace), scrollView.leadingAnchor.constraint(equalTo: leadingAnchor, constant:containerViewInset), scrollView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -contentTrailingSpace), scrollView.bottomAnchor.constraint(equalTo: buttonGroupData.topAnchor, constant: -contentLabelBottomSpace), // Constraints for the contentStackView contentStackView.topAnchor.constraint(equalTo: scrollView.topAnchor), contentStackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), contentStackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -contentTrailingSpace), contentStackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -contentTrailingSpace), contentStackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor) ]) contentStackViewBottomConstraint = contentStackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor) contentStackViewBottomConstraint?.activate() } /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { super.updateView() // Update surface and background backgroundColor = backgroundColorConfiguration.getColor(self) scrollView.indicatorStyle = surface == .light ? .black : .white closeCrossButton.surface = surface buttonGroupData.surface = surface titleLabel.surface = surface contentLabel.surface = surface // Re-arrange contentStack contentStackView.removeArrangedSubviews() titleLabel.text = modalModel.title contentLabel.text = modalModel.content titleLabel.textColor = textColorConfiguration.getColor(self) contentLabel.textColor = textColorConfiguration.getColor(self) titleLabel.sizeToFit() contentLabel.sizeToFit() // Add buttons data if provided if let buttons = modalModel.buttonData, buttons.count > 0 { buttonGroupData.buttons = buttons let percent = UIDevice.isIPad ? 50.0 : 100.0 buttonGroupData.rowQuantityTablet = 2 buttonGroupData.rowQuantityPhone = 1 buttonGroupData.childWidth = .percentage(percent) } // Update title, content and contentview var addedTitle = false if let titleText = modalModel.title, !titleText.isEmpty { contentStackView.addArrangedSubview(titleLabel) addedTitle = true } var addedContent = false if let contentText = modalModel.content, !contentText.isEmpty { contentStackView.addArrangedSubview(contentLabel) addedContent = true } else if let contentView = modalModel.contentView { contentView.translatesAutoresizingMaskIntoConstraints = false if var surfaceable = contentView as? Surfaceable { surfaceable.surface = surface } contentStackView.addArrangedSubview(contentView) addedContent = true } if addedTitle && addedContent { contentStackView.spacing = contentLabelTopSpace } closeCrossButton.isHidden = modalModel.hideCloseButton contentStackView.setNeedsLayout() contentStackView.layoutIfNeeded() scrollView.setNeedsLayout() scrollView.layoutIfNeeded() } /// Used to update any Accessibility properties. open override func updateAccessibility() { super.updateAccessibility() primaryAccessibilityElement.accessibilityHint = "Double tap on the cross button to close." primaryAccessibilityElement.accessibilityFrameInContainerSpace = .init(origin: .zero, size: frame.size) } open override var accessibilityElements: [Any]? { get { var elements: [Any] = [primaryAccessibilityElement] contentStackView.arrangedSubviews.forEach{ elements.append($0) } elements.append(buttonGroupData) return elements } set {} } }