diff --git a/VDSSample.xcodeproj/project.pbxproj b/VDSSample.xcodeproj/project.pbxproj index bb308b5..7cccad1 100644 --- a/VDSSample.xcodeproj/project.pbxproj +++ b/VDSSample.xcodeproj/project.pbxproj @@ -93,6 +93,7 @@ EAD062A52A3B5CDF0015965D /* Slider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAD062A42A3B5CDF0015965D /* Slider.swift */; }; EAD062AD2A3B86950015965D /* BadgeIndicatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAD062AC2A3B86950015965D /* BadgeIndicatorViewController.swift */; }; EAD068902A55FC11002E3A2D /* LoaderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAD0688F2A55FC11002E3A2D /* LoaderViewController.swift */; }; + EAEC94CF2BFCFE090064FB2F /* Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAEC94CE2BFCFE090064FB2F /* Test.swift */; }; EAEEEC942B1F824500531FC2 /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAEEEC932B1F824500531FC2 /* Bundle.swift */; }; EAF7F07D2899698800B287F5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EAF7F07B2899698800B287F5 /* Assets.xcassets */; }; EAF7F09C2899B92400B287F5 /* CheckboxItemViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F09B2899B92400B287F5 /* CheckboxItemViewController.swift */; }; @@ -189,6 +190,7 @@ EAD062A42A3B5CDF0015965D /* Slider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Slider.swift; sourceTree = ""; }; EAD062AC2A3B86950015965D /* BadgeIndicatorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeIndicatorViewController.swift; sourceTree = ""; }; EAD0688F2A55FC11002E3A2D /* LoaderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoaderViewController.swift; sourceTree = ""; }; + EAEC94CE2BFCFE090064FB2F /* Test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Test.swift; sourceTree = ""; }; EAEEEC932B1F824500531FC2 /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; EAF7F07B2899698800B287F5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; EAF7F09B2899B92400B287F5 /* CheckboxItemViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxItemViewController.swift; sourceTree = ""; }; @@ -350,6 +352,7 @@ EA3C3BB328996775000CA526 /* ToggleViewController.swift */, EAB2375F29E88D5D00AABE9A /* TooltipViewController.swift */, EAB2376B29E9E74900AABE9A /* TrailingTooltipLabelViewController.swift */, + EAEC94CE2BFCFE090064FB2F /* Test.swift */, ); path = ViewControllers; sourceTree = ""; @@ -532,6 +535,7 @@ EA4DB30428DCD25B00103EE3 /* BadgeViewController.swift in Sources */, EAB2376029E88D5D00AABE9A /* TooltipViewController.swift in Sources */, EA89204828B66CE2006B9984 /* ScrollViewKeyboardAvoiding.swift in Sources */, + EAEC94CF2BFCFE090064FB2F /* Test.swift in Sources */, EA0FC2C12912DC5500DF80B4 /* TextLinkCaretViewController.swift in Sources */, 71B23C312B921D740027F7D9 /* PaginationViewController.swift in Sources */, EAF7F09C2899B92400B287F5 /* CheckboxItemViewController.swift in Sources */, diff --git a/VDSSample/ViewControllers/MenuViewController.swift b/VDSSample/ViewControllers/MenuViewController.swift index 9ec0747..90cb912 100644 --- a/VDSSample/ViewControllers/MenuViewController.swift +++ b/VDSSample/ViewControllers/MenuViewController.swift @@ -66,6 +66,7 @@ class MenuCell: UITableViewCell { class MenuViewController: UITableViewController, TooltipLaunchable { static let items: [MenuComponent] = [ + MenuComponent(title: "Test", completed: true, viewController: TestViewController.self), MenuComponent(title: "DropShadow Tester", completed: true, viewController: DropShadowViewController.self), MenuComponent(title: "TableView Tester", completed: true, viewController: TableViewTestController.self), MenuComponent(title: "Badge", completed: true, viewController: BadgeViewController.self), diff --git a/VDSSample/ViewControllers/Test.swift b/VDSSample/ViewControllers/Test.swift new file mode 100644 index 0000000..167d991 --- /dev/null +++ b/VDSSample/ViewControllers/Test.swift @@ -0,0 +1,587 @@ +// +// Test.swift +// VDSSample +// +// Created by Matt Bruce on 5/21/24. +// + +import Foundation +import UIKit +import VDS +import VDSTokens +import Combine + +class TestViewController: BaseViewController { + + + + lazy var helperTextPositionPickerSelectorView = { + PickerSelectorView(title: "", + picker: self.picker, + items: EntryFieldBase2.HelperTextPlacement.allCases) + }() + + var labelTextField = TextField() + var errorTextField = TextField() + var successTextField = TextField() + var helperTextField = TextField() + var showErrorSwitch = Toggle() + var showSuccessSwitch = Toggle() + + override func viewDidLoad() { + super.viewDidLoad() + addContentTopView(view: component) + component.labelText = "Title For the Component Blah blah blah" + component.errorText = "Error For the Component" + component.helperText = "Helper For the Component" + setupPicker() + setupModel() + } + + override func setupForm(){ + super.setupForm() + let general = FormSection().with { + $0.title = "\n\nGeneral Settings" + } + + general.addFormRow(label: "Surface", view: surfacePickerSelectorView) + general.addFormRow(label: "Label Text", view: labelTextField) + general.addFormRow(label: "Helper Text Placement", view: helperTextPositionPickerSelectorView) + general.addFormRow(label: "Helper Text", view: helperTextField) + general.addFormRow(label: "Error", view: showErrorSwitch) + general.addFormRow(label: "Error Text", view: errorTextField) +// general.addFormRow(label: "Success", view: showSuccessSwitch) +// general.addFormRow(label: "Success Text", view: successTextField) + + append(section: general) + + showErrorSwitch.onChange = { [weak self] sender in + guard let self else { return } + self.component.showError = sender.isOn + if self.component.showError != sender.isOn { + self.showErrorSwitch.isOn = self.component.showError + } + } + +// showSuccessSwitch.onChange = { [weak self] sender in +// guard let self else { return } +// self.component.showSuccess = sender.isOn +// if self.component.showSuccess != sender.isOn { +// self.showSuccessSwitch.isOn = self.component.showSuccess +// } +// } + + labelTextField + .textPublisher + .sink { [weak self] text in + self?.component.labelText = text + }.store(in: &subscribers) + + helperTextField + .textPublisher + .sink { [weak self] text in + self?.component.helperText = text + }.store(in: &subscribers) + + errorTextField + .textPublisher + .sink { [weak self] text in + self?.component.errorText = text + }.store(in: &subscribers) + + + } + + func setupModel() { + //setup UI + surfacePickerSelectorView.text = component.surface.rawValue + helperTextPositionPickerSelectorView.text = component.helperTextPlacement.rawValue + labelTextField.text = component.labelText + helperTextField.text = component.helperText + errorTextField.text = component.errorText + } + + func setupPicker(){ + + surfacePickerSelectorView.onPickerDidSelect = { [weak self] item in + self?.component.surface = item + self?.contentTopView.backgroundColor = item.color + } + + helperTextPositionPickerSelectorView.onPickerDidSelect = { [weak self] item in + self?.component.helperTextPlacement = item + } + } +} + +open class EntryFieldBase2: Control, Changeable, FormFieldInternalValidatable { + + //-------------------------------------------------- + // 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 position of the helper text. + public enum HelperTextPlacement: String, CaseIterable { + case bottom, right + } + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + internal let mainStackView = UIStackView().with { + $0.axis = .vertical + $0.alignment = .fill + $0.spacing = 8 + $0.translatesAutoresizingMaskIntoConstraints = false + } + + internal let contentStackView = UIStackView().with { + $0.axis = .vertical + $0.alignment = .fill + $0.distribution = .fill + $0.spacing = 8 + $0.translatesAutoresizingMaskIntoConstraints = false + } + + /// only used for helperTextPosition == .right + internal let row1StackView = UIStackView().with { + $0.axis = .horizontal + $0.spacing = 8 + $0.alignment = .top + $0.distribution = .fillEqually + } + + /// only used for helperTextPosition == .right + internal let row2StackView = UIStackView().with { + $0.axis = .horizontal + $0.spacing = 8 + $0.alignment = .top + $0.distribution = .fillEqually + } + + internal var fieldStackView: UIStackView = { + return UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .horizontal + $0.distribution = .fill + $0.alignment = .top + } + }() + + /// This is a vertical stack used for the errorLabel and helperLabel. + internal var bottomContainerStackView: UIStackView = { + return UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .vertical + $0.distribution = .fill + $0.spacing = VDSLayout.space2X + } + }() + + /// This is the view that will be wrapped with the border for userInteraction. + /// The only subview of this view is the fieldStackView + internal var containerView: UIView = { + return UIView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + } + }() + + /// This is set by a local method. + internal var bottomContainerView: UIView! + + //-------------------------------------------------- + // MARK: - Configuration Properties + //-------------------------------------------------- + // Sizes are from InVision design specs. + internal var maxWidth: CGFloat { frame.size.width } + internal var minWidth: CGFloat { containerSize.width } + internal var containerSize: CGSize { CGSize(width: minWidth, height: 44) } + + internal let primaryColorConfiguration = ViewColorConfiguration().with { + $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forDisabled: true) + $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forDisabled: false) + } + + internal let secondaryColorConfiguration = ViewColorConfiguration().with { + $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forDisabled: true) + $0.setSurfaceColors(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark, forDisabled: false) + } + + internal var backgroundColorConfiguration = ControlColorConfiguration().with { + $0.setSurfaceColors(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark, forState: .normal) + $0.setSurfaceColors(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark, forState: .disabled) + $0.setSurfaceColors(VDSColor.feedbackErrorBackgroundOnlight, VDSColor.feedbackErrorBackgroundOndark, forState: .error) + $0.setSurfaceColors(VDSColor.feedbackErrorBackgroundOnlight, VDSColor.feedbackErrorBackgroundOndark, forState: [.error, .focused]) + } + + internal var borderColorConfiguration = ControlColorConfiguration().with { + $0.setSurfaceColors(VDSFormControlsColor.borderOnlight, VDSFormControlsColor.borderOndark, forState: .normal) + $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOnlight, forState: .focused) + $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOnlight, forState: [.focused, .error]) + $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled) + $0.setSurfaceColors(VDSColor.feedbackErrorOnlight, VDSColor.feedbackErrorOndark, forState: .error) + $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: [.disabled,.error]) + } + + 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) + } + + internal var readOnlyBorderColorConfiguration = ControlColorConfiguration().with { + $0.setSurfaceColors(VDSFormControlsColor.borderReadonlyOnlight, VDSFormControlsColor.borderReadonlyOndark, forState: .normal) + } + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + open var onChangeSubscriber: AnyCancellable? + + open var titleLabel = Label().with { + $0.setContentCompressionResistancePriority(.required, for: .vertical) + $0.textStyle = .bodySmall + } + + open var errorLabel = Label().with { + $0.setContentCompressionResistancePriority(.required, for: .vertical) + $0.textStyle = .bodySmall + $0.accessibilityValue = "error" + } + + open var helperLabel = Label().with { + $0.setContentCompressionResistancePriority(.required, for: .vertical) + $0.textStyle = .bodySmall + } + + open var statusIcon: Icon = Icon().with { + $0.size = .medium + } + + open var labelText: String? { didSet { setNeedsUpdate() } } + + open var helperText: String? { didSet { setNeedsUpdate() } } + + /// Whether not to show the error. + open var showError: Bool = false { didSet { setNeedsUpdate() } } + + /// FormFieldValidator + open var validator: (any FormFieldValidatorable)? + + /// Override UIControl state to add the .error state if showError is true. + open override var state: UIControl.State { + get { + var state = super.state + if showError || hasInternalError { + state.insert(.error) + } + return state + } + } + + open var errorText: String? { didSet { setNeedsUpdate() } } + + open var tooltipModel: Tooltip.TooltipModel? { didSet { setNeedsUpdate() } } + + open var transparentBackground: Bool = false { didSet { setNeedsUpdate() } } + + open var width: CGFloat? { didSet { setNeedsUpdate() } } + + open var inputId: String? { didSet { setNeedsUpdate() } } + + /// The text of this textField. + open var value: String? { + get { fatalError("must be read from subclass")} + } + + open var defaultValue: AnyHashable? { didSet { setNeedsUpdate() } } + + open var isRequired: Bool = false { didSet { setNeedsUpdate() } } + + open var isReadOnly: Bool = false { didSet { setNeedsUpdate() } } + + open var helperTextPlacement: HelperTextPlacement = .bottom { + didSet { + updateHelperTextPosition() + } + } + + open var rules = [AnyRule]() + + //-------------------------------------------------- + // 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() + // Configure Labels + titleLabel.translatesAutoresizingMaskIntoConstraints = false + + //add ContainerStackView + //this is the horizontal stack that contains + //the left, InputContainer, Icons, Buttons + containerView.addSubview(fieldStackView) + fieldStackView.pinToSuperView(.uniform(VDSLayout.space3X)) + + let fieldContainerView = getFieldContainer() + fieldContainerView.translatesAutoresizingMaskIntoConstraints = false + + //add the view to add input fields + fieldStackView.addArrangedSubview(fieldContainerView) + fieldStackView.addArrangedSubview(statusIcon) + fieldStackView.setCustomSpacing(VDSLayout.space3X, after: fieldContainerView) + + //get the container this is what show helper text, error text + //can include other for character count, max length + bottomContainerView = getBottomContainer() + + //this is the vertical stack that contains error text, helper text + bottomContainerStackView.addArrangedSubview(errorLabel) + bottomContainerStackView.addArrangedSubview(helperLabel) + + // Add arranged subviews to textFieldStackView + contentStackView.addArrangedSubview(containerView) + contentStackView.addArrangedSubview(bottomContainerView) + + // Add arranged subviews to mainStackView + mainStackView.addArrangedSubview(titleLabel) + mainStackView.addArrangedSubview(contentStackView) + + // Add mainStackView to the view + addSubview(mainStackView) + + mainStackView.pinToSuperView() + + // Initial position of the helper label + updateHelperTextPosition() + + // Set the UIImageView as the left view of the UITextField + let iconContainerView: UIView = UIView() + iconContainerView.addSubview(creditCardImageView) + creditCardImageView.pinToSuperView(.init(top: 0, left: 0, bottom: 0, right: 10)) + + textField.leftView = iconContainerView + textField.leftViewMode = .always + } + + /// Updates the UI + open override func updateView() { + super.updateView() + updateContainerView() + updateTitleLabel() + updateErrorLabel() + updateHelperLabel() + + let imageName = InputField.CreditCardType.generic.imageName(surface: surface) + creditCardImageView.image = BundleManager.shared.image(for: imageName) + } + + /// Resets to default settings. + open override func reset() { + super.reset() + titleLabel.reset() + errorLabel.reset() + helperLabel.reset() + + titleLabel.textStyle = .bodySmall + errorLabel.textStyle = .bodySmall + helperLabel.textStyle = .bodySmall + + labelText = nil + helperText = nil + showError = false + errorText = nil + tooltipModel = nil + transparentBackground = false + width = nil + inputId = nil + defaultValue = nil + isRequired = false + isReadOnly = false + onChange = nil + } + + //-------------------------------------------------- + // MARK: - Public Methods + //-------------------------------------------------- + /// Container for the area in which the user interacts. + open func getFieldContainer() -> UIView { + return 2textField + //fatalError("Subclass must return the view that contains the field/view the user will interact with.") + } + + /// Container for the area in which helper or error text presents. + open func getBottomContainer() -> UIView { + return bottomContainerStackView + } + + open func validate(){ + updateRules() + validator = FormFieldValidator(field: self, rules: rules) + validator?.validate() + setNeedsUpdate() + } + + open func updateTitleLabel() { + + //update the local vars for the label since we no + //long have a model + var attributes: [any LabelAttributeModel] = [] + var updatedLabelText = labelText + + //dealing with the "Optional" addition to the text + if let oldText = updatedLabelText, !isRequired, !oldText.hasSuffix("Optional") { + if isEnabled { + let optionColorAttr = ColorLabelAttribute(location: oldText.count + 2, + length: 8, + color: VDSColor.elementsSecondaryOnlight) + + attributes.append(optionColorAttr) + } + updatedLabelText = "\(oldText) Optional" + } + + if let tooltipModel { + attributes.append(TooltipLabelAttribute(surface: surface, model: tooltipModel, presenter: self)) + } + + //set the titleLabel + titleLabel.text = updatedLabelText + titleLabel.attributes = attributes + titleLabel.surface = surface + titleLabel.isEnabled = isEnabled + } + + open func updateErrorLabel(){ + if showError, let errorText { + errorLabel.text = errorText + errorLabel.surface = surface + errorLabel.isEnabled = isEnabled + errorLabel.isHidden = false + statusIcon.name = .error + statusIcon.surface = surface + statusIcon.isHidden = !isEnabled || state.contains(.focused) + } else if hasInternalError, let internalErrorText { + errorLabel.text = internalErrorText + errorLabel.surface = surface + errorLabel.isEnabled = isEnabled + errorLabel.isHidden = false + statusIcon.name = .error + statusIcon.surface = surface + statusIcon.isHidden = !isEnabled || state.contains(.focused) + } else { + statusIcon.isHidden = true + errorLabel.isHidden = true + } + statusIcon.color = iconColorConfiguration.getColor(self) + } + + open func updateHelperLabel(){ + //set the helper label position + if let helperText { + helperLabel.text = helperText + helperLabel.surface = surface + helperLabel.isEnabled = isEnabled + helperLabel.isHidden = false + } else { + helperLabel.isHidden = true + } + } + + //-------------------------------------------------- + // MARK: - Private Methods + //-------------------------------------------------- + internal func updateRules() { + rules.removeAll() + if self.isRequired { + let rule = RequiredRule() + if let errorText, !errorText.isEmpty { + rule.errorMessage = errorText + } else if let labelText{ + rule.errorMessage = "You must enter a \(labelText)" + } else { + rule.errorMessage = "You must enter a value" + } + rules.append(.init(rule)) + } + } + + internal func updateContainerView() { + containerView.backgroundColor = backgroundColorConfiguration.getColor(self) + containerView.layer.borderColor = borderColorConfiguration.getColor(self).cgColor + containerView.layer.borderWidth = VDSFormControls.borderWidth + containerView.layer.cornerRadius = VDSFormControls.borderRadius + } + + internal func updateHelperTextPosition() { + + titleLabel.removeFromSuperview() + helperLabel.removeFromSuperview() + + contentStackView.removeFromSuperview() + mainStackView.removeArrangedSubviews() + + //rows for helper-right + row1StackView.removeArrangedSubviews() + row2StackView.removeArrangedSubviews() + row1StackView.removeFromSuperview() + row2StackView.removeFromSuperview() + + switch helperTextPlacement { + case .bottom: + //add helper back into the contentView + bottomContainerStackView.addArrangedSubview(helperLabel) + mainStackView.addArrangedSubview(titleLabel) + mainStackView.addArrangedSubview(contentStackView) + + case .right: + //first row + row1StackView.addArrangedSubview(titleLabel) + //add spacer + row1StackView.addArrangedSubview(UIView()) + + //second row + row2StackView.addArrangedSubview(contentStackView) + //add under spacer + row2StackView.addArrangedSubview(helperLabel) + + //add 2 rows to vertical stack to create the grid + mainStackView.addArrangedSubview(row1StackView) + mainStackView.addArrangedSubview(row2StackView) + } + } + + internal let textField = VDS.TextField().with { + $0.translatesAutoresizingMaskIntoConstraints = false + } + + internal var creditCardImageView = UIImageView().with { + $0.height(20) + $0.width(32) + $0.isAccessibilityElement = false + $0.translatesAutoresizingMaskIntoConstraints = false + $0.contentMode = .scaleAspectFill + $0.clipsToBounds = true + } +} + +extension UIStackView { + public func removeArrangedSubviews() { + arrangedSubviews.forEach { removeArrangedSubview($0) } + } +}