// // 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() var widthTextField = NumericField() 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: "Width", view: widthTextField) 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) widthTextField .numberPublisher .sink { [weak self] number in self?.component.width = number?.cgFloatValue }.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: - Constraints //-------------------------------------------------- internal var widthConstraint: NSLayoutConstraint? //-------------------------------------------------- // 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: 150, 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() // Add mainStackView to the view addSubview(mainStackView) let layoutGuide = UILayoutGuide() addLayoutGuide(layoutGuide) layoutGuide .pinTop() .pinLeading() .pinBottom() .pinTrailingLessThanOrEqualTo(anchor: trailingAnchor, constant: 0, priority: .defaultHigh) addSubview(mainStackView) mainStackView.pin(layoutGuide) widthConstraint = layoutGuide.width(constant: containerSize.width).deactivate() //add ContainerStackView //this is the horizontal stack that contains //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) // 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() updateContainerWidth() 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 textField //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 } } open func updateContainerWidth() { if let width, width >= minWidth, width <= maxWidth, helperTextPlacement != .right { widthConstraint?.constant = width widthConstraint?.activate() } else { widthConstraint?.deactivate() } } //-------------------------------------------------- // 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) } } } class WidthView: View { let stackView = UIStackView() let customView = UIView() var customViewWidthConstraint: NSLayoutConstraint! override func setup() { super.setup() setupStackView() setupCustomView() } func setupStackView() { stackView.axis = .vertical stackView.distribution = .fillProportionally stackView.alignment = .fill stackView.translatesAutoresizingMaskIntoConstraints = false addSubview(stackView) stackView.pinToSuperView() } func setupCustomView() { customView.translatesAutoresizingMaskIntoConstraints = false customView.backgroundColor = .red let otherView = View() otherView.backgroundColor = .green stackView.addArrangedSubview(otherView) otherView.addSubview(customView) customView.pinTop().pinBottom().pinLeading().pinTrailingLessThanOrEqualTo() // Add width constraint with lower priority customViewWidthConstraint = customView.widthAnchor.constraint(equalToConstant: 60) customViewWidthConstraint.priority = UILayoutPriority(999) // Initially activate or deactivate based on your requirement customViewWidthConstraint.isActive = false } func setCustomWidth(to width: CGFloat) { customViewWidthConstraint.constant = width customViewWidthConstraint.isActive = true layoutIfNeeded() } func resetWidthToFill() { customViewWidthConstraint.isActive = false layoutIfNeeded() } } class WidthViewController: BaseViewController { var widthTextField = NumericField() override func viewDidLoad() { super.viewDidLoad() addContentTopView(view: component) component.height(150) } override func setupForm(){ super.setupForm() addFormRow(label: "Width", view: widthTextField) widthTextField .numberPublisher .sink { [weak self] number in if let width = number?.cgFloatValue, width > 50, width < 250 { self?.component.setCustomWidth(to: width) } else { self?.component.resetWidthToFill() } }.store(in: &subscribers) } } class CircleIconViewController: BaseViewController { lazy var usePickerSelectorView = { PickerSelectorView(title: "", picker: self.picker, items: [.primary, .secondary]) }() lazy var colorPickerSelectorView = { PickerSelectorView(title: "", picker: self.picker, items: UIColor.VDSColor.allCases) }() lazy var namePickerSelectorView = { PickerSelectorView(title: "", picker: self.picker, items: Icon.Name.all.sorted{ $0.rawValue < $1.rawValue }) }() lazy var sizePickerSelectorView = { PickerSelectorView(title: "", picker: self.picker, items: Icon.Size.allCases) }() override func viewDidLoad() { super.viewDidLoad() addContentTopView(view: .makeWrapper(for: component)) setupPicker() setupModel() } override func setupForm(){ super.setupForm() addFormRow(label: "Surface", view: surfacePickerSelectorView) addFormRow(label: "Use", view: usePickerSelectorView) addFormRow(label: "Name", view: namePickerSelectorView) addFormRow(label: "Size", view: sizePickerSelectorView) } func setupModel() { //setup UI surfacePickerSelectorView.text = component.surface.rawValue sizePickerSelectorView.text = component.iconModel.size.rawValue namePickerSelectorView.text = component.iconModel.name.rawValue usePickerSelectorView.text = component.use.rawValue } func setupPicker(){ surfacePickerSelectorView.onPickerDidSelect = { [weak self] item in self?.component.surface = item self?.contentTopView.backgroundColor = item.color } sizePickerSelectorView.onPickerDidSelect = { [weak self] item in self?.updateIconModel() } usePickerSelectorView.onPickerDidSelect = { [weak self] item in self?.component.use = item } namePickerSelectorView.onPickerDidSelect = { [weak self] item in self?.updateIconModel() } } func updateIconModel() { let model = CircleIcon.IconModel(name: namePickerSelectorView.selectedItem, size: sizePickerSelectorView.selectedItem) component.iconModel = model } } open class CircleIcon: View { open var use: Use = .primary { didSet { setNeedsUpdate() } } open var icon = Icon() open var iconModel = IconModel() { didSet { setNeedsUpdate() } } private var contentView = View() open override func setup() { super.setup() addSubview(contentView) contentView.addSubview(icon) icon.pinToSuperView(.uniform(VDSLayout.space1X)) .pinCenterX() .pinCenterY() contentView.pinTop() contentView.pinLeading() contentView.pinTrailingLessThanOrEqualTo() contentView.pinBottomLessThanOrEqualTo() } open override var intrinsicContentSize: CGSize { contentView.frame.size } private let primaryIconColor = SurfaceColorConfiguration(VDSColor.paletteWhite, VDSColor.paletteWhite) private let primaryBackgroundColor = SurfaceColorConfiguration(VDSColor.paletteRed, VDSColor.paletteRed) private let secondaryIconColor = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteWhite) private let secondaryBackgroundColor = SurfaceColorConfiguration(VDSColor.paletteGray65, VDSColor.paletteGray44) private var iconColor: UIColor { use == .primary ? primaryIconColor.getColor(surface) : secondaryIconColor.getColor(surface) } private var layerBackgroundColor: UIColor { use == .primary ? primaryBackgroundColor.getColor(surface) : secondaryBackgroundColor.getColor(surface) } open override func updateView() { super.updateView() icon.surface = .dark icon.name = iconModel.name icon.size = iconModel.size icon.color = iconColor contentView.layer.backgroundColor = layerBackgroundColor.cgColor } open override func layoutSubviews() { super.layoutSubviews() contentView.layer.cornerRadius = min(bounds.width, bounds.height) / 2 contentView.layer.masksToBounds = true } /// Model that represents the options available for the descriptive icon. public struct IconModel { /// A representation that will be used to render the icon with corresponding name. public var name: Icon.Name /// Enum for a preset height and width for the icon. public var size: Icon.Size public init(name: Icon.Name = .threedAd, size: Icon.Size = .XLarge) { self.name = name self.size = size } } }