diff --git a/VDS/Components/DropdownSelect/DropdownSelect.swift b/VDS/Components/DropdownSelect/DropdownSelect.swift index a4a7c741..ac5cb29b 100644 --- a/VDS/Components/DropdownSelect/DropdownSelect.swift +++ b/VDS/Components/DropdownSelect/DropdownSelect.swift @@ -111,16 +111,12 @@ open class DropdownSelect: EntryFieldBase { $0.font = TextStyle.bodyLarge.font } - /// Determines the placement of the helper text. - open var helperTextPlacement: HelperTextPlacement = .bottom { didSet { setNeedsUpdate() } } - open var optionsPicker = UIPickerView() //-------------------------------------------------- // MARK: - Constraints //-------------------------------------------------- internal var inlineWidthConstraint: NSLayoutConstraint? - internal var titleLabelWidthConstraint: NSLayoutConstraint? //-------------------------------------------------- // MARK: - Configuration Properties @@ -133,13 +129,8 @@ open class DropdownSelect: EntryFieldBase { /// 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() - widthConstraint?.activate() + super.setup() - titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal) - titleLabel.setContentHuggingPriority(.required, for: .horizontal) - titleLabelWidthConstraint = titleLabel.width(constant: 0) - fieldStackView.isAccessibilityElement = true fieldStackView.accessibilityLabel = "Dropdown Select" inlineDisplayLabel.isAccessibilityElement = true @@ -285,28 +276,6 @@ open class DropdownSelect: EntryFieldBase { statusIcon.color = iconColorConfiguration.getColor(self) } - open override func updateHelperLabel(){ - //remove first - secondaryStackView.removeFromSuperview() - helperLabel.removeFromSuperview() - - super.updateHelperLabel() - - //set the helper label position - if helperText != nil { - if helperTextPlacement == .right { - horizontalStackView.addArrangedSubview(secondaryStackView) - horizontalStackView.addArrangedSubview(helperLabel) - primaryStackView.addArrangedSubview(horizontalStackView) - } else { - bottomContainerStackView.addArrangedSubview(helperLabel) - primaryStackView.addArrangedSubview(secondaryStackView) - } - } else { - primaryStackView.addArrangedSubview(secondaryStackView) - } - } - open override func updateAccessibility() { super.updateAccessibility() let selectedOption = selectedOptionLabel.text ?? "" @@ -343,13 +312,7 @@ open class DropdownSelect: EntryFieldBase { setNeedsUpdate() UIAccessibility.post(notification: .layoutChanged, argument: fieldStackView) } - - open override func layoutSubviews() { - super.layoutSubviews() - titleLabelWidthConstraint?.constant = containerView.frame.width - titleLabelWidthConstraint?.isActive = helperTextPlacement == .right - } - + open override var canBecomeFirstResponder: Bool { return dropdownField.canBecomeFirstResponder } diff --git a/VDS/Components/TextFields/EntryFieldBase.swift b/VDS/Components/TextFields/EntryFieldBase.swift index f766fafd..631cf029 100644 --- a/VDS/Components/TextFields/EntryFieldBase.swift +++ b/VDS/Components/TextFields/EntryFieldBase.swift @@ -36,38 +36,41 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { public enum HelperTextPlacement: String, CaseIterable { case bottom, right } - + //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- - internal var primaryStackView: UIStackView = { - return UIStackView().with { - $0.translatesAutoresizingMaskIntoConstraints = false - $0.axis = .vertical - $0.distribution = .fill - $0.alignment = .leading - } - }() - - /// This is the veritcal stack view that has 2 rows, the containerView and the return view - /// of the getBottomContainer() method, by default returns the bottomContainerStackView. - internal let secondaryStackView = UIStackView().with { - $0.translatesAutoresizingMaskIntoConstraints = false + internal let mainStackView = UIStackView().with { $0.axis = .vertical - $0.distribution = .fill + $0.alignment = .fill + $0.spacing = 8 + $0.translatesAutoresizingMaskIntoConstraints = false } - /// 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 - } - }() + 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 + } - /// This is a horizontal Stack View that is placed inside the containterView (bordered view) - /// The first arrangedView will be the view from getFieldContainer() - /// The second view is the statusIcon. internal var fieldStackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false @@ -86,9 +89,18 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { $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! - open var rules = [AnyRule]() - //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- @@ -136,7 +148,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- - open var onChangeSubscriber: AnyCancellable? + open var onChangeSubscriber: AnyCancellable? open var titleLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) @@ -199,36 +211,34 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { open var isRequired: Bool = false { didSet { setNeedsUpdate() } } open var isReadOnly: Bool = false { didSet { setNeedsUpdate() } } - - //-------------------------------------------------- - // MARK: - Constraints - //-------------------------------------------------- - internal var heightConstraint: NSLayoutConstraint? - internal var widthConstraint: NSLayoutConstraint? - + + 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() - - isAccessibilityElement = false - addSubview(primaryStackView) - //create the wrapping view - heightConstraint = containerView.heightGreaterThanEqualTo(constant: containerSize.height) - widthConstraint = containerView.width(constant: frame.size.width) - - secondaryStackView.addArrangedSubview(containerView) - secondaryStackView.setCustomSpacing(8, after: containerView) + // Add mainStackView to the view + addSubview(mainStackView) + + mainStackView.pinToSuperView() //add ContainerStackView //this is the horizontal stack that contains - //the left, InputContainer, Icons, Buttons + //InputContainer, Icons, Buttons containerView.addSubview(fieldStackView) fieldStackView.pinToSuperView(.uniform(VDSLayout.space3X)) - + let fieldContainerView = getFieldContainer() fieldContainerView.translatesAutoresizingMaskIntoConstraints = false @@ -239,29 +249,33 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { //get the container this is what show helper text, error text //can include other for character count, max length - let bottomContainer = getBottomContainer() + bottomContainerView = getBottomContainer() //this is the vertical stack that contains error text, helper text bottomContainerStackView.addArrangedSubview(errorLabel) bottomContainerStackView.addArrangedSubview(helperLabel) - primaryStackView.addArrangedSubview(titleLabel) - primaryStackView.addArrangedSubview(secondaryStackView) - secondaryStackView.addArrangedSubview(bottomContainer) - - primaryStackView.setCustomSpacing(4, after: titleLabel) - - primaryStackView - .pinTop() - .pinLeading() - .pinTrailing(0, .defaultHigh) - .pinBottom(0, .defaultHigh) - - titleLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() - errorLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() - helperLabel.textColorConfiguration = secondaryColorConfiguration.eraseToAnyColorable() + // 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() } - + + /// Updates the UI + open override func updateView() { + super.updateView() + updateContainerView() + updateTitleLabel() + updateErrorLabel() + updateHelperLabel() + } + /// Resets to default settings. open override func reset() { super.reset() @@ -284,48 +298,12 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { defaultValue = nil isRequired = false isReadOnly = false - onChange = nil + onChange = nil } - - /// Used to make changes to the View based off a change events or from local properties. - open override func updateView() { - super.updateView() - updateContainerView() - updateTitleLabel() - updateErrorLabel() - updateHelperLabel() - updateContainerWidth() - } - - open func validate(){ - updateRules() - validator = FormFieldValidator(field: self, rules: rules) - validator?.validate() - setNeedsUpdate() - } - - //-------------------------------------------------- - // MARK: - Private Methods - //-------------------------------------------------- - internal func updateContainerView() { - containerView.backgroundColor = backgroundColorConfiguration.getColor(self) - containerView.layer.borderColor = isReadOnly ? readOnlyBorderColorConfiguration.getColor(self).cgColor : borderColorConfiguration.getColor(self).cgColor - containerView.layer.borderWidth = VDSFormControls.borderWidth - containerView.layer.cornerRadius = VDSFormControls.borderRadius - } - + //-------------------------------------------------- // MARK: - Public Methods //-------------------------------------------------- - open func updateContainerWidth() { - if let width, width > minWidth && width < maxWidth { - widthConstraint?.constant = width - } else { - widthConstraint?.constant = maxWidth >= minWidth ? maxWidth : minWidth - } - widthConstraint?.activate() - } - /// Container for the area in which the user interacts. open func getFieldContainer() -> UIView { fatalError("Subclass must return the view that contains the field/view the user will interact with.") @@ -335,22 +313,14 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { open func getBottomContainer() -> UIView { return bottomContainerStackView } - - 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)) - } + + 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 @@ -380,7 +350,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { titleLabel.surface = surface titleLabel.isEnabled = isEnabled } - + open func updateErrorLabel(){ if showError, let errorText { errorLabel.text = errorText @@ -416,4 +386,73 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { 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) + } + } +} + +extension UIStackView { + public func removeArrangedSubviews() { + arrangedSubviews.forEach { removeArrangedSubview($0) } + } } diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index 60adf47a..737404d4 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -34,7 +34,6 @@ open class InputField: EntryFieldBase { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- - internal var titleLabelWidthConstraint: NSLayoutConstraint? internal override var minWidth: CGFloat { fieldType.handler().minWidth } internal override var maxWidth: CGFloat { let frameWidth = frame.size.width @@ -172,25 +171,17 @@ open class InputField: EntryFieldBase { /// If given, this will be shown if showSuccess if true. open var successText: String? { didSet { setNeedsUpdate() } } - - /// Determines the placement of the helper text. - open var helperTextPlacement: HelperTextPlacement = .bottom { didSet { setNeedsUpdate() } } //-------------------------------------------------- // 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() - - titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal) - titleLabel.setContentHuggingPriority(.required, for: .horizontal) - titleLabelWidthConstraint = titleLabel.width(constant: 0) - + super.setup() textField.heightAnchor.constraint(equalToConstant: 20).isActive = true textField.delegate = self - primaryStackView.addArrangedSubview(successLabel) - primaryStackView.setCustomSpacing(8, after: successLabel) + mainStackView.addArrangedSubview(successLabel) + mainStackView.setCustomSpacing(8, after: successLabel) fieldStackView.addArrangedSubview(actionTextLink) @@ -262,27 +253,6 @@ open class InputField: EntryFieldBase { } } - open override func updateHelperLabel(){ - //remove first - secondaryStackView.removeFromSuperview() - helperLabel.removeFromSuperview() - - super.updateHelperLabel() - - //set the helper label position - if helperText != nil { - if helperTextPlacement == .right { - horizontalStackView.addArrangedSubview(secondaryStackView) - horizontalStackView.addArrangedSubview(helperLabel) - primaryStackView.addArrangedSubview(horizontalStackView) - } else { - bottomContainerStackView.addArrangedSubview(helperLabel) - primaryStackView.addArrangedSubview(secondaryStackView) - } - } else { - primaryStackView.addArrangedSubview(secondaryStackView) - } - } override func updateRules() { super.updateRules() @@ -318,12 +288,6 @@ open class InputField: EntryFieldBase { set { super.accessibilityElements = newValue } } - open override func layoutSubviews() { - super.layoutSubviews() - titleLabelWidthConstraint?.constant = containerView.frame.width - titleLabelWidthConstraint?.isActive = helperTextPlacement == .right - } - open override var canBecomeFirstResponder: Bool { return textField.canBecomeFirstResponder }