// // DropdownSelect.swift // VDS // // Created by Kanamarlapudi, Vasavi on 28/03/24. // import Foundation import UIKit import VDSColorTokens import VDSFormControlsTokens import Combine /// A dropdown select is an expandable menu of predefined options that allows a customer to make a single selection. @objc(VDSDropdownSelect) open class DropdownSelect: EntryFieldBase { //-------------------------------------------------- // 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 //-------------------------------------------------- /// If true, the label will be displayed inside the dropdown containerView. Otherwise, the label will be above the dropdown containerView like a normal text input. open var showInlineLabel: Bool = false { didSet { setNeedsUpdate() }} /// Allows unique ID to be passed to the element. open var selectId: Int? { didSet { setNeedsUpdate() }} /// Current SelectedItem open var selectedItem: DropdownOptionModel? { guard let selectId else { return nil } return options[selectId] } /// Array of options to show open var options: [DropdownOptionModel] = [] { didSet { setNeedsUpdate() }} /// A callback when the selected option changes. Passes parameters (option). open var onItemSelected: ((Int, DropdownOptionModel) -> Void)? //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- internal var minWidthDefault = 66.0 internal var minWidthInlineLabel = 102.0 //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- open var inlineDisplayLabel = Label().with { $0.textAlignment = .left $0.textStyle = .boldBodyLarge $0.lineBreakMode = .byCharWrapping $0.sizeToFit() } open var selectedOptionLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.textAlignment = .left $0.textStyle = .bodyLarge $0.lineBreakMode = .byCharWrapping } open var dropdownField = UITextField().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.tintColor = UIColor.clear $0.font = TextStyle.bodyLarge.font } open var optionsPicker = UIPickerView() //-------------------------------------------------- // MARK: - Constraints //-------------------------------------------------- internal var inlineWidthConstraint: NSLayoutConstraint? //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- internal override var containerSize: CGSize { CGSize(width: showInlineLabel ? minWidthInlineLabel : width ?? minWidthDefault, height: 44) } internal let iconColorConfiguration = ControlColorConfiguration().with { $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .normal) $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled) $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .error) } //-------------------------------------------------- // 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() accessibilityLabel = "Dropdown Select" // stackview for controls in EntryFieldBase.controlContainerView let controlStackView = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .horizontal $0.spacing = VDSFormControls.spaceInset } controlContainerView.addSubview(controlStackView) controlStackView.pinToSuperView() controlStackView.addArrangedSubview(dropdownField) controlStackView.addArrangedSubview(inlineDisplayLabel) controlStackView.addArrangedSubview(selectedOptionLabel) controlStackView.setCustomSpacing(0, after: dropdownField) controlStackView.setCustomSpacing(VDSLayout.Spacing.space1X.value, after: inlineDisplayLabel) controlStackView.setCustomSpacing(VDSLayout.Spacing.space3X.value, after: selectedOptionLabel) dropdownField.width(0) inlineWidthConstraint = inlineDisplayLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 0) inlineWidthConstraint?.isActive = true // setting color config inlineDisplayLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() selectedOptionLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() // Options PickerView optionsPicker.delegate = self optionsPicker.dataSource = self optionsPicker.isHidden = true dropdownField.inputView = optionsPicker dropdownField.inputAccessoryView = { let inputToolbar = UIToolbar().with { $0.barStyle = .default $0.isTranslucent = true $0.items=[ UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace, target: self, action: nil), UIBarButtonItem(title: "Done", style: UIBarButtonItem.Style.done, target: self, action: #selector(pickerDoneClicked)) ] } inputToolbar.sizeToFit() return inputToolbar }() // tap gesture containerStackView .publisher(for: UITapGestureRecognizer()) .sink { [weak self] _ in self?.launchPicker() } .store(in: &subscribers) } /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { super.updateView() updateInlineLabel() dropdownField.isUserInteractionEnabled = readOnly ? false : true selectedOptionLabel.surface = surface selectedOptionLabel.isEnabled = isEnabled } /// Resets to default settings. open override func reset() { super.reset() inlineDisplayLabel.textStyle = .boldBodyLarge selectedOptionLabel.textStyle = .bodyLarge showInlineLabel = false options = [] selectId = nil } //-------------------------------------------------- // MARK: - Public Methods //-------------------------------------------------- open override func updateTitleLabel() { //update the local vars for the label since we no long have a model var attributes: [any LabelAttributeModel] = [] var updatedLabelText = labelText updatedLabelText = showInlineLabel ? "" : updatedLabelText if let oldText = updatedLabelText, !required, !oldText.hasSuffix("Optional") { let optionColorAttr = ColorLabelAttribute(location: oldText.count + 2, length: 8, color: secondaryColorConfiguration.getColor(self)) updatedLabelText = showInlineLabel ? "Optional" : "\(oldText) Optional" attributes.append(optionColorAttr) } if let tooltipModel { attributes.append(TooltipLabelAttribute(surface: surface, model: tooltipModel, presenter: self)) } titleLabel.text = updatedLabelText titleLabel.attributes = attributes titleLabel.surface = surface titleLabel.isEnabled = isEnabled } open func updateInlineLabel() { inlineWidthConstraint?.isActive = false /// inline label text and selected option text separated by ':' if let labelText, !labelText.isEmpty { inlineDisplayLabel.text = showInlineLabel ? (labelText + ":") : "" } else { inlineDisplayLabel.text = showInlineLabel ? labelText : "" } inlineDisplayLabel.surface = surface inlineDisplayLabel.isEnabled = isEnabled /// Update width as per updated text size inlineWidthConstraint = inlineDisplayLabel.widthAnchor.constraint(equalToConstant: inlineDisplayLabel.intrinsicContentSize.width) inlineWidthConstraint?.isActive = true if let selectId, selectId < options.count { updateSelectedOptionLabel(option: options[selectId]) } } open func updateSelectedOptionLabel(option: DropdownOptionModel? = nil) { selectedOptionLabel.text = option?.text ?? "" value = option?.value } open override func updateErrorLabel() { super.updateErrorLabel() if !showError && !hasInternalError { icon.name = .downCaret } icon.surface = surface icon.isHidden = readOnly ? true : false icon.color = iconColorConfiguration.getColor(self) } @objc open func pickerDoneClicked() { optionsPicker.isHidden = true dropdownField.resignFirstResponder() } } //-------------------------------------------------- // MARK: - UIPickerView Delegate & Datasource //-------------------------------------------------- extension DropdownSelect: UIPickerViewDelegate, UIPickerViewDataSource { internal func launchPicker() { if optionsPicker.isHidden { dropdownField.becomeFirstResponder() } else { dropdownField.resignFirstResponder() } optionsPicker.isHidden = !optionsPicker.isHidden } public func numberOfComponents(in pickerView: UIPickerView) -> Int { return 1 } public func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { return options.count } public func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { guard options.count > row else { return nil } return options[row].text } public func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { guard options.count > row else { return } selectId = row updateSelectedOptionLabel(option: options[row]) self.onItemSelected?(row, options[row]) } }