// // 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) } } }