// // Button.swift // VDS // // Created by Jarrod Courtney on 9/16/22. // import Foundation import UIKit import VDSColorTokens import VDSFormControlsTokens import Combine /// A button is an interactive element that triggers an action. Buttons are prominent and attention-getting, with more visual emphasis than any of the Text Link components. For this reason, buttons are best suited for critical and driving actions. This class can be used within a ``ButtonGroup``. /// /// If you are using AutoLayoutConstraints you have a combination of Leading/Left and Trailing/Right NSLayoutConstraints, /// you need to ensure that one of these Horizontal Contraints is not constraint of "equatTo". If you are to pin the left/right edges /// to its parent this object will stretch to the parent's width. @objc(VDSButton) open class Button: ButtonBase, Useable { //-------------------------------------------------- // 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. public enum Size: String, CaseIterable { case large case small /// Height for this size of button. public var height: CGFloat { switch self { case .large: return 44 case .small: return 32 } } /// Corner radius for this size of button. public var cornerRadius: CGFloat { height / 2 } /// Minimum width for this size of button. public var minimumWidth: CGFloat { switch self { case .large: return 76 case .small: return 60 } } /// EdgeInsets for this size of button. public var edgeInsets: UIEdgeInsets { switch self { case .large: return .axis(horizontal: 24, vertical: 12) case .small: return .axis(horizontal: 16, vertical: 8) } } } /// Enum used to describe the width of a fixed value or percentage of parent's width. public enum Width { case percentage(CGFloat) case value(CGFloat) } //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- private var initialSetupPerformed = false //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- /// The ButtonSize for ths Button. open var size: Size = .large { didSet { setNeedsUpdate() } } /// The Use for this Button. open var use: Use = .primary { didSet { setNeedsUpdate() } } private var _width: Width? = nil /// If there is a width that is larger than this size's minmumWidth, the button will resize to this width. open var width: Width? { get { _width } set { if let newValue { switch newValue { case .percentage(let percentage): if percentage <= 100.0 { _width = newValue } case .value(let value): if value > 0 && value > size.minimumWidth { _width = newValue } } } else { _width = nil } setNeedsUpdate() } } open override var textColor: UIColor { textColorConfiguration.getColor(self) } /// TextStyle used on the titleLabel. open override var textStyle: TextStyle { size == .large ? TextStyle.boldBodyLarge : TextStyle.boldBodySmall } /// The natural size for the receiving view, considering only properties of the view itself. open override var intrinsicContentSize: CGSize { // get the intrinsic size var defaultSize = super.intrinsicContentSize // ensure the size height defaultSize.height = size.height // take the max width either intrinsic or size's minimumWidth defaultSize.width = max(defaultSize.width, size.minimumWidth) guard let width else { return defaultSize } switch width { case .percentage(let percentage): // test the superview's width against the percentage to ensure // it is greater than the size's minimum width guard let superWidth = superview?.frame.width else { return defaultSize } // if so set the width off percentage defaultSize.width = max(superWidth * (percentage / 100), size.minimumWidth) return defaultSize case .value(let value): // test fixed value vs minimum width and take the greater value defaultSize.width = max(value, size.minimumWidth) return defaultSize } } //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- // Background Color Config private var primaryBackgroundColorConfiguration = ControlColorConfiguration().with { $0.setSurfaceColors(VDSColor.backgroundPrimaryDark, VDSColor.backgroundPrimaryLight, forState: .normal) $0.setSurfaceColors(VDSColor.interactiveActiveOnlight, VDSColor.interactiveActiveOndark, forState: .highlighted) $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled) } private var secondaryBackgroundColorConfiguration = ControlColorConfiguration() private var backgroundColorConfiguration: ControlColorConfiguration{ use == .primary ? primaryBackgroundColorConfiguration : secondaryBackgroundColorConfiguration } // Border Color Config private var primaryBorderColorConfiguration = ControlColorConfiguration().with { $0.setSurfaceColors(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOnlight, forState: .normal) $0.setSurfaceColors(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOnlight, forState: .highlighted) $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled) } private var secondaryBorderColorConfiguration = ControlColorConfiguration().with { $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .normal) $0.setSurfaceColors(VDSColor.interactiveActiveOnlight, VDSColor.interactiveActiveOndark, forState: .highlighted) $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled) } private var borderColorConfiguration: ControlColorConfiguration { use == .primary ? primaryBorderColorConfiguration : secondaryBorderColorConfiguration } // Text Color Config private var primaryTextColorConfiguration = ControlColorConfiguration().with { $0.setSurfaceColors(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOnlight, forState: .normal) $0.setSurfaceColors(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOnlight, forState: .highlighted) $0.setSurfaceColors(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOnlight, forState: .disabled) } private var secondaryTextColorConfiguration = ControlColorConfiguration().with { $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .normal) $0.setSurfaceColors(VDSColor.interactiveActiveOnlight, VDSColor.interactiveActiveOndark, forState: .highlighted) $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled) } private var textColorConfiguration: ControlColorConfiguration { use == .primary ? primaryTextColorConfiguration : secondaryTextColorConfiguration } //-------------------------------------------------- // 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() isAccessibilityElement = true accessibilityTraits = .button } /// Resets to default settings. open override func reset() { super.reset() shouldUpdateView = false use = .primary width = nil size = .large shouldUpdateView = true setNeedsUpdate() } /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { super.updateView() let bgColor = backgroundColorConfiguration.getColor(self) let borderColor = borderColorConfiguration.getColor(self) let borderWidth = use == .secondary ? VDSFormControls.widthBorder : 0.0 let cornerRadius = size.cornerRadius let edgeInsets = size.edgeInsets backgroundColor = bgColor layer.borderColor = borderColor.cgColor layer.cornerRadius = cornerRadius layer.borderWidth = borderWidth contentEdgeInsets = edgeInsets invalidateIntrinsicContentSize() } open override func layoutSubviews() { super.layoutSubviews() invalidateIntrinsicContentSize() } } extension Use { public var color: UIColor { return self == .primary ? VDSColor.backgroundPrimaryDark : .clear } }