// // Toggle.swift // VDS // // Created by Matt Bruce on 7/22/22. // import Foundation import UIKit import VDSColorTokens import Combine public enum ToggleTextSize: String, CaseIterable { case small, large } public enum ToggleTextWeight: String, CaseIterable { case regular, bold } public enum ToggleTextPosition: String, CaseIterable { case left, right } /** A custom implementation of Apple's UISwitch. By default this class begins in the off state. Container: The background of the toggle control. Knob: The circular indicator that slides on the container. */ @objc(VDSToggle) public class Toggle: ToggleBase{ public override func initialSetup() { super.initialSetup() publisher(for: .touchUpInside) .sink { control in control.toggle() }.store(in: &subscribers) } } @objc(VDSToggleBase) open class ToggleBase: Control, Accessable, DataTrackable, BinaryColorable { //-------------------------------------------------- // 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 stackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .horizontal $0.distribution = .fill } }() private var label = Label() private var toggleView: UIView = { return UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false } }() private var knobView: UIView = { return UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.backgroundColor = .white } }() //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- // Sizes are from InVision design specs. public let toggleSize = CGSize(width: 52, height: 24) public let toggleContainerSize = CGSize(width: 52, height: 44) public let knobSize = CGSize(width: 20, height: 20) private var toggleColorConfiguration = BinaryDisabledSurfaceColorConfiguration().with { $0.forTrue.enabled.lightColor = VDSColor.paletteGreen26 $0.forTrue.enabled.darkColor = VDSColor.paletteGreen34 $0.forTrue.disabled.lightColor = VDSColor.interactiveDisabledOnlight $0.forTrue.disabled.darkColor = VDSColor.interactiveDisabledOndark $0.forFalse.enabled.lightColor = VDSColor.elementsSecondaryOnlight $0.forFalse.enabled.darkColor = VDSColor.paletteGray44 $0.forFalse.disabled.lightColor = VDSColor.interactiveDisabledOnlight $0.forFalse.disabled.darkColor = VDSColor.interactiveDisabledOndark } private var knobColorConfiguration = BinaryDisabledSurfaceColorConfiguration().with { $0.forTrue.enabled.lightColor = VDSColor.elementsPrimaryOndark $0.forTrue.enabled.darkColor = VDSColor.elementsPrimaryOndark $0.forTrue.disabled.lightColor = VDSColor.paletteGray95 $0.forTrue.disabled.darkColor = VDSColor.paletteGray44 $0.forFalse.enabled.lightColor = VDSColor.elementsPrimaryOndark $0.forFalse.enabled.darkColor = VDSColor.elementsPrimaryOndark $0.forFalse.disabled.lightColor = VDSColor.paletteGray95 $0.forFalse.disabled.darkColor = VDSColor.paletteGray44 } private var typograpicalStyle: TypographicalStyle { if textSize == .small { if textWeight == .bold { return .BoldBodySmall } else { return .BodySmall } } else { if textWeight == .bold { return .BoldBodyLarge } else { return .BodyLarge } } } //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- open var isOn: Bool = false { didSet { didChange() }} open var showText: Bool = false { didSet { didChange() }} open var onText: String = "On" { didSet { didChange() }} open var offText: String = "Off" { didSet { didChange() }} open var textSize: ToggleTextSize = .small { didSet { didChange() }} open var textWeight: ToggleTextWeight = .regular { didSet { didChange() }} open var textPosition: ToggleTextPosition = .left { didSet { didChange() }} open var inputId: String? { didSet { didChange() }} open var value: AnyHashable? { didSet { didChange() }} open var dataAnalyticsTrack: String? { didSet { didChange() }} open var dataClickStream: String? { didSet { didChange() }} open var dataTrack: String? { didSet { didChange() }} open var accessibilityHintEnabled: String? { didSet { didChange() }} open var accessibilityHintDisabled: String? { didSet { didChange() }} open var accessibilityValueEnabled: String? { didSet { didChange() }} open var accessibilityValueDisabled: String? { didSet { didChange() }} open var accessibilityLabelEnabled: String? { didSet { didChange() }} open var accessibilityLabelDisabled: String? { didSet { didChange() }} //-------------------------------------------------- // MARK: - Constraints //-------------------------------------------------- private var knobLeadingConstraint: NSLayoutConstraint? private var knobTrailingConstraint: NSLayoutConstraint? private var knobHeightConstraint: NSLayoutConstraint? private var knobWidthConstraint: NSLayoutConstraint? private var toggleHeightConstraint: NSLayoutConstraint? private var toggleWidthConstraint: NSLayoutConstraint? //functions //-------------------------------------------------- // MARK: - Toggle //-------------------------------------------------- private func constrainKnob(){ self.knobLeadingConstraint?.isActive = false self.knobTrailingConstraint?.isActive = false if isOn { self.knobTrailingConstraint = self.toggleView.trailingAnchor.constraint(equalTo: self.knobView.trailingAnchor, constant: 2) self.knobLeadingConstraint = self.knobView.leadingAnchor.constraint(greaterThanOrEqualTo: self.toggleView.leadingAnchor) } else { self.knobTrailingConstraint = self.toggleView.trailingAnchor.constraint(greaterThanOrEqualTo: self.knobView.trailingAnchor) self.knobLeadingConstraint = self.knobView.leadingAnchor.constraint(equalTo: self.toggleView.leadingAnchor, constant: 2) } self.knobTrailingConstraint?.isActive = true self.knobLeadingConstraint?.isActive = true self.knobWidthConstraint?.constant = self.knobSize.width self.layoutIfNeeded() } private func updateToggle() { let toggleColor = toggleColorConfiguration.getColor(self) let knobColor = knobColorConfiguration.getColor(self) if disabled { toggleView.backgroundColor = toggleColor knobView.backgroundColor = knobColor constrainKnob() } else { UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseIn, animations: { self.toggleView.backgroundColor = toggleColor self.knobView.backgroundColor = knobColor }, completion: nil) UIView.animate(withDuration: 0.33, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5, options: [], animations: { [weak self] in self?.constrainKnob() }, completion: nil) } } //-------------------------------------------------- // MARK: - Labels //-------------------------------------------------- private func updateLabel() { stackView.spacing = showText ? 12 : 0 if stackView.subviews.contains(label) { label.removeFromSuperview() } if showText { label.textPosition = textPosition == .left ? .left : .right label.typograpicalStyle = typograpicalStyle label.text = isOn ? onText : offText label.surface = surface label.disabled = disabled if textPosition == .left { stackView.insertArrangedSubview(label, at: 0) } else { stackView.addArrangedSubview(label) } } } //-------------------------------------------------- // 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 let toggleContainerView = UIView() toggleContainerView.translatesAutoresizingMaskIntoConstraints = false toggleContainerView.backgroundColor = .clear toggleContainerView.widthAnchor.constraint(equalToConstant: toggleContainerSize.width).isActive = true toggleContainerView.heightAnchor.constraint(equalToConstant: toggleContainerSize.height).isActive = true toggleHeightConstraint = toggleView.heightAnchor.constraint(equalToConstant: toggleSize.height) toggleHeightConstraint?.isActive = true toggleWidthConstraint = toggleView.widthAnchor.constraint(equalToConstant: toggleSize.width) toggleWidthConstraint?.isActive = true toggleView.layer.cornerRadius = toggleSize.height / 2.0 knobView.layer.cornerRadius = knobSize.height / 2.0 toggleView.backgroundColor = toggleColorConfiguration.getColor(self) toggleContainerView.addSubview(toggleView) toggleView.addSubview(knobView) knobHeightConstraint = knobView.heightAnchor.constraint(equalToConstant: knobSize.height) knobHeightConstraint?.isActive = true knobWidthConstraint = knobView.widthAnchor.constraint(equalToConstant: knobSize.width) knobWidthConstraint?.isActive = true knobView.centerYAnchor.constraint(equalTo: toggleView.centerYAnchor).isActive = true knobView.topAnchor.constraint(greaterThanOrEqualTo: toggleView.topAnchor).isActive = true toggleView.bottomAnchor.constraint(greaterThanOrEqualTo: knobView.bottomAnchor).isActive = true updateLabel() stackView.addArrangedSubview(toggleContainerView) stackView.topAnchor.constraint(equalTo: topAnchor).isActive = true stackView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true stackView.widthAnchor.constraint(greaterThanOrEqualToConstant: toggleContainerSize.width).isActive = true stackView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true toggleView.centerXAnchor.constraint(equalTo: toggleContainerView.centerXAnchor).isActive = true toggleView.centerYAnchor.constraint(equalTo: toggleContainerView.centerYAnchor).isActive = true } public override func reset() { super.reset() toggleView.backgroundColor = toggleColorConfiguration.getColor(self) knobView.backgroundColor = knobColorConfiguration.getColor(self) setAccessibilityLabel() } /// This will toggle the state of the Toggle and execute the actionBlock if provided. open func toggle() { isOn.toggle() isSelected = isOn sendActions(for: .valueChanged) } //-------------------------------------------------- // MARK: - State //-------------------------------------------------- open override func updateView() { updateLabel() updateToggle() setAccessibilityHint() setAccessibilityValue(isOn) setAccessibilityLabel(isOn) backgroundColor = surface.color } }