From fc3ce375ac7502cc6eb2eec527eed121c7706ca4 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 8 Aug 2022 10:47:17 -0500 Subject: [PATCH] refactored into base control Signed-off-by: Matt Bruce --- VDS.xcodeproj/project.pbxproj | 16 + VDS/Components/Checkbox/Checkbox.swift | 291 +--------------- VDS/Components/Checkbox/CheckboxModel.swift | 68 +--- VDS/Components/RadioButton/RadioButton.swift | 284 +--------------- .../RadioButton/RadioButtonModel.swift | 69 +--- .../SelectorBase/SelectorBase.swift | 316 ++++++++++++++++++ .../SelectorBase/SelectorModel.swift | 79 +++++ 7 files changed, 443 insertions(+), 680 deletions(-) create mode 100644 VDS/Components/SelectorBase/SelectorBase.swift create mode 100644 VDS/Components/SelectorBase/SelectorModel.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index ed7cbbf0..c6251dea 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -62,6 +62,8 @@ EAF7F0FB289DB1AC00B287F5 /* VDSColorTokens.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = EAF7F0E9289DB0DA00B287F5 /* VDSColorTokens.xcframework */; }; EAF7F11728A1475A00B287F5 /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F11528A1475A00B287F5 /* RadioButton.swift */; }; EAF7F11828A1475A00B287F5 /* RadioButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F11628A1475A00B287F5 /* RadioButtonModel.swift */; }; + EAF7F12C28A1617600B287F5 /* SelectorBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F12B28A1617600B287F5 /* SelectorBase.swift */; }; + EAF7F12F28A1619600B287F5 /* SelectorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F12E28A1619600B287F5 /* SelectorModel.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -135,6 +137,8 @@ EAF7F0EB289DB0DA00B287F5 /* VDSLayoutTokens.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = VDSLayoutTokens.xcframework; path = ../SharedFrameworks/VDSLayoutTokens.xcframework; sourceTree = ""; }; EAF7F11528A1475A00B287F5 /* RadioButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = ""; }; EAF7F11628A1475A00B287F5 /* RadioButtonModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadioButtonModel.swift; sourceTree = ""; }; + EAF7F12B28A1617600B287F5 /* SelectorBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectorBase.swift; sourceTree = ""; }; + EAF7F12E28A1619600B287F5 /* SelectorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectorModel.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -223,6 +227,7 @@ EAF7F092289985E200B287F5 /* Checkbox */, EA3361A0288B1E6F0071C351 /* Toggle */, EA3362412892EF700071C351 /* Label */, + EAF7F12D28A1617A00B287F5 /* SelectorBase */, ); path = Components; sourceTree = ""; @@ -361,6 +366,15 @@ path = RadioButton; sourceTree = ""; }; + EAF7F12D28A1617A00B287F5 /* SelectorBase */ = { + isa = PBXGroup; + children = ( + EAF7F12B28A1617600B287F5 /* SelectorBase.swift */, + EAF7F12E28A1619600B287F5 /* SelectorModel.swift */, + ); + path = SelectorBase; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -478,8 +492,10 @@ EAF7F0B5289C126F00B287F5 /* UILabel.swift in Sources */, EAF7F0A6289B0CE000B287F5 /* Resetable.swift in Sources */, EA3361C328902D960071C351 /* Toggle.swift in Sources */, + EAF7F12C28A1617600B287F5 /* SelectorBase.swift in Sources */, EAF7F0A0289AB7EC00B287F5 /* View.swift in Sources */, EAF7F11828A1475A00B287F5 /* RadioButtonModel.swift in Sources */, + EAF7F12F28A1619600B287F5 /* SelectorModel.swift in Sources */, EA3362402892EF6C0071C351 /* Label.swift in Sources */, EAF7F0B3289B1ADC00B287F5 /* LabelAttributeAction.swift in Sources */, EA33622E2891EA3C0071C351 /* DispatchQueue+Once.swift in Sources */, diff --git a/VDS/Components/Checkbox/Checkbox.swift b/VDS/Components/Checkbox/Checkbox.swift index df42e39d..c24212db 100644 --- a/VDS/Components/Checkbox/Checkbox.swift +++ b/VDS/Components/Checkbox/Checkbox.swift @@ -19,58 +19,7 @@ import Combine public class Checkbox: CheckboxBase{} -open class CheckboxBase: Control, Changable { - - //-------------------------------------------------- - // MARK: - Private Properties - //-------------------------------------------------- - private var mainStackView: UIStackView = { - let stackView = UIStackView() - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.alignment = .top - stackView.axis = .vertical - return stackView - }() - - private var checkboxStackView: UIStackView = { - let stackView = UIStackView() - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.alignment = .top - stackView.axis = .horizontal - return stackView - }() - - private var checkboxLabelStackView: UIStackView = { - let stackView = UIStackView() - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.axis = .vertical - return stackView - }() - - private var primaryLabel: Label = { - let label = Label() - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - private var secondaryLabel: Label = { - let label = Label() - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - private var errorLabel: Label = { - let label = Label() - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - private var checkboxView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - return view - }() - +open class CheckboxBase: SelectorBase { //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- @@ -114,77 +63,7 @@ open class CheckboxBase: Control, Changable config.forTrue.darkColor = VDSColor.elementsPrimaryOnlight return config }() - - //-------------------------------------------------- - // MARK: - Public Properties - //-------------------------------------------------- - public var onChange: Blocks.ActionBlock? - @Proxy(\.model.id) - open var id: String? - - @Proxy(\.model.on) - open var isOn: Bool - - @Proxy(\.model.labelText) - open var labelText: String? - - @Proxy(\.model.childText) - open var childText: String? - - @Proxy(\.model.showError) - open var showError: Bool - - @Proxy(\.model.errorText) - open var errorText: String? - - @Proxy(\.model.inputId) - open var inputId: String? - - @Proxy(\.model.value) - open var value: AnyHashable? - - @Proxy(\.model.dataAnalyticsTrack) - open var dataAnalyticsTrack: String? - - @Proxy(\.model.dataClickStream) - open var dataClickStream: String? - - @Proxy(\.model.dataTrack) - open var dataTrack: String? - - @Proxy(\.model.accessibilityHintEnabled) - open var accessibilityHintEnabled: String? - - @Proxy(\.model.accessibilityHintDisabled) - open var accessibilityHintDisabled: String? - - @Proxy(\.model.accessibilityValueEnabled) - open var accessibilityValueEnabled: String? - - @Proxy(\.model.accessibilityValueDisabled) - open var accessibilityValueDisabled: String? - - @Proxy(\.model.accessibilityLabelEnabled) - open var accessibilityLabelEnabled: String? - - @Proxy(\.model.accessibilityLabelDisabled) - open var accessibilityLabelDisabled: String? - - //-------------------------------------------------- - // MARK: - Computed Properties - //-------------------------------------------------- - open override var isEnabled: Bool { - get { !model.disabled } - set { - //create local vars for clear coding - let disabled = !newValue - if model.disabled != disabled { - model.disabled = disabled - } - } - } - //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- @@ -199,108 +78,18 @@ open class CheckboxBase: Control, Changable required public init?(coder: NSCoder) { super.init(with: ModelType()) } - - //-------------------------------------------------- - // MARK: - Constraints - //-------------------------------------------------- - - private var knobLeadingConstraint: NSLayoutConstraint? - private var knobTrailingConstraint: NSLayoutConstraint? - private var knobHeightConstraint: NSLayoutConstraint? - private var knobWidthConstraint: NSLayoutConstraint? - private var checkboxHeightConstraint: NSLayoutConstraint? - private var checkboxWidthConstraint: NSLayoutConstraint? - - //functions - //-------------------------------------------------- - // MARK: - Lifecycle - //-------------------------------------------------- - - open override func setup() { - super.setup() - self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(Checkbox.toggleAndAction))) - isAccessibilityElement = true - accessibilityTraits = .button - addSubview(mainStackView) - - mainStackView.addArrangedSubview(checkboxStackView) - mainStackView.addArrangedSubview(errorLabel) - checkboxStackView.addArrangedSubview(checkboxView) - checkboxStackView.addArrangedSubview(checkboxLabelStackView) - checkboxLabelStackView.addArrangedSubview(primaryLabel) - checkboxLabelStackView.addArrangedSubview(secondaryLabel) - - checkboxHeightConstraint = checkboxView.heightAnchor.constraint(equalToConstant: checkboxSize.height) - checkboxHeightConstraint?.isActive = true - - checkboxWidthConstraint = checkboxView.widthAnchor.constraint(equalToConstant: checkboxSize.width) - checkboxWidthConstraint?.isActive = true - - updateCheckbox(model) - - mainStackView.topAnchor.constraint(equalTo: topAnchor).isActive = true - mainStackView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - mainStackView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - mainStackView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true - - } - - func updateLabels(_ viewModel: ModelType) { - - //deal with labels - if model.shouldShowLabels { - //add the stackview to hold the 2 labels - //top label - if let labelModel = viewModel.labelModel { - primaryLabel.set(with: labelModel) - primaryLabel.isHidden = false - } else { - primaryLabel.isHidden = true - } - - //bottom label - if let childModel = viewModel.childModel { - secondaryLabel.set(with: childModel) - secondaryLabel.isHidden = false - } else { - secondaryLabel.isHidden = true - } - checkboxStackView.spacing = 12 - checkboxLabelStackView.spacing = 4 - checkboxLabelStackView.isHidden = false - - } else { - checkboxStackView.spacing = 0 - checkboxLabelStackView.spacing = 0 - checkboxLabelStackView.isHidden = true - } - - //either add/remove the error from the main stack - if let errorModel = model.errorModel, model.shouldShowError { - errorLabel.set(with: errorModel) - mainStackView.spacing = 8 - errorLabel.isHidden = false - } else { - mainStackView.spacing = 0 - errorLabel.isHidden = true - } - - } - - public override func reset() { - super.reset() - updateCheckbox(model) - setAccessibilityLabel() - onChange = nil - } - //-------------------------------------------------- // MARK: - Checkbox View //-------------------------------------------------- /// Manages the appearance of the checkbox. private var shapeLayer: CAShapeLayer? - private func updateCheckbox(_ viewModel: ModelType) { + + open override func getSelectorSize() -> CGSize { + return checkboxSize + } + + open override func updateSelector(_ viewModel: ModelType) { //get the colors let backgroundColor = checkboxBackgroundColorConfiguration.getColor(viewModel) let borderColor = checkboxBorderColorConfiguration.getColor(viewModel) @@ -311,13 +100,13 @@ open class CheckboxBase: Control, Changable self.shapeLayer = nil } - checkboxView.backgroundColor = backgroundColor - checkboxView.layer.borderColor = borderColor.cgColor - checkboxView.layer.cornerRadius = 2.0 - checkboxView.layer.borderWidth = 1.0 + selectorView.backgroundColor = backgroundColor + selectorView.layer.borderColor = borderColor.cgColor + selectorView.layer.cornerRadius = 2.0 + selectorView.layer.borderWidth = 1.0 if shapeLayer == nil { - let bounds = checkboxView.bounds + let bounds = selectorView.bounds let length = max(bounds.size.height, bounds.size.width) guard length > 0.0, shapeLayer == nil else { return } @@ -346,64 +135,12 @@ open class CheckboxBase: Control, Changable shapeLayer.lineJoin = .miter shapeLayer.lineWidth = 2 CATransaction.withDisabledAnimations { - shapeLayer.strokeEnd = model.on ? 1 : 0 + shapeLayer.strokeEnd = model.selected ? 1 : 0 } } + } - } - //-------------------------------------------------- - // MARK: - Actions - //-------------------------------------------------- - open override func sendAction(_ action: Selector, to target: Any?, for event: UIEvent?) { - super.sendAction(action, to: target, for: event) - toggleAndAction() - } - - open override func sendActions(for controlEvents: UIControl.Event) { - super.sendActions(for: controlEvents) - toggleAndAction() - } - - /// This will checkbox the state of the Checkbox and execute the actionBlock if provided. - @objc public func toggleAndAction() { - isOn.toggle() - onChange?() - } - - override open func accessibilityActivate() -> Bool { - // Hold state in case User wanted isAnimated to remain off. - guard isUserInteractionEnabled else { return false } - sendActions(for: .touchUpInside) - return true - } - - //-------------------------------------------------- - // MARK: - UIResponder - //-------------------------------------------------- - open override func touchesEnded(_ touches: Set, with event: UIEvent?) { - - sendActions(for: .touchUpInside) - } - - //-------------------------------------------------- - // MARK: - State - //-------------------------------------------------- - /// Follow the SwiftUI View paradigm - /// - Parameter viewModel: state - open override func onStateChange(viewModel: ModelType) { - let enabled = !viewModel.disabled - - updateLabels(viewModel) - updateCheckbox(viewModel) - setAccessibilityHint(enabled) - setAccessibilityValue(viewModel.on) - setAccessibilityLabel(viewModel.on) - isUserInteractionEnabled = !viewModel.disabled - setNeedsLayout() - layoutIfNeeded() - } - //-------------------------------------------------- // MARK: - Color Class Configurations //-------------------------------------------------- diff --git a/VDS/Components/Checkbox/CheckboxModel.swift b/VDS/Components/Checkbox/CheckboxModel.swift index 3d686404..9f0d96cc 100644 --- a/VDS/Components/Checkbox/CheckboxModel.swift +++ b/VDS/Components/Checkbox/CheckboxModel.swift @@ -8,78 +8,16 @@ import Foundation import UIKit -public protocol CheckboxModel: Modelable, FormFieldable, Errorable, DataTrackable, Accessable, BinaryColorable { - var id: String? { get set } - var on: Bool { get set } - var labelText: String? { get set } - var labelTextAttributes: [LabelAttributeModel]? { get set } - var childText: String? { get set } - var childTextAttributes: [LabelAttributeModel]? { get set } +public protocol CheckboxModel: SelectorModel, BinaryColorable { } extension CheckboxModel { - public var userTrueColor: Bool { return on } - - public var fontCategory: FontCategory { - get { return .body } - set { return } - } - - public var shouldShowError: Bool { - guard showError && !disabled && errorText?.isEmpty == false else { return false } - return true - } - - public var shouldShowLabels: Bool { - guard labelText?.isEmpty == false || childText?.isEmpty == false else { return false } - return true - } - - public var labelModel: DefaultLabelModel? { - guard let labelText = labelText else { return nil } - var model = DefaultLabelModel() - model.fontSize = .large - model.textPosition = .left - model.fontWeight = .bold - model.fontCategory = .body - model.text = labelText - model.surface = surface - model.disabled = disabled - model.attributes = labelTextAttributes - return model - } - - public var childModel: DefaultLabelModel? { - guard let childText = childText else { return nil } - var model = DefaultLabelModel() - model.fontSize = .large - model.textPosition = .left - model.fontWeight = .regular - model.fontCategory = .body - model.text = childText - model.surface = surface - model.disabled = disabled - model.attributes = childTextAttributes - return model - } - - public var errorModel: DefaultLabelModel? { - guard let errorText = errorText, showError else { return nil } - var model = DefaultLabelModel() - model.fontSize = .medium - model.textPosition = .left - model.fontWeight = .regular - model.fontCategory = .body - model.text = errorText - model.surface = surface - model.disabled = disabled - return model - } + public var userTrueColor: Bool { return selected } } public struct DefaultCheckboxModel: CheckboxModel { public var id: String? - public var on: Bool = false + public var selected: Bool = false public var labelText: String? public var labelTextAttributes: [LabelAttributeModel]? diff --git a/VDS/Components/RadioButton/RadioButton.swift b/VDS/Components/RadioButton/RadioButton.swift index dc2c67cb..70d82082 100644 --- a/VDS/Components/RadioButton/RadioButton.swift +++ b/VDS/Components/RadioButton/RadioButton.swift @@ -19,57 +19,7 @@ import Combine public class RadioButton: RadioButtonBase{} -open class RadioButtonBase: Control, Changable { - - //-------------------------------------------------- - // MARK: - Private Properties - //-------------------------------------------------- - private var mainStackView: UIStackView = { - let stackView = UIStackView() - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.alignment = .top - stackView.axis = .vertical - return stackView - }() - - private var radioButtonStackView: UIStackView = { - let stackView = UIStackView() - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.alignment = .top - stackView.axis = .horizontal - return stackView - }() - - private var radioButtonLabelStackView: UIStackView = { - let stackView = UIStackView() - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.axis = .vertical - return stackView - }() - - private var primaryLabel: Label = { - let label = Label() - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - private var secondaryLabel: Label = { - let label = Label() - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - private var errorLabel: Label = { - let label = Label() - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - private var radioButtonView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - return view - }() +open class RadioButtonBase: SelectorBase { //-------------------------------------------------- // MARK: - Configuration Properties @@ -113,77 +63,7 @@ open class RadioButtonBase: Control, Cha config.forTrue.disabled.darkColor = VDSColor.interactiveDisabledOndark return config }() - - //-------------------------------------------------- - // MARK: - Public Properties - //-------------------------------------------------- - public var onChange: Blocks.ActionBlock? - @Proxy(\.model.id) - open var id: String? - - @Proxy(\.model.on) - open var isOn: Bool - - @Proxy(\.model.labelText) - open var labelText: String? - - @Proxy(\.model.childText) - open var childText: String? - - @Proxy(\.model.showError) - open var showError: Bool - - @Proxy(\.model.errorText) - open var errorText: String? - - @Proxy(\.model.inputId) - open var inputId: String? - - @Proxy(\.model.value) - open var value: AnyHashable? - - @Proxy(\.model.dataAnalyticsTrack) - open var dataAnalyticsTrack: String? - - @Proxy(\.model.dataClickStream) - open var dataClickStream: String? - - @Proxy(\.model.dataTrack) - open var dataTrack: String? - - @Proxy(\.model.accessibilityHintEnabled) - open var accessibilityHintEnabled: String? - - @Proxy(\.model.accessibilityHintDisabled) - open var accessibilityHintDisabled: String? - - @Proxy(\.model.accessibilityValueEnabled) - open var accessibilityValueEnabled: String? - - @Proxy(\.model.accessibilityValueDisabled) - open var accessibilityValueDisabled: String? - - @Proxy(\.model.accessibilityLabelEnabled) - open var accessibilityLabelEnabled: String? - - @Proxy(\.model.accessibilityLabelDisabled) - open var accessibilityLabelDisabled: String? - - //-------------------------------------------------- - // MARK: - Computed Properties - //-------------------------------------------------- - open override var isEnabled: Bool { - get { !model.disabled } - set { - //create local vars for clear coding - let disabled = !newValue - if model.disabled != disabled { - model.disabled = disabled - } - } - } - //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- @@ -198,108 +78,18 @@ open class RadioButtonBase: Control, Cha required public init?(coder: NSCoder) { super.init(with: ModelType()) } - - //-------------------------------------------------- - // MARK: - Constraints - //-------------------------------------------------- - - private var knobLeadingConstraint: NSLayoutConstraint? - private var knobTrailingConstraint: NSLayoutConstraint? - private var knobHeightConstraint: NSLayoutConstraint? - private var knobWidthConstraint: NSLayoutConstraint? - private var radioButtonHeightConstraint: NSLayoutConstraint? - private var radioButtonWidthConstraint: NSLayoutConstraint? - - //functions - //-------------------------------------------------- - // MARK: - Lifecycle - //-------------------------------------------------- - - open override func setup() { - super.setup() - self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(RadioButton.toggleAndAction))) - isAccessibilityElement = true - accessibilityTraits = .button - addSubview(mainStackView) - - mainStackView.addArrangedSubview(radioButtonStackView) - mainStackView.addArrangedSubview(errorLabel) - radioButtonStackView.addArrangedSubview(radioButtonView) - radioButtonStackView.addArrangedSubview(radioButtonLabelStackView) - radioButtonLabelStackView.addArrangedSubview(primaryLabel) - radioButtonLabelStackView.addArrangedSubview(secondaryLabel) - - radioButtonHeightConstraint = radioButtonView.heightAnchor.constraint(equalToConstant: radioButtonSize.height) - radioButtonHeightConstraint?.isActive = true - - radioButtonWidthConstraint = radioButtonView.widthAnchor.constraint(equalToConstant: radioButtonSize.width) - radioButtonWidthConstraint?.isActive = true - - updateRadioButton(model) - - mainStackView.topAnchor.constraint(equalTo: topAnchor).isActive = true - mainStackView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - mainStackView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - mainStackView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true - - } - - func updateLabels(_ viewModel: ModelType) { - - //deal with labels - if model.shouldShowLabels { - //add the stackview to hold the 2 labels - //top label - if let labelModel = viewModel.labelModel { - primaryLabel.set(with: labelModel) - primaryLabel.isHidden = false - } else { - primaryLabel.isHidden = true - } - - //bottom label - if let childModel = viewModel.childModel { - secondaryLabel.set(with: childModel) - secondaryLabel.isHidden = false - } else { - secondaryLabel.isHidden = true - } - radioButtonStackView.spacing = 12 - radioButtonLabelStackView.spacing = 4 - radioButtonLabelStackView.isHidden = false - - } else { - radioButtonStackView.spacing = 0 - radioButtonLabelStackView.spacing = 0 - radioButtonLabelStackView.isHidden = true - } - - //either add/remove the error from the main stack - if let errorModel = model.errorModel, model.shouldShowError { - errorLabel.set(with: errorModel) - mainStackView.spacing = 8 - errorLabel.isHidden = false - } else { - mainStackView.spacing = 0 - errorLabel.isHidden = true - } - - } - - public override func reset() { - super.reset() - updateRadioButton(model) - setAccessibilityLabel() - onChange = nil - } //-------------------------------------------------- // MARK: - RadioButton View //-------------------------------------------------- /// Manages the appearance of the radioButton. private var shapeLayer: CAShapeLayer? - private func updateRadioButton(_ viewModel: ModelType) { + open override func getSelectorSize() -> CGSize { + return radioButtonSize + } + + open override func updateSelector(_ viewModel: ModelType) { //get the colors let backgroundColor = radioButtonBackgroundColorConfiguration.getColor(viewModel) let borderColor = radioButtonBorderColorConfiguration.getColor(viewModel) @@ -309,13 +99,13 @@ open class RadioButtonBase: Control, Cha shapeLayer.removeFromSuperlayer() self.shapeLayer = nil } - radioButtonView.backgroundColor = backgroundColor - radioButtonView.layer.borderColor = borderColor.cgColor - radioButtonView.layer.cornerRadius = radioButtonView.bounds.width * 0.5 - radioButtonView.layer.borderWidth = 1.0 + selectorView.backgroundColor = backgroundColor + selectorView.layer.borderColor = borderColor.cgColor + selectorView.layer.cornerRadius = selectorView.bounds.width * 0.5 + selectorView.layer.borderWidth = 1.0 if shapeLayer == nil { - let bounds = radioButtonView.bounds + let bounds = selectorView.bounds let selectedBounds = radioButtonSelectedSize let length = max(bounds.size.height, bounds.size.width) guard length > 0.0, shapeLayer == nil else { return } @@ -333,58 +123,6 @@ open class RadioButtonBase: Control, Cha } } - //-------------------------------------------------- - // MARK: - Actions - //-------------------------------------------------- - open override func sendAction(_ action: Selector, to target: Any?, for event: UIEvent?) { - super.sendAction(action, to: target, for: event) - toggleAndAction() - } - - open override func sendActions(for controlEvents: UIControl.Event) { - super.sendActions(for: controlEvents) - toggleAndAction() - } - - /// This will radioButton the state of the RadioButton and execute the actionBlock if provided. - @objc public func toggleAndAction() { - isOn.toggle() - onChange?() - } - - override open func accessibilityActivate() -> Bool { - // Hold state in case User wanted isAnimated to remain off. - guard isUserInteractionEnabled else { return false } - sendActions(for: .touchUpInside) - return true - } - - //-------------------------------------------------- - // MARK: - UIResponder - //-------------------------------------------------- - open override func touchesEnded(_ touches: Set, with event: UIEvent?) { - - sendActions(for: .touchUpInside) - } - - //-------------------------------------------------- - // MARK: - State - //-------------------------------------------------- - /// Follow the SwiftUI View paradigm - /// - Parameter viewModel: state - open override func onStateChange(viewModel: ModelType) { - let enabled = !viewModel.disabled - - updateLabels(viewModel) - updateRadioButton(viewModel) - setAccessibilityHint(enabled) - setAccessibilityValue(viewModel.on) - setAccessibilityLabel(viewModel.on) - isUserInteractionEnabled = !viewModel.disabled - setNeedsLayout() - layoutIfNeeded() - } - //-------------------------------------------------- // MARK: - Color Class Configurations //-------------------------------------------------- diff --git a/VDS/Components/RadioButton/RadioButtonModel.swift b/VDS/Components/RadioButton/RadioButtonModel.swift index e79f048d..f602437c 100644 --- a/VDS/Components/RadioButton/RadioButtonModel.swift +++ b/VDS/Components/RadioButton/RadioButtonModel.swift @@ -8,78 +8,17 @@ import Foundation import UIKit -public protocol RadioButtonModel: Modelable, FormFieldable, Errorable, DataTrackable, Accessable, BinaryColorable { - var id: String? { get set } - var on: Bool { get set } - var labelText: String? { get set } - var labelTextAttributes: [LabelAttributeModel]? { get set } - var childText: String? { get set } - var childTextAttributes: [LabelAttributeModel]? { get set } +public protocol RadioButtonModel: SelectorModel, BinaryColorable { + } extension RadioButtonModel { - public var userTrueColor: Bool { return on } - - public var fontCategory: FontCategory { - get { return .body } - set { return } - } - - public var shouldShowError: Bool { - guard showError && !disabled && errorText?.isEmpty == false else { return false } - return true - } - - public var shouldShowLabels: Bool { - guard labelText?.isEmpty == false || childText?.isEmpty == false else { return false } - return true - } - - public var labelModel: DefaultLabelModel? { - guard let labelText = labelText else { return nil } - var model = DefaultLabelModel() - model.fontSize = .large - model.textPosition = .left - model.fontWeight = .bold - model.fontCategory = .body - model.text = labelText - model.surface = surface - model.disabled = disabled - model.attributes = labelTextAttributes - return model - } - - public var childModel: DefaultLabelModel? { - guard let childText = childText else { return nil } - var model = DefaultLabelModel() - model.fontSize = .large - model.textPosition = .left - model.fontWeight = .regular - model.fontCategory = .body - model.text = childText - model.surface = surface - model.disabled = disabled - model.attributes = childTextAttributes - return model - } - - public var errorModel: DefaultLabelModel? { - guard let errorText = errorText, showError else { return nil } - var model = DefaultLabelModel() - model.fontSize = .medium - model.textPosition = .left - model.fontWeight = .regular - model.fontCategory = .body - model.text = errorText - model.surface = surface - model.disabled = disabled - return model - } + public var userTrueColor: Bool { return selected } } public struct DefaultRadioButtonModel: RadioButtonModel { public var id: String? - public var on: Bool = false + public var selected: Bool = false public var labelText: String? public var labelTextAttributes: [LabelAttributeModel]? diff --git a/VDS/Components/SelectorBase/SelectorBase.swift b/VDS/Components/SelectorBase/SelectorBase.swift new file mode 100644 index 00000000..34678aed --- /dev/null +++ b/VDS/Components/SelectorBase/SelectorBase.swift @@ -0,0 +1,316 @@ +// +// SelectorControl.swift +// VDS +// +// Created by Matt Bruce on 8/8/22. +// + +import Foundation +import UIKit +import VDSColorTokens +import VDSFormControlsTokens +import Combine + +open class SelectorBase: Control, Changable { + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + private var mainStackView: UIStackView = { + let stackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.alignment = .top + stackView.axis = .vertical + return stackView + }() + + private var selectorStackView: UIStackView = { + let stackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.alignment = .top + stackView.axis = .horizontal + return stackView + }() + + private var selectorLabelStackView: UIStackView = { + let stackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .vertical + return stackView + }() + + private var primaryLabel: Label = { + let label = Label() + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private var secondaryLabel: Label = { + let label = Label() + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private var errorLabel: Label = { + let label = Label() + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + public var selectorView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + public var onChange: Blocks.ActionBlock? + + @Proxy(\.model.id) + open var id: String? + + //can't bind to @Proxy + open override var isSelected: Bool { + get { model.selected } + set { + if model.selected != newValue { + model.selected = newValue + } + } + } + + @Proxy(\.model.labelText) + open var labelText: String? + + @Proxy(\.model.childText) + open var childText: String? + + @Proxy(\.model.showError) + open var showError: Bool + + @Proxy(\.model.errorText) + open var errorText: String? + + @Proxy(\.model.inputId) + open var inputId: String? + + @Proxy(\.model.value) + open var value: AnyHashable? + + @Proxy(\.model.dataAnalyticsTrack) + open var dataAnalyticsTrack: String? + + @Proxy(\.model.dataClickStream) + open var dataClickStream: String? + + @Proxy(\.model.dataTrack) + open var dataTrack: String? + + @Proxy(\.model.accessibilityHintEnabled) + open var accessibilityHintEnabled: String? + + @Proxy(\.model.accessibilityHintDisabled) + open var accessibilityHintDisabled: String? + + @Proxy(\.model.accessibilityValueEnabled) + open var accessibilityValueEnabled: String? + + @Proxy(\.model.accessibilityValueDisabled) + open var accessibilityValueDisabled: String? + + @Proxy(\.model.accessibilityLabelEnabled) + open var accessibilityLabelEnabled: String? + + @Proxy(\.model.accessibilityLabelDisabled) + open var accessibilityLabelDisabled: String? + + //-------------------------------------------------- + // MARK: - Computed Properties + //-------------------------------------------------- + open override var isEnabled: Bool { + get { !model.disabled } + set { + //create local vars for clear coding + let disabled = !newValue + if model.disabled != disabled { + model.disabled = disabled + } + } + } + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + public convenience init() { + self.init(with: ModelType()) + } + + required public init(with model: ModelType) { + super.init(with: model) + } + + required public init?(coder: NSCoder) { + super.init(with: ModelType()) + } + + //-------------------------------------------------- + // MARK: - Constraints + //-------------------------------------------------- + + private var knobLeadingConstraint: NSLayoutConstraint? + private var knobTrailingConstraint: NSLayoutConstraint? + private var knobHeightConstraint: NSLayoutConstraint? + private var knobWidthConstraint: NSLayoutConstraint? + private var selectorHeightConstraint: NSLayoutConstraint? + private var selectorWidthConstraint: NSLayoutConstraint? + + //functions + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + + open override func setup() { + super.setup() + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(Checkbox.toggleAndAction))) + + isAccessibilityElement = true + accessibilityTraits = .button + addSubview(mainStackView) + + mainStackView.addArrangedSubview(selectorStackView) + mainStackView.addArrangedSubview(errorLabel) + selectorStackView.addArrangedSubview(selectorView) + selectorStackView.addArrangedSubview(selectorLabelStackView) + selectorLabelStackView.addArrangedSubview(primaryLabel) + selectorLabelStackView.addArrangedSubview(secondaryLabel) + + let selectorSize = getSelectorSize() + selectorHeightConstraint = selectorView.heightAnchor.constraint(equalToConstant: selectorSize.height) + selectorHeightConstraint?.isActive = true + + selectorWidthConstraint = selectorView.widthAnchor.constraint(equalToConstant: selectorSize.width) + selectorWidthConstraint?.isActive = true + + updateSelector(model) + + mainStackView.topAnchor.constraint(equalTo: topAnchor).isActive = true + mainStackView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true + mainStackView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true + mainStackView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true + + } + + func updateLabels(_ viewModel: ModelType) { + + //deal with labels + if model.shouldShowLabels { + //add the stackview to hold the 2 labels + //top label + if let labelModel = viewModel.labelModel { + primaryLabel.set(with: labelModel) + primaryLabel.isHidden = false + } else { + primaryLabel.isHidden = true + } + + //bottom label + if let childModel = viewModel.childModel { + secondaryLabel.set(with: childModel) + secondaryLabel.isHidden = false + } else { + secondaryLabel.isHidden = true + } + selectorStackView.spacing = 12 + selectorLabelStackView.spacing = 4 + selectorLabelStackView.isHidden = false + + } else { + selectorStackView.spacing = 0 + selectorLabelStackView.spacing = 0 + selectorLabelStackView.isHidden = true + } + + //either add/remove the error from the main stack + if let errorModel = model.errorModel, model.shouldShowError { + errorLabel.set(with: errorModel) + mainStackView.spacing = 8 + errorLabel.isHidden = false + } else { + mainStackView.spacing = 0 + errorLabel.isHidden = true + } + + } + + public override func reset() { + super.reset() + updateSelector(model) + setAccessibilityLabel() + onChange = nil + } + + //-------------------------------------------------- + // MARK: - Selector View + //-------------------------------------------------- + open func getSelectorSize() -> CGSize { + fatalError("must override") + } + + open func updateSelector(_ viewModel: ModelType) { + } + + //-------------------------------------------------- + // MARK: - Actions + //-------------------------------------------------- + open override func sendAction(_ action: Selector, to target: Any?, for event: UIEvent?) { + super.sendAction(action, to: target, for: event) + toggleAndAction() + } + + open override func sendActions(for controlEvents: UIControl.Event) { + super.sendActions(for: controlEvents) + toggleAndAction() + } + + /// This will checkbox the state of the Checkbox and execute the actionBlock if provided. + @objc public func toggleAndAction() { + isSelected.toggle() + onChange?() + } + + override open func accessibilityActivate() -> Bool { + // Hold state in case User wanted isAnimated to remain off. + guard isUserInteractionEnabled else { return false } + sendActions(for: .touchUpInside) + return true + } + + //-------------------------------------------------- + // MARK: - UIResponder + //-------------------------------------------------- + open override func touchesEnded(_ touches: Set, with event: UIEvent?) { + + sendActions(for: .touchUpInside) + } + + //-------------------------------------------------- + // MARK: - State + //-------------------------------------------------- + /// Follow the SwiftUI View paradigm + /// - Parameter viewModel: state + open override func onStateChange(viewModel: ModelType) { + let enabled = !viewModel.disabled + + updateLabels(viewModel) + updateSelector(viewModel) + setAccessibilityHint(enabled) + setAccessibilityValue(viewModel.selected) + setAccessibilityLabel(viewModel.selected) + isUserInteractionEnabled = !viewModel.disabled + setNeedsLayout() + layoutIfNeeded() + } +} + diff --git a/VDS/Components/SelectorBase/SelectorModel.swift b/VDS/Components/SelectorBase/SelectorModel.swift new file mode 100644 index 00000000..cc633b57 --- /dev/null +++ b/VDS/Components/SelectorBase/SelectorModel.swift @@ -0,0 +1,79 @@ +// +// SelectorModel.swift +// VDS +// +// Created by Matt Bruce on 8/8/22. +// + +import Foundation + +public protocol Selectable { + var selected: Bool { get set } +} + +public protocol SelectorModel: Modelable, FormFieldable, Errorable, DataTrackable, Accessable, Selectable { + var id: String? { get set } + var labelText: String? { get set } + var labelTextAttributes: [LabelAttributeModel]? { get set } + var childText: String? { get set } + var childTextAttributes: [LabelAttributeModel]? { get set } +} + +extension SelectorModel { + + public var fontCategory: FontCategory { + get { return .body } + set { return } + } + + public var shouldShowError: Bool { + guard showError && !disabled && errorText?.isEmpty == false else { return false } + return true + } + + public var shouldShowLabels: Bool { + guard labelText?.isEmpty == false || childText?.isEmpty == false else { return false } + return true + } + + public var labelModel: DefaultLabelModel? { + guard let labelText = labelText else { return nil } + var model = DefaultLabelModel() + model.fontSize = .large + model.textPosition = .left + model.fontWeight = .bold + model.fontCategory = .body + model.text = labelText + model.surface = surface + model.disabled = disabled + model.attributes = labelTextAttributes + return model + } + + public var childModel: DefaultLabelModel? { + guard let childText = childText else { return nil } + var model = DefaultLabelModel() + model.fontSize = .large + model.textPosition = .left + model.fontWeight = .regular + model.fontCategory = .body + model.text = childText + model.surface = surface + model.disabled = disabled + model.attributes = childTextAttributes + return model + } + + public var errorModel: DefaultLabelModel? { + guard let errorText = errorText, showError else { return nil } + var model = DefaultLabelModel() + model.fontSize = .medium + model.textPosition = .left + model.fontWeight = .regular + model.fontCategory = .body + model.text = errorText + model.surface = surface + model.disabled = disabled + return model + } +}