vds_ios/VDS/Components/InputStepper/InputStepper.swift

311 lines
12 KiB
Swift

//
// InputStepper.swift
// VDS
//
// Created by Kanamarlapudi, Vasavi on 24/06/24.
//
import Foundation
import UIKit
import VDSCoreTokens
import Combine
/// A stepper is a two-segment control that people use to increase or decrease an incremental value.
@objc(VDSInputStepper)
open class InputStepper: EntryFieldBase {
//--------------------------------------------------
// 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: - Enums
//--------------------------------------------------
/// Enum used to describe the size of Input Stepper.
public enum Size: String, CaseIterable {
case large, small
}
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
/// Accepts a string or number value to control the width of input stepper.
/// auto(default) - The control's width is determined by the combined width of the value, trailing text and padding
/// Value - The control's width can be set to a fixed pixel or percentage value.
open var controlWidth: String? = "auto" { didSet { setNeedsUpdate() } }
/// Default value of the input stepper, defaults to '0'.
open var defaultValue:Int = 0 { didSet { setNeedsUpdate() } }
/// Allows an id to be passed to input stepper.
open var id: Int? { didSet { setNeedsUpdate() } }
/// Maximum value of the input stepper, defaults to '99'.
open var maxValue: Int? {
get { return _maxValue }
set {
if let newValue, newValue <= 99 && newValue > 0 {
_maxValue = newValue
} else {
_maxValue = 99
}
setNeedsUpdate()
}
}
/// Minimum value of the input stepper, defaults to '0'.
open var minValue: Int? {
get { return _minValue }
set {
if let newValue, newValue >= 0 && newValue < 99 {
_minValue = newValue
} else {
_minValue = 0
}
setNeedsUpdate()
}
}
/// The size of the input stepper. Defaults to 'large'.
open var size: Size = .large {
didSet {
updateStepperContainerViewSize()
setNeedsUpdate()
}
}
/// Accepts any text or character to appear next to input stepper value.
open var trailingText: String? { didSet { setNeedsUpdate() } }
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
internal var _maxValue: Int = 99
internal var _minValue: Int = 0
/// This is the view that will be wrapped with the border for userInteraction.
/// The only subview of this view is the stepperStackView.
internal var stepperContainerView = View().with {
$0.isAccessibilityElement = true
}
internal var stepperStackView = UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.axis = .horizontal
$0.distribution = .fill
$0.alignment = .fill
}
internal var decrementButton = ButtonIcon().with {
$0.kind = .ghost
$0.iconName = Icon.Name(name: "minus")
$0.iconOffset = .init(x: -2, y: 0)
$0.customContainerSize = 32
$0.icon.customSize = 16
$0.backgroundColor = .clear
}
internal var incrementButton = ButtonIcon().with {
$0.kind = .ghost
$0.iconName = Icon.Name(name: "plus")
$0.iconOffset = .init(x: 2, y: 0)
$0.customContainerSize = 32
$0.icon.customSize = 16
$0.backgroundColor = .clear
}
internal var textLabel = Label().with {
$0.setContentCompressionResistancePriority(.required, for: .vertical)
$0.textStyle = .boldBodyLarge
$0.numberOfLines = 1
$0.lineBreakMode = .byTruncatingTail
}
//--------------------------------------------------
// MARK: - Constraints
//--------------------------------------------------
internal var stepperWidthConstraint: NSLayoutConstraint?
internal var stepperHeightConstraint: NSLayoutConstraint?
//--------------------------------------------------
// MARK: - Configuration Properties
//--------------------------------------------------
internal override var containerSize: CGSize { CGSize(width: size == .large ? largeMinWidth : smallMinWidth, height: size == .large ? largeMinHeight : smallMinHeight) }
internal var largeMinWidth = 121
internal var smallMinWidth = 90
internal var largeMinHeight = 44
internal var smallMinHeight = 32
internal let labelColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight , VDSColor.elementsPrimaryOndark)
internal let labelDisabledColorConfiguration = SurfaceColorConfiguration(VDSColor.interactiveDisabledOnlight , VDSColor.interactiveDisabledOndark)
//--------------------------------------------------
// MARK: - Lifecycle
//--------------------------------------------------
open override func initialSetup() {
super.initialSetup()
}
/// 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()
// accessibility
isAccessibilityElement = false
accessibilityLabel = "Input Stepper"
// Set initial states
containerView.isEnabled = false
statusIcon.isHidden = true
// Add listeners
decrementButton.onClick = { _ in self.decrementButtonClick() }
incrementButton.onClick = { _ in self.incrementButtonClick() }
// setting color config
textLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable()
}
open override func getFieldContainer() -> UIView {
stepperStackView.addArrangedSubview(decrementButton)
stepperStackView.addArrangedSubview(textLabel)
stepperStackView.addArrangedSubview(incrementButton)
// Set space between decrement button, label, and increment button relative to input Stepper size.
let space = size == .large ? VDSLayout.space3X : VDSLayout.space2X
stepperStackView.setCustomSpacing(space, after: decrementButton)
stepperStackView.setCustomSpacing(space, after: textLabel)
// Update Edge insets relative to input Stepper size.
stepperStackView.pinToSuperView(.uniform(size == .large ? 6.0 : VDSLayout.space1X))
// stepperContainerView for controls in EntryFieldBase.controlContainerView
stepperContainerView.addSubview(stepperStackView)
stepperWidthConstraint = stepperContainerView.widthAnchor.constraint(equalToConstant: containerSize.width)
stepperWidthConstraint?.deactivate()
stepperHeightConstraint = stepperContainerView.heightAnchor.constraint(equalToConstant: containerSize.height)
stepperHeightConstraint?.deactivate()
return stepperContainerView
}
/// Used to make changes to the View based off a change events or from local properties.
open override func updateView() {
super.updateView()
statusIcon.isHidden = true
updateConstraintsToFieldStackView(flag: false)
// Update label text, style, color, ande surface.
textLabel.text = String(defaultValue) + " " + (trailingText ?? "")
textLabel.textStyle = size == .large ? .boldBodyLarge : .boldBodySmall
textLabel.textColorConfiguration = !isEnabled ? labelDisabledColorConfiguration.eraseToAnyColorable() : labelColorConfiguration.eraseToAnyColorable()
textLabel.surface = surface
// Update increment and decrement button.
updateButtonStates()
// Update stepper container border and corner radius.
setControlWidth(controlWidth)
updateContainerView(flag: false)
}
/// Resets to default settings.
open override func reset() {
super.reset()
textLabel.reset()
textLabel.textStyle = .boldBodyLarge
textLabel.text = ""
controlWidth = nil
minValue = nil
maxValue = nil
trailingText = nil
helperTextPlacement = .bottom
}
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------
internal func checkDefaultValue() {
defaultValue = defaultValue > _maxValue ? _maxValue : defaultValue < _minValue ? _minValue : defaultValue
}
internal func decrementButtonClick() {
defaultValue = defaultValue - 1
checkDefaultValue()
updateButtonStates()
}
internal func incrementButtonClick() {
defaultValue = defaultValue + 1
checkDefaultValue()
updateButtonStates()
}
internal func updateButtonStates() {
decrementButton.customContainerSize = size == .large ? 32 : 24
incrementButton.customContainerSize = size == .large ? 32 : 24
decrementButton.surface = surface
incrementButton.surface = surface
if isReadOnly || !isEnabled {
decrementButton.isEnabled = false
incrementButton.isEnabled = false
} else {
decrementButton.isEnabled = defaultValue > _minValue ? true : false
incrementButton.isEnabled = defaultValue < _maxValue ? true : false
}
}
// Update edge insets and height when size changes.
internal func updateStepperContainerViewSize() {
updateButtonStates()
// Update Edge insets if size changes applied.
stepperStackView.removeFromSuperview()
stepperContainerView.addSubview(stepperStackView)
stepperStackView.pinToSuperView(.uniform(size == .large ? 6.0 : VDSLayout.space1X))
// Update height if size changes applied.
stepperHeightConstraint?.deactivate()
stepperHeightConstraint = stepperContainerView.heightAnchor.constraint(equalToConstant: containerSize.height)
stepperHeightConstraint?.activate()
}
// Set control width to input stepper.
internal func setControlWidth(_ text: String?) {
if let text, text == "auto" {
stepperWidthConstraint?.deactivate()
} else if let controlWidth = Int(text ?? "") {
// Set controlWidth provided which is either pixel or percentage
let width = width ?? CGFloat(containerView.frame.size.width)
updateStepperContainerWidth(controlWidth: CGFloat(controlWidth), width: width)
}
}
// Handling the controlwidth without going beyond the width of the parent container.
internal func updateStepperContainerWidth(controlWidth: CGFloat, width: CGFloat) {
if controlWidth >= containerSize.width && controlWidth <= width {
stepperWidthConstraint?.deactivate()
stepperWidthConstraint?.constant = controlWidth
stepperWidthConstraint?.activate()
} else if controlWidth >= width {
stepperWidthConstraint?.deactivate()
stepperWidthConstraint?.constant = width
stepperWidthConstraint?.activate()
}
}
}