// // EntryField.swift // VDS // // Created by Matt Bruce on 10/3/22. // import Foundation import UIKit import VDSColorTokens import VDSFormControlsTokens import Combine open class EntryField: Control { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- internal var stackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical $0.distribution = .fill } }() internal var titleLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) } internal var errorLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) } internal var helperLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) } internal var containerView: UIView = { return UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false } }() internal var tooltipView: UIView? //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- // Sizes are from InVision design specs. internal let containerSize = CGSize(width: 45, height: 45) internal let primaryColorConfig = DisabledSurfaceColorConfiguration().with { $0.disabled.lightColor = VDSColor.interactiveDisabledOnlight $0.disabled.darkColor = VDSColor.interactiveDisabledOndark $0.enabled.lightColor = VDSColor.elementsPrimaryOnlight $0.enabled.darkColor = VDSColor.elementsPrimaryOndark } internal let secondaryColorConfig = DisabledSurfaceColorConfiguration().with { $0.disabled.lightColor = VDSColor.interactiveDisabledOnlight $0.disabled.darkColor = VDSColor.interactiveDisabledOndark $0.enabled.lightColor = VDSColor.elementsSecondaryOnlight $0.enabled.darkColor = VDSColor.elementsSecondaryOndark } internal lazy var backgroundColorConfiguration: AnyColorable = { return getBackgroundConfig() }() internal lazy var borderColorConfiguration: AnyColorable = { return getBorderConfig() }() //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- @Proxy(\.model.labelText) open var labelText: String? @Proxy(\.model.helperText) open var helperText: String? @Proxy(\.model.showError) open var showError: Bool @Proxy(\.model.errorText) open var errorText: String? @Proxy(\.model.tooltipTitle) open var tooltipTitle: String? @Proxy(\.model.tooltipContent) open var tooltipContent: String? @Proxy(\.model.transparentBackground) open var transparentBackground: Bool @Proxy(\.model.width) open var width: CGFloat? @Proxy(\.model.maxLength) open var maxLength: Int? @Proxy(\.model.inputId) open var inputId: String? @Proxy(\.model.value) open var value: AnyHashable? @Proxy(\.model.defaultVaue) open var defaultValue: AnyHashable? @Proxy(\.model.required) open var required: Bool @Proxy(\.model.readOnly) open var readOnly: Bool //-------------------------------------------------- // MARK: - Constraints //-------------------------------------------------- internal var heightConstraint: NSLayoutConstraint? internal var widthConstraint: NSLayoutConstraint? //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- open override func setup() { super.setup() //add tapGesture to self publisher(for: UITapGestureRecognizer()).sink { [weak self] _ in self?.sendActions(for: .touchUpInside) }.store(in: &subscribers) isAccessibilityElement = true accessibilityTraits = .button addSubview(stackView) //create the wrapping view heightConstraint = containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: containerSize.height) heightConstraint?.isActive = true widthConstraint = containerView.widthAnchor.constraint(equalToConstant: 0) let container = getContainer() stackView.addArrangedSubview(titleLabel) stackView.addArrangedSubview(container) stackView.addArrangedSubview(errorLabel) stackView.addArrangedSubview(helperLabel) stackView.setCustomSpacing(4, after: titleLabel) stackView.setCustomSpacing(8, after: container) stackView.setCustomSpacing(8, after: errorLabel) stackView.topAnchor.constraint(equalTo: topAnchor).isActive = true stackView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true stackView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true stackView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true titleLabel.textColorConfiguration = primaryColorConfig.eraseToAnyColorable() errorLabel.textColorConfiguration = primaryColorConfig.eraseToAnyColorable() helperLabel.textColorConfiguration = secondaryColorConfig.eraseToAnyColorable() } open func getContainer() -> UIView { return containerView } open func getBackgroundConfig() -> AnyColorable { return ErrorDisabledSurfaceColorConfiguration().with { $0.enabled.lightColor = VDSFormControlsColor.backgroundOnlight $0.enabled.darkColor = VDSFormControlsColor.backgroundOndark $0.disabled.lightColor = VDSFormControlsColor.backgroundOnlight $0.disabled.darkColor = VDSFormControlsColor.backgroundOndark //error doesn't care enabled/disable $0.error.lightColor = VDSColor.feedbackErrorBackgroundOnlight $0.error.darkColor = VDSColor.feedbackErrorBackgroundOndark }.eraseToAnyColorable() } open func getBorderConfig() -> AnyColorable { return ErrorDisabledSurfaceColorConfiguration().with { $0.enabled.lightColor = VDSFormControlsColor.borderOnlight $0.enabled.darkColor = VDSFormControlsColor.borderOnlight $0.disabled.lightColor = VDSColor.interactiveDisabledOnlight $0.disabled.darkColor = VDSColor.interactiveDisabledOndark //error doesn't care enabled/disable $0.error.lightColor = VDSColor.feedbackErrorOnlight $0.error.darkColor = VDSColor.feedbackErrorOndark }.eraseToAnyColorable() } open func getToolTipView(viewModel: ModelType) -> UIView? { guard let toolTipTitleModel = viewModel.tooltipTitleModel, let toolTipContentModel = viewModel.tooltipContentModel else { return nil } let stack = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical $0.distribution = .fill $0.spacing = 4 } let title = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) } title.set(with: toolTipTitleModel) let content = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) } content.set(with: toolTipContentModel) stack.addArrangedSubview(title) stack.addArrangedSubview(content) stack.backgroundColor = backgroundColorConfiguration.getColor(viewModel) return stack } open func showToolTipView(){ } public override func reset() { super.reset() setAccessibilityLabel() } //-------------------------------------------------- // MARK: - State //-------------------------------------------------- open override func updateView(viewModel: ModelType) { containerView.backgroundColor = backgroundColorConfiguration.getColor(viewModel) containerView.layer.borderColor = borderColorConfiguration.getColor(viewModel).cgColor containerView.layer.borderWidth = 1 containerView.layer.cornerRadius = 4 updateTitleLabel(viewModel: viewModel) updateErrorLabel(viewModel: viewModel) updateHelperLabel(viewModel: viewModel) setAccessibilityHint() backgroundColor = viewModel.surface.color setNeedsLayout() layoutIfNeeded() } open func updateTitleLabel(viewModel: ModelType) { //update the title model if the required flag is false var titleLabelModel = viewModel.labelModel .addOptional(required: viewModel.required, colorConfiguration: secondaryColorConfig) //tooltip action if let view = getToolTipView(viewModel: viewModel) { tooltipView = view let toolTipAction = PassthroughSubject() titleLabelModel = titleLabelModel.addToolTip(action: toolTipAction, colorConfiguration: primaryColorConfig) toolTipAction.sink { [weak self] in self?.showToolTipView() }.store(in: &subscribers) } else { tooltipView = nil } titleLabel.set(with: titleLabelModel) } open func updateErrorLabel(viewModel: ModelType){ if viewModel.showError, let errorLabelModel = viewModel.errorLabelModel { errorLabel.set(with: errorLabelModel) errorLabel.isHidden = false } else { errorLabel.isHidden = true } } open func updateHelperLabel(viewModel: ModelType){ //set the helper label position if let helperLabelModel = viewModel.helperLabelModel { helperLabel.set(with: helperLabelModel) helperLabel.isHidden = false } else { helperLabel.isHidden = true } } } //-------------------------------------------------- // MARK: - Color Class Configurations //-------------------------------------------------- internal class ErrorDisabledSurfaceColorConfiguration: DisabledSurfaceColorable { typealias ModelType = Errorable & Surfaceable & Disabling var error = SurfaceColorConfiguration() var disabled = SurfaceColorConfiguration() var enabled = SurfaceColorConfiguration() required public init(){} func getColor(_ viewModel: any ModelType) -> UIColor { //only show error is enabled and showError == true let showErrorColor = !viewModel.disabled && viewModel.showError if showErrorColor { return error.getColor(viewModel) } else { return getDisabledColor(viewModel) } } } extension DefaultLabelModel { public func addOptional(required: Bool, colorConfiguration: DisabledSurfaceColorConfiguration) -> DefaultLabelModel { guard let text = text, !required else { return self} let optionColorAttr = ColorLabelAttribute(location: text.count + 2, length: 8, color: colorConfiguration.getColor(self)) let newText = "\(text) Optional" return copyWith { $0.text = newText $0.attributes = [optionColorAttr] } } public func addToolTip(action: PassthroughSubject, colorConfiguration: DisabledSurfaceColorConfiguration) -> DefaultLabelModel { guard let text = text else { return self} var newAttributes = attributes ?? [] let newText = "\(text) " //create a little space between the final character and tooltip image let toolTip = ToolTipLabelAttribute(action: action, location: newText.count, length: 1, tintColor: colorConfiguration.getColor(self)) newAttributes.append(toolTip) return copyWith { $0.text = newText $0.attributes = newAttributes } } }