vds_ios/VDS/BaseClasses/Selector/SelectorItemBase.swift
Matt Bruce c41599578a added ParentViewProtocol to views and updated children
Signed-off-by: Matt Bruce <matt.bruce@verizon.com>
2024-08-22 16:03:47 -05:00

406 lines
13 KiB
Swift

//
// SelectorItemBase.swift
// VDS
//
// Created by Matt Bruce on 6/5/23.
//
import Foundation
import UIKit
import Combine
import VDSCoreTokens
/// Base Class used to build out a SelectorControlable control.
open class SelectorItemBase<Selector: SelectorBase>: Control, Errorable, Changeable, Groupable, ParentViewProtocol {
//--------------------------------------------------
// 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 && isEnabled && 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
//--------------------------------------------------
open var children: [any ViewProtocol] { [label, childLabel, errorLabel, selectorView] }
open var onChangeSubscriber: AnyCancellable?
/// Label used to render labelText.
open var label = Label().with {
$0.setContentCompressionResistancePriority(.required, for: .vertical)
$0.setContentCompressionResistancePriority(.required, for: .horizontal)
$0.textStyle = .boldBodyLarge
}
/// Label used to render childText.
open var childLabel = Label().with {
$0.setContentCompressionResistancePriority(.required, for: .vertical)
$0.setContentCompressionResistancePriority(.required, for: .horizontal)
$0.textStyle = .bodyLarge
}
/// Label used to render errorText.
open var errorLabel = Label().with {
$0.setContentCompressionResistancePriority(.required, for: .vertical)
$0.setContentCompressionResistancePriority(.required, for: .horizontal)
$0.textStyle = .bodyMedium
}
/// Generic Object used to allow the user to 'Select'.
open var selectorView = Selector()
open override var isSelected: Bool { didSet { setNeedsUpdate() } }
/// Text shown in the label.
open var labelText: String? { didSet { setNeedsUpdate() } }
/// Array of LabelAttributeModel objects used in rendering the labelText.
open var labelTextAttributes: [any LabelAttributeModel]? { didSet { setNeedsUpdate() } }
/// Instead of use labelText and labelTextAttirbutes, this is a fully baked NSAttributedString with both text and attributes.
open var labelAttributedText: NSAttributedString? {
didSet {
label.attributedText = labelAttributedText
setNeedsUpdate()
}
}
/// Text shown in the childLabel.
open var childText: String? { didSet { setNeedsUpdate() } }
/// Array of LabelAttributeModel objects used in rendering the childText.
open var childTextAttributes: [any LabelAttributeModel]? { didSet { setNeedsUpdate() } }
/// Instead of use childText and childTextAttirbutes, this is a fully baked NSAttributedString with both text and attributes.
open var childAttributedText: NSAttributedString? {
didSet {
childLabel.attributedText = childAttributedText
setNeedsUpdate()
}
}
/// 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 {
state.insert(.error)
}
return state
}
}
var _showError: Bool = false
open var showError: Bool {
get { _showError }
set {
if !isSelected && _showError != newValue {
_showError = newValue
setNeedsUpdate()
}
}
}
open var errorText: String? { didSet { setNeedsUpdate() } }
open var inputId: String? { didSet { setNeedsUpdate() } }
open var value: AnyHashable? { hiddenValue }
open var hiddenValue: AnyHashable? { didSet { setNeedsUpdate() } }
open override var accessibilityAction: ((Control) -> Void)? {
didSet {
selectorView.accessibilityAction = { [weak self] selectorItemBase in
guard let self else { return }
accessibilityAction?(self)
}
}
}
//--------------------------------------------------
// 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()
selectorView.isAccessibilityElement = true
isAccessibilityElement = false
addSubview(mainStackView)
mainStackView.isUserInteractionEnabled = false
mainStackView.addArrangedSubview(selectorStackView)
mainStackView.addArrangedSubview(errorLabel)
selectorStackView.addArrangedSubview(selectorView)
selectorStackView.addArrangedSubview(selectorLabelStackView)
selectorLabelStackView.addArrangedSubview(label)
selectorLabelStackView.addArrangedSubview(childLabel)
mainStackView
.pinTop()
.pinLeading()
.pinTrailing()
.pinBottom(0, .defaultHigh)
}
open override func setDefaults() {
super.setDefaults()
onClick = { [weak self] control in
guard let self, isEnabled else { return }
toggle()
}
selectorView.accessibilityAction = { [weak self] _ in
guard let self, isEnabled else { return }
toggle()
}
selectorView.bridge_accessibilityLabelBlock = { [weak self ] in
guard let self else { return "" }
var accessibilityLabels = [String]()
if isSelected {
accessibilityLabels.append("selected")
}
accessibilityLabels.append("\(Selector.self)")
if let text = labelText, !text.isEmpty {
accessibilityLabels.append(text)
}
if let text = childText, !text.isEmpty {
accessibilityLabels.append(text)
}
if !isEnabled {
accessibilityLabels.append("dimmed")
}
if let errorText, showError, !errorText.isEmpty {
accessibilityLabels.append("error, \(errorText)")
}
return accessibilityLabels.joined(separator: ", ")
}
selectorView.bridge_accessibilityHintBlock = { [weak self] in
guard let self else { return "" }
return !isEnabled ? "" : "Double tap to activate."
}
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
isSelected = false
onChange = nil
}
/// Used to make changes to the View based off a change events or from local properties.
open override func updateView() {
super.updateView()
updateLabels()
selectorView.showError = showError
selectorView.isSelected = isSelected
selectorView.isHighlighted = isHighlighted
selectorView.isEnabled = isEnabled
selectorView.surface = surface
}
open override var accessibilityElements: [Any]? {
get {
var elements = [Any]()
elements.append(selectorView)
if let text = labelText, !text.isEmpty {
elements.append(label)
}
if let text = childText, !text.isEmpty {
elements.append(childLabel)
}
if let errorText, showError, !errorText.isEmpty {
elements.append(errorLabel)
}
return elements
}
set {
super.accessibilityElements = newValue
}
}
/// Overriden to take the hit if there is an onClickSubscriber and the view is not a UIControl
open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard isEnabled else { return super.hitTest(point, with: event) }
let labelPoint = convert(point, to: label)
let childLabelPoint = convert(point, to: childLabel)
if label.isAction(for: labelPoint) {
return label
} else if childLabel.isAction(for: childLabelPoint) {
return childLabel
} else {
guard !UIAccessibility.isVoiceOverRunning else { return nil }
return super.hitTest(point, with: event)
}
}
/// Resets to default settings.
open override func reset() {
label.reset()
childLabel.reset()
errorLabel.reset()
super.reset()
}
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------
/// Update all labels with Text as well as adding and removing the labels.
func updateLabels() {
//deal with labels
if shouldShowLabels {
//add the stackview to hold the 2 labels
//top label
if let labelText {
label.surface = surface
label.isEnabled = isEnabled
label.attributes = labelTextAttributes
label.text = labelText
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.isEnabled = isEnabled
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.isEnabled = isEnabled
mainStackView.spacing = 8
errorLabel.isHidden = false
} else {
mainStackView.spacing = 0
errorLabel.isHidden = true
}
}
/// This will change to state of the Selector.
open func toggle() {}
open override func accessibilityActivate() -> Bool {
guard isEnabled, isUserInteractionEnabled else { return false }
var value = true
// if #available(iOS 17, *) {
// if let block = accessibilityAction {
// block(self)
//
// } else if let block = accessibilityActivateBlock {
// value = block()
//
// } else if let block = bridge_accessibilityActivateBlock {
// value = block()
//
// } else {
// toggle()
// }
// } else {
if let block = accessibilityAction {
block(self)
} else if let block = bridge_accessibilityActivateBlock {
value = block()
} else {
toggle()
}
// }
return value
}
}