From 8472a5ec3801c8b464963f6dd5f7976035e70154 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 5 Jun 2023 10:42:19 -0500 Subject: [PATCH] added selector base controls for element and item Signed-off-by: Matt Bruce --- VDS.xcodeproj/project.pbxproj | 22 ++- VDS/Classes/SelectorBase.swift | 98 ++++++++++ VDS/Classes/SelectorItemBase.swift | 276 +++++++++++++++++++++++++++++ 3 files changed, 391 insertions(+), 5 deletions(-) create mode 100644 VDS/Classes/SelectorBase.swift create mode 100644 VDS/Classes/SelectorItemBase.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index c091ed02..26d8ae24 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 5F21D7BF28DCEB3D003E7CD6 /* Useable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F21D7BE28DCEB3D003E7CD6 /* Useable.swift */; }; 5FC35BE328D51405004EBEAC /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FC35BE228D51405004EBEAC /* Button.swift */; }; EA0FC2C62914222900DF80B4 /* ButtonGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0FC2C52914222900DF80B4 /* ButtonGroup.swift */; }; + EA1DA1CB2A2E36DC001C51D2 /* SelectorBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1DA1CA2A2E36DC001C51D2 /* SelectorBase.swift */; }; EA1F266528B945070033E859 /* RadioSwatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1F266128B945070033E859 /* RadioSwatch.swift */; }; EA1F266628B945070033E859 /* RadioSwatchGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1F266228B945070033E859 /* RadioSwatchGroup.swift */; }; EA297A5529FB07760031ED56 /* TooltipLabelAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA297A5429FB07760031ED56 /* TooltipLabelAttribute.swift */; }; @@ -97,6 +98,8 @@ EAB5FF0129424ACB00998C17 /* UIControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB5FF0029424ACB00998C17 /* UIControl.swift */; }; EABFEB642A26473700C4C106 /* NSAttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = EABFEB632A26473700C4C106 /* NSAttributedString.swift */; }; EAC71A1D2A2E155A00E47A9F /* Checkbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */; }; + EAC71A1F2A2E173D00E47A9F /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */; }; + EAC71A212A2E1DC000E47A9F /* SelectorItemBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A202A2E1DC000E47A9F /* SelectorItemBase.swift */; }; EAC846F3294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */; }; EAC9257D29119B5400091998 /* TextLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC9257C29119B5400091998 /* TextLink.swift */; }; EAC925832911B35400091998 /* TextLinkCaret.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC925822911B35300091998 /* TextLinkCaret.swift */; }; @@ -119,7 +122,7 @@ EAF7F0B3289B1ADC00B287F5 /* ActionLabelAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0B2289B1ADC00B287F5 /* ActionLabelAttribute.swift */; }; EAF7F0B7289C12A600B287F5 /* UITapGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0B6289C12A600B287F5 /* UITapGestureRecognizer.swift */; }; EAF7F0B9289C139800B287F5 /* ColorConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0B8289C139800B287F5 /* ColorConfiguration.swift */; }; - EAF7F11728A1475A00B287F5 /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F11528A1475A00B287F5 /* RadioButton.swift */; }; + EAF7F11728A1475A00B287F5 /* RadioButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F11528A1475A00B287F5 /* RadioButtonItem.swift */; }; EAF7F13328A2A16500B287F5 /* AttachmentLabelAttributeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F13228A2A16500B287F5 /* AttachmentLabelAttributeModel.swift */; }; /* End PBXBuildFile section */ @@ -140,6 +143,7 @@ 5F21D7BE28DCEB3D003E7CD6 /* Useable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Useable.swift; sourceTree = ""; }; 5FC35BE228D51405004EBEAC /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = ""; }; EA0FC2C52914222900DF80B4 /* ButtonGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroup.swift; sourceTree = ""; }; + EA1DA1CA2A2E36DC001C51D2 /* SelectorBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectorBase.swift; sourceTree = ""; }; EA1F266128B945070033E859 /* RadioSwatch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadioSwatch.swift; sourceTree = ""; }; EA1F266228B945070033E859 /* RadioSwatchGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadioSwatchGroup.swift; sourceTree = ""; }; EA297A5429FB07760031ED56 /* TooltipLabelAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooltipLabelAttribute.swift; sourceTree = ""; }; @@ -226,6 +230,8 @@ EAB5FF0029424ACB00998C17 /* UIControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControl.swift; sourceTree = ""; }; EABFEB632A26473700C4C106 /* NSAttributedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSAttributedString.swift; sourceTree = ""; }; EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkbox.swift; sourceTree = ""; }; + EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = ""; }; + EAC71A202A2E1DC000E47A9F /* SelectorItemBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectorItemBase.swift; sourceTree = ""; }; EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroupCollectionViewCell.swift; sourceTree = ""; }; EAC9257C29119B5400091998 /* TextLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextLink.swift; sourceTree = ""; }; EAC925822911B35300091998 /* TextLinkCaret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextLinkCaret.swift; sourceTree = ""; }; @@ -247,7 +253,7 @@ EAF7F0B2289B1ADC00B287F5 /* ActionLabelAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionLabelAttribute.swift; sourceTree = ""; }; EAF7F0B6289C12A600B287F5 /* UITapGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITapGestureRecognizer.swift; sourceTree = ""; }; EAF7F0B8289C139800B287F5 /* ColorConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorConfiguration.swift; sourceTree = ""; }; - EAF7F11528A1475A00B287F5 /* RadioButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = ""; }; + EAF7F11528A1475A00B287F5 /* RadioButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadioButtonItem.swift; sourceTree = ""; }; EAF7F13228A2A16500B287F5 /* AttachmentLabelAttributeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentLabelAttributeModel.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -466,8 +472,10 @@ EAF7F0B8289C139800B287F5 /* ColorConfiguration.swift */, EA3361B5288B2A410071C351 /* Control.swift */, EAF7F09F289AB7EC00B287F5 /* View.swift */, - EA4DB18428CA967F00103EE3 /* SelectorGroupHandlerBase.swift */, EAB5FEF02927F4AA00998C17 /* SelfSizingCollectionView.swift */, + EA4DB18428CA967F00103EE3 /* SelectorGroupHandlerBase.swift */, + EAC71A202A2E1DC000E47A9F /* SelectorItemBase.swift */, + EA1DA1CA2A2E36DC001C51D2 /* SelectorBase.swift */, ); path = Classes; sourceTree = ""; @@ -708,7 +716,8 @@ EAF7F11428A1470D00B287F5 /* RadioButton */ = { isa = PBXGroup; children = ( - EAF7F11528A1475A00B287F5 /* RadioButton.swift */, + EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */, + EAF7F11528A1475A00B287F5 /* RadioButtonItem.swift */, EAB1D29B28A5618900DAE764 /* RadioButtonGroup.swift */, ); path = RadioButton; @@ -857,9 +866,10 @@ EAC925842911C63100091998 /* Colorable.swift in Sources */, EAB5FEF5292D371F00998C17 /* ButtonBase.swift in Sources */, EA978EC5291D6AFE00ACC883 /* AnyLabelAttribute.swift in Sources */, + EAC71A1F2A2E173D00E47A9F /* RadioButton.swift in Sources */, EA33622C2891E73B0071C351 /* FontProtocol.swift in Sources */, EA596ABD2A16B4EC00300C4B /* Tab.swift in Sources */, - EAF7F11728A1475A00B287F5 /* RadioButton.swift in Sources */, + EAF7F11728A1475A00B287F5 /* RadioButtonItem.swift in Sources */, EA985BEE2968A92400F2FF2E /* TitleLockupSubTitleModel.swift in Sources */, EA985BF22968B5BB00F2FF2E /* TitleLockupTextStyle.swift in Sources */, EAB1D2CD28ABE76100DAE764 /* Withable.swift in Sources */, @@ -926,9 +936,11 @@ EA3361B8288B2AAA0071C351 /* ViewProtocol.swift in Sources */, EA3361BF288B2EA60071C351 /* Handlerable.swift in Sources */, EA3361A8288B23300071C351 /* UIColor.swift in Sources */, + EA1DA1CB2A2E36DC001C51D2 /* SelectorBase.swift in Sources */, EAC9257D29119B5400091998 /* TextLink.swift in Sources */, EA1F266628B945070033E859 /* RadioSwatchGroup.swift in Sources */, EA596ABF2A16B4F500300C4B /* Tabs.swift in Sources */, + EAC71A212A2E1DC000E47A9F /* SelectorItemBase.swift in Sources */, EA985BEC2968A91200F2FF2E /* TitleLockupTitleModel.swift in Sources */, 5FC35BE328D51405004EBEAC /* Button.swift in Sources */, ); diff --git a/VDS/Classes/SelectorBase.swift b/VDS/Classes/SelectorBase.swift new file mode 100644 index 00000000..315aab1f --- /dev/null +++ b/VDS/Classes/SelectorBase.swift @@ -0,0 +1,98 @@ +// +// SelectorBase.swift +// VDS +// +// Created by Matt Bruce on 6/5/23. +// + +import Foundation +import Combine +import VDSColorTokens +import VDSFormControlsTokens + +public protocol SelectorControlable: Control, Changeable { + var showError: Bool { get set } + var size: CGSize { get set } + var backgroundColorConfig: ControlColorConfiguration { get set } + var borderColorConfig: ControlColorConfiguration { get set } + var selectorColorConfig: ControlColorConfiguration { get set } +} + +open class SelectorBase: Control, SelectorControlable { + public var onChangeSubscriber: AnyCancellable? { + willSet { + if let onChangeSubscriber { + onChangeSubscriber.cancel() + } + } + } + + open var size = CGSize(width: 20, height: 20) { didSet { setNeedsUpdate() }} + + var _showError: Bool = false + open var showError: Bool { + get { _showError } + set { + if !isSelected && _showError != newValue { + _showError = newValue + setNeedsUpdate() + } + } + } + + open override var state: UIControl.State { + get { + var state = super.state + if showError { + state.insert(.error) + } + return state + } + } + + open var backgroundColorConfig = ControlColorConfiguration() { didSet { setNeedsUpdate() }} + + open var borderColorConfig = ControlColorConfiguration() { didSet { setNeedsUpdate() }} + + open var selectorColorConfig = ControlColorConfiguration() { didSet { setNeedsUpdate() }} + + //-------------------------------------------------- + // MARK: - Constraints + //-------------------------------------------------- + private var selectorHeightConstraint: NSLayoutConstraint? + private var selectorWidthConstraint: NSLayoutConstraint? + + internal var shapeLayer: CAShapeLayer? + + open override func setup() { + super.setup() + let layoutGuide = UILayoutGuide() + addLayoutGuide(layoutGuide) + + selectorHeightConstraint = layoutGuide.heightAnchor.constraint(equalToConstant: size.height) + selectorHeightConstraint?.isActive = true + + selectorWidthConstraint = layoutGuide.widthAnchor.constraint(equalToConstant: size.width) + selectorWidthConstraint?.isActive = true + + NSLayoutConstraint.activate([ + layoutGuide.topAnchor.constraint(equalTo: topAnchor), + layoutGuide.bottomAnchor.constraint(equalTo: bottomAnchor), + layoutGuide.leadingAnchor.constraint(equalTo: leadingAnchor), + layoutGuide.trailingAnchor.constraint(equalTo: trailingAnchor)]) + + layer.cornerRadius = 2.0 + layer.borderWidth = VDSFormControls.widthBorder + + } + open override func updateView() { + super.updateView() + + selectorHeightConstraint?.constant = size.height + selectorWidthConstraint?.constant = size.width + + setNeedsLayout() + layoutIfNeeded() + } + +} diff --git a/VDS/Classes/SelectorItemBase.swift b/VDS/Classes/SelectorItemBase.swift new file mode 100644 index 00000000..3982843a --- /dev/null +++ b/VDS/Classes/SelectorItemBase.swift @@ -0,0 +1,276 @@ +// +// SelectorItemBase.swift +// VDS +// +// Created by Matt Bruce on 6/5/23. +// + +import Foundation +import UIKit +import Combine +import VDSColorTokens +import VDSFormControlsTokens + +/// Checkboxes are a multi-select component through which a customer indicates a choice. If a binary choice, the component is a checkbox. If the choice has multiple options, the component is a ``CheckboxGroup``. +open class SelectorItemBase: Control, Errorable, Changeable { + + //-------------------------------------------------- + // 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: - Private Properties + //-------------------------------------------------- + private var shouldShowError: Bool { + guard showError && !disabled && errorText?.isEmpty == false else { return false } + return true + } + + private var shouldShowLabels: Bool { + guard labelText?.isEmpty == false || childText?.isEmpty == false || labelAttributedText?.string.isEmpty == false || childAttributedText?.string.isEmpty == false else { return false } + return true + } + + private var mainStackView = UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.alignment = .top + $0.axis = .vertical + } + + private var selectorStackView = UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.alignment = .top + $0.axis = .horizontal + } + + private var selectorLabelStackView = UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .vertical + } + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + open var onChangeSubscriber: AnyCancellable? { + willSet { + if let onChangeSubscriber { + onChangeSubscriber.cancel() + } + } + } + + open var label = Label().with { + $0.setContentCompressionResistancePriority(.required, for: .vertical) + $0.textPosition = .left + $0.textStyle = .boldBodyLarge + } + + open var childLabel = Label().with { + $0.setContentCompressionResistancePriority(.required, for: .vertical) + $0.textPosition = .left + $0.textStyle = .bodyLarge + } + + open var errorLabel = Label().with { + $0.setContentCompressionResistancePriority(.required, for: .vertical) + $0.textPosition = .left + $0.textStyle = .bodyMedium + } + + open var selectorView = Selector() + + open var isAnimated: Bool = true { didSet { setNeedsUpdate() }} + + open override var isSelected: Bool { didSet { setNeedsUpdate() }} + + open var labelText: String? { didSet { setNeedsUpdate() }} + + open var labelTextAttributes: [any LabelAttributeModel]? { didSet { setNeedsUpdate() }} + + open var labelAttributedText: NSAttributedString? { + didSet { + label.useAttributedText = !(labelAttributedText?.string.isEmpty ?? true) + label.attributedText = labelAttributedText + setNeedsUpdate() + } + } + + open var childText: String? { didSet { setNeedsUpdate() }} + + open var childTextAttributes: [any LabelAttributeModel]? { didSet { setNeedsUpdate() }} + + open var childAttributedText: NSAttributedString? { + didSet { + childLabel.useAttributedText = !(childAttributedText?.string.isEmpty ?? true) + childLabel.attributedText = childAttributedText + setNeedsUpdate() + } + } + + var _showError: Bool = false + open var showError: Bool { + get { _showError } + set { + if !isSelected && _showError != newValue { + _showError = newValue + setNeedsUpdate() + } + } + } + + open override var state: UIControl.State { + get { + var state = super.state + if showError { + state.insert(.error) + } + return state + } + } + + open var errorText: String? { didSet { setNeedsUpdate() }} + + open var inputId: String? { didSet { setNeedsUpdate() }} + + open var value: AnyHashable? { didSet { setNeedsUpdate() }} + + //functions + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + open override func initialSetup() { + super.initialSetup() + onClick = { control in + control.toggle() + } + } + + open override func setup() { + super.setup() + + isAccessibilityElement = true + accessibilityTraits = .button + addSubview(mainStackView) + mainStackView.isUserInteractionEnabled = false + + mainStackView.addArrangedSubview(selectorStackView) + mainStackView.addArrangedSubview(errorLabel) + selectorStackView.addArrangedSubview(selectorView) + selectorStackView.addArrangedSubview(selectorLabelStackView) + selectorLabelStackView.addArrangedSubview(label) + selectorLabelStackView.addArrangedSubview(childLabel) + mainStackView.pinToSuperView() + } + + func updateLabels() { + + //deal with labels + if shouldShowLabels { + //add the stackview to hold the 2 labels + //top label + if let labelText { + label.surface = surface + label.disabled = disabled + label.attributes = labelTextAttributes + label.text = labelText + label.isHidden = false + } else if labelAttributedText != nil { + label.isHidden = false + + } else { + label.isHidden = true + } + + //bottom label + if let childText { + childLabel.text = childText + childLabel.surface = surface + childLabel.disabled = disabled + childLabel.attributes = childTextAttributes + childLabel.isHidden = false + + } else if childAttributedText != nil { + childLabel.isHidden = false + + } else { + childLabel.isHidden = true + } + selectorStackView.spacing = 12 + selectorLabelStackView.spacing = 4 + selectorLabelStackView.isHidden = false + + } else { + selectorStackView.spacing = 0 + selectorLabelStackView.spacing = 0 + selectorLabelStackView.isHidden = true + } + + //either add/remove the error from the main stack + if let errorText, shouldShowError { + errorLabel.text = errorText + errorLabel.surface = surface + errorLabel.disabled = disabled + mainStackView.spacing = 8 + errorLabel.isHidden = false + } else { + mainStackView.spacing = 0 + errorLabel.isHidden = true + } + } + + open override func reset() { + super.reset() + shouldUpdateView = false + label.reset() + childLabel.reset() + errorLabel.reset() + + label.textStyle = .boldBodyLarge + childLabel.textStyle = .bodyLarge + errorLabel.textStyle = .bodyMedium + + labelText = nil + labelTextAttributes = nil + labelAttributedText = nil + childText = nil + childTextAttributes = nil + childAttributedText = nil + showError = false + errorText = nil + inputId = nil + value = nil + isSelected = false + + shouldUpdateView = true + setNeedsUpdate() + } + + /// This will checkbox the state of the Selector and execute the actionBlock if provided. + open func toggle() {} + + //-------------------------------------------------- + // MARK: - State + //-------------------------------------------------- + open override func updateView() { + updateLabels() + selectorView.showError = showError + selectorView.isSelected = isSelected + selectorView.isHighlighted = isHighlighted + updateAccessibilityLabel() + } + + open override func updateAccessibilityLabel() { + setAccessibilityLabel(for: [label, childLabel, errorLabel]) + } +}