refactored radiobutton for new subclasses

Signed-off-by: Matt Bruce <matt.bruce@verizon.com>
This commit is contained in:
Matt Bruce 2023-06-05 10:43:02 -05:00
parent 42e6f9025c
commit 17fe50223c
3 changed files with 82 additions and 332 deletions

View File

@ -2,7 +2,7 @@
// RadioButton.swift // RadioButton.swift
// VDS // VDS
// //
// Created by Matt Bruce on 7/22/22. // Created by Matt Bruce on 6/5/23.
// //
import Foundation import Foundation
@ -12,352 +12,58 @@ import VDSColorTokens
import VDSFormControlsTokens import VDSFormControlsTokens
@objc(VDSRadioButton) @objc(VDSRadioButton)
open class RadioButton: Control, Errorable, Changeable { open class RadioButton: SelectorBase {
//-------------------------------------------------- open var selectedSize = CGSize(width: 10, height: 10) { didSet { setNeedsUpdate() }}
// 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: - Private Properties
//--------------------------------------------------
private var shouldShowError: Bool {
guard showError && !disabled && errorText?.isEmpty == false else { return false }
return true
}
private var shouldShowLabels: Bool {
guard labelText?.isEmpty == false || childText?.isEmpty == false || labelAttributedText?.string.isEmpty == false || childAttributedText?.string.isEmpty == false else { return false }
return true
}
private var mainStackView = UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.alignment = .top
$0.axis = .vertical
}
private var selectorStackView = UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.alignment = .top
$0.axis = .horizontal
}
private var selectorLabelStackView = UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.axis = .vertical
}
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
public var onChangeSubscriber: AnyCancellable? {
willSet {
if let onChangeSubscriber {
onChangeSubscriber.cancel()
}
}
}
open var label = Label().with {
$0.setContentCompressionResistancePriority(.required, for: .vertical)
$0.textPosition = .left
$0.textStyle = .boldBodyLarge
}
open var childLabel = Label().with {
$0.setContentCompressionResistancePriority(.required, for: .vertical)
$0.textPosition = .left
$0.textStyle = .bodyLarge
}
open var errorLabel = Label().with {
$0.setContentCompressionResistancePriority(.required, for: .vertical)
$0.textPosition = .left
$0.textStyle = .bodyMedium
}
public var selectorView = UIView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
}
open var labelText: String? { didSet { setNeedsUpdate() }}
open var labelTextAttributes: [any LabelAttributeModel]? { didSet { setNeedsUpdate() }}
open var labelAttributedText: NSAttributedString? {
didSet {
label.useAttributedText = !(labelAttributedText?.string.isEmpty ?? true)
label.attributedText = labelAttributedText
setNeedsUpdate()
}
}
open var childText: String? { didSet { setNeedsUpdate() }}
open var childTextAttributes: [any LabelAttributeModel]? { didSet { setNeedsUpdate() }}
open var childAttributedText: NSAttributedString? {
didSet {
childLabel.useAttributedText = !(childAttributedText?.string.isEmpty ?? true)
childLabel.attributedText = childAttributedText
setNeedsUpdate()
}
}
open var showError: Bool = false { didSet { setNeedsUpdate() }}
open override var state: UIControl.State {
get {
var state = super.state
if showError {
state.insert(.error)
}
return state
}
}
open var errorText: String? { didSet { setNeedsUpdate() }}
open var inputId: String? { didSet { setNeedsUpdate() }}
open var value: AnyHashable? { didSet { setNeedsUpdate() }}
//--------------------------------------------------
// MARK: - Constraints
//--------------------------------------------------
private var selectorHeightConstraint: NSLayoutConstraint?
private var selectorWidthConstraint: NSLayoutConstraint?
//functions
//--------------------------------------------------
// MARK: - Lifecycle
//--------------------------------------------------
open override func initialSetup() {
super.initialSetup()
onClick = { control in
control.toggle()
}
}
open override func setup() { open override func setup() {
super.setup() super.setup()
isAccessibilityElement = true backgroundColorConfig.setSurfaceColors(VDSColor.feedbackErrorBackgroundOnlight, VDSColor.feedbackErrorBackgroundOndark, forState: .error)
accessibilityTraits = .button
addSubview(mainStackView) borderColorConfig.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .selected)
mainStackView.isUserInteractionEnabled = false borderColorConfig.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .highlighted)
borderColorConfig.setSurfaceColors(VDSFormControlsColor.borderOnlight, VDSFormControlsColor.borderOndark, forState: .normal)
borderColorConfig.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: [.disabled])
borderColorConfig.setSurfaceColors(VDSColor.feedbackErrorOnlight, VDSColor.feedbackErrorOndark, forState: .error)
selectorColorConfig.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .selected)
selectorColorConfig.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: [.selected, .disabled])
}
open override func layoutSubviews() {
super.layoutSubviews()
//get the colors
let backgroundColor = backgroundColorConfig.getColor(self)
let borderColor = borderColorConfig.getColor(self)
let selectorColor = selectorColorConfig.getColor(self)
mainStackView.addArrangedSubview(selectorStackView)
mainStackView.addArrangedSubview(errorLabel)
selectorStackView.addArrangedSubview(selectorView)
selectorStackView.addArrangedSubview(selectorLabelStackView)
selectorLabelStackView.addArrangedSubview(label)
selectorLabelStackView.addArrangedSubview(childLabel)
let selectorSize = getSelectorSize()
selectorHeightConstraint = selectorView.heightAnchor.constraint(equalToConstant: selectorSize.height)
selectorHeightConstraint?.isActive = true
selectorWidthConstraint = selectorView.widthAnchor.constraint(equalToConstant: selectorSize.width)
selectorWidthConstraint?.isActive = true
updateSelector()
mainStackView.pinToSuperView()
}
func updateLabels() {
//deal with labels
if shouldShowLabels {
//add the stackview to hold the 2 labels
//top label
if let labelText {
label.text = labelText
label.surface = surface
label.disabled = disabled
label.attributes = labelTextAttributes
label.isHidden = false
} else if labelAttributedText != nil {
label.isHidden = false
} else {
label.isHidden = true
}
//bottom label
if let childText {
childLabel.text = childText
childLabel.surface = surface
childLabel.disabled = disabled
childLabel.attributes = childTextAttributes
childLabel.isHidden = false
} else if childAttributedText != nil {
childLabel.isHidden = false
} else {
childLabel.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 errorText, shouldShowError {
errorLabel.text = errorText
errorLabel.surface = surface
errorLabel.disabled = disabled
mainStackView.spacing = 8
errorLabel.isHidden = false
} else {
mainStackView.spacing = 0
errorLabel.isHidden = true
}
}
open override func reset() {
super.reset()
shouldUpdateView = false
label.reset()
childLabel.reset()
errorLabel.reset()
label.textStyle = .boldBodyLarge
childLabel.textStyle = .bodyLarge
errorLabel.textStyle = .bodyMedium
labelText = nil
labelTextAttributes = nil
labelAttributedText = nil
childText = nil
childTextAttributes = nil
childAttributedText = nil
showError = false
errorText = nil
inputId = nil
value = nil
isSelected = false
shouldUpdateView = true
setNeedsUpdate()
}
/// This will checkbox the state of the Selector and execute the actionBlock if provided.
open func toggle() {
guard !isSelected else { return }
//removed error
if showError && isSelected == false {
showError.toggle()
}
isSelected.toggle()
sendActions(for: .valueChanged)
}
//--------------------------------------------------
// MARK: - State
//--------------------------------------------------
open override func updateView() {
updateLabels()
updateSelector()
updateAccessibilityLabel()
}
open override func updateAccessibilityLabel() {
setAccessibilityLabel(for: [label, childLabel])
}
//--------------------------------------------------
// MARK: - Configuration Properties
//--------------------------------------------------
public let radioButtonSize = CGSize(width: 20, height: 20)
public let radioButtonSelectedSize = CGSize(width: 10, height: 10)
private var backgroundColorConfiguration = ControlColorConfiguration().with {
$0.setSurfaceColors(VDSColor.feedbackErrorBackgroundOnlight, VDSColor.feedbackErrorBackgroundOndark, forState: .error)
}
private var borderColorConfiguration = ControlColorConfiguration().with {
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .selected)
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .highlighted)
$0.setSurfaceColors(VDSFormControlsColor.borderOnlight, VDSFormControlsColor.borderOndark, forState: .normal)
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: [.disabled])
$0.setSurfaceColors(VDSColor.feedbackErrorOnlight, VDSColor.feedbackErrorOndark, forState: .error)
}
private var checkColorConfiguration = ControlColorConfiguration().with {
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .selected)
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: [.selected, .disabled])
}
//--------------------------------------------------
// MARK: - RadioButton View
//--------------------------------------------------
/// Manages the appearance of the radioButton.
private var shapeLayer: CAShapeLayer?
open func getSelectorSize() -> CGSize {
radioButtonSize
}
open func updateSelector() {
if let shapeLayer = shapeLayer, let sublayers = layer.sublayers, sublayers.contains(shapeLayer) { if let shapeLayer = shapeLayer, let sublayers = layer.sublayers, sublayers.contains(shapeLayer) {
shapeLayer.removeFromSuperlayer() shapeLayer.removeFromSuperlayer()
self.shapeLayer = nil self.shapeLayer = nil
} }
let bounds = radioButtonSize let bounds = bounds
let length = max(bounds.height, bounds.width)
guard length > 0.0, shapeLayer == nil else { return }
//get the colors self.backgroundColor = backgroundColor
let backgroundColor = backgroundColorConfiguration.getColor(self) layer.borderColor = borderColor.cgColor
let borderColor = borderColorConfiguration.getColor(self) layer.cornerRadius = bounds.width * 0.5
let radioSelectedColor = checkColorConfiguration.getColor(self) layer.borderWidth = VDSFormControls.widthBorder
selectorView.backgroundColor = backgroundColor
selectorView.layer.borderColor = borderColor.cgColor
selectorView.layer.cornerRadius = bounds.width * 0.5
selectorView.layer.borderWidth = VDSFormControls.widthBorder
if shapeLayer == nil { if shapeLayer == nil {
let selectedBounds = radioButtonSelectedSize let selectedBounds = selectedSize
let bezierPath = UIBezierPath(ovalIn: CGRect(x: (bounds.width - selectedBounds.width) / 2, let bezierPath = UIBezierPath(ovalIn: CGRect(x: (bounds.width - selectedBounds.width) / 2,
y: (bounds.height - selectedBounds.height) / 2, y: (bounds.height - selectedBounds.height) / 2,
width: radioButtonSelectedSize.width, width: selectedSize.width,
height: radioButtonSelectedSize.height)) height: selectedSize.height))
let shapeLayer = CAShapeLayer() let shapeLayer = CAShapeLayer()
self.shapeLayer = shapeLayer self.shapeLayer = shapeLayer
shapeLayer.frame = selectorView.bounds shapeLayer.frame = bounds
layer.addSublayer(shapeLayer) layer.addSublayer(shapeLayer)
shapeLayer.fillColor = radioSelectedColor.cgColor shapeLayer.fillColor = selectorColor.cgColor
shapeLayer.path = bezierPath.cgPath shapeLayer.path = bezierPath.cgPath
} }
} }
} }

View File

@ -9,12 +9,12 @@ import Foundation
import UIKit import UIKit
@objc(VDSRadioButtonGroup) @objc(VDSRadioButtonGroup)
open class RadioButtonGroup: SelectorGroupSelectedHandlerBase<RadioButton> { open class RadioButtonGroup: SelectorGroupSelectedHandlerBase<RadioButtonItem> {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Public Properties // MARK: - Public Properties
//-------------------------------------------------- //--------------------------------------------------
public override var selectorViews: [RadioButton] { public override var selectorViews: [RadioButtonItem] {
willSet { willSet {
mainStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } mainStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
} }
@ -33,7 +33,7 @@ open class RadioButtonGroup: SelectorGroupSelectedHandlerBase<RadioButton> {
didSet { didSet {
if let selectorModels { if let selectorModels {
selectorViews = selectorModels.map { model in selectorViews = selectorModels.map { model in
return RadioButton().with { return RadioButtonItem().with {
$0.disabled = model.disabled $0.disabled = model.disabled
$0.surface = model.surface $0.surface = model.surface
$0.inputId = model.inputId $0.inputId = model.inputId
@ -93,7 +93,7 @@ open class RadioButtonGroup: SelectorGroupSelectedHandlerBase<RadioButton> {
mainStackView.pinToSuperView() mainStackView.pinToSuperView()
} }
public override func didSelect(_ selectedControl: RadioButton) { public override func didSelect(_ selectedControl: RadioButtonItem) {
if let selectedHandler { if let selectedHandler {
updateToggle(selectedHandler) updateToggle(selectedHandler)
} }
@ -104,7 +104,7 @@ open class RadioButtonGroup: SelectorGroupSelectedHandlerBase<RadioButton> {
valueChanged() valueChanged()
} }
private func updateToggle(_ radioButton: RadioButton) { private func updateToggle(_ radioButton: RadioButtonItem) {
if radioButton.showError && radioButton.isSelected == false { if radioButton.showError && radioButton.isSelected == false {
radioButton.showError.toggle() radioButton.showError.toggle()
} }

View File

@ -0,0 +1,44 @@
//
// RadioButton.swift
// VDS
//
// Created by Matt Bruce on 7/22/22.
//
import Foundation
import UIKit
@objc(VDSRadioButtonItem)
open class RadioButtonItem: SelectorItemBase<RadioButton> {
//--------------------------------------------------
// 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: - Overrides
//--------------------------------------------------
/// This will checkbox the state of the Selector and execute the actionBlock if provided.
open override func toggle() {
guard !isSelected else { return }
//removed error
if showError && isSelected == false {
showError.toggle()
}
isSelected.toggle()
sendActions(for: .valueChanged)
}
}