// // BaseButton.swift // VDS // // Created by Matt Bruce on 11/22/22. // import Foundation import UIKit import VDSColorTokens import VDSFormControlsTokens import Combine /// Base class used for UIButton type classes. @objc(VDSButtonBase) open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- required public init() { super.init(frame: .zero) initialSetup() } public override init(frame: CGRect) { super.init(frame: .zero) initialSetup() } public required init?(coder: NSCoder) { super.init(coder: coder) initialSetup() } //-------------------------------------------------- // MARK: - Combine Properties //-------------------------------------------------- /// Set of Subscribers for any Publishers for this Control. open var subscribers = Set() open var onClickSubscriber: AnyCancellable? { willSet { if let onClickSubscriber { onClickSubscriber.cancel() } } } //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- private var initialSetupPerformed = false //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- /// Key of whether or not updateView() is called in setNeedsUpdate() open var shouldUpdateView: Bool = true open var surface: Surface = .light { didSet { setNeedsUpdate() } } /// Text that will be used in the titleLabel. open var text: String? { didSet { textSetMode = .text; setNeedsUpdate() } } /// Array of LabelAttributeModel objects used in rendering the text. open var textAttributes: [any LabelAttributeModel]? { nil } /// TextStyle used on the titleLabel. open var textStyle: TextStyle { .defaultStyle } /// UIColor used on the titleLabel text. open var textColor: UIColor { .black } /// Will determine if a scaled font should be used for the titleLabel font. open var useScaledFont: Bool = false { didSet { setNeedsUpdate() } } open var userInfo = [String: Primitive]() /// State of animating isHighlight. public var isHighlighting = false /// Whether the Button should handle the isHighlighted state. open var shouldHighlight: Bool { isHighlighting == false } /// Whether the Control is highlighted or not. open override var isHighlighted: Bool { didSet { if shouldHighlight { isHighlighting = true setNeedsUpdate() isHighlighting = false } } } /// Whether the Control is enabled or not. open override var isEnabled: Bool { didSet { setNeedsUpdate() } } //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- open func initialSetup() { if !initialSetupPerformed { initialSetupPerformed = true backgroundColor = .clear translatesAutoresizingMaskIntoConstraints = false accessibilityCustomActions = [] setup() setNeedsUpdate() } } open func setup() { translatesAutoresizingMaskIntoConstraints = false titleLabel?.adjustsFontSizeToFitWidth = false titleLabel?.lineBreakMode = .byTruncatingTail titleLabel?.numberOfLines = 1 } open func updateView() { restyleText() } open func updateAccessibility() { if isEnabled { accessibilityTraits.remove(.notEnabled) } else { accessibilityTraits.insert(.notEnabled) } } open func reset() { shouldUpdateView = false surface = .light isEnabled = true text = nil accessibilityCustomActions = [] shouldUpdateView = true setNeedsUpdate() } //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- override open var intrinsicContentSize: CGSize { let intrinsicContentSize = super.intrinsicContentSize let adjustedWidth = intrinsicContentSize.width + titleEdgeInsets.left + titleEdgeInsets.right let adjustedHeight = intrinsicContentSize.height + titleEdgeInsets.top + titleEdgeInsets.bottom return CGSize(width: adjustedWidth, height: adjustedHeight) } private enum TextSetMode { case text case attributedText } private var textSetMode: TextSetMode = .text /// :nodoc: override public func setTitle(_ title: String?, for state: UIControl.State) { // When text is set, we may need to re-style it as attributedText // with the correct paragraph style to achieve the desired line height. text = title } /// :nodoc: override public func setAttributedTitle(_ title: NSAttributedString?, for state: UIControl.State) { // When text is set, we may need to re-style it as attributedText // with the correct paragraph style to achieve the desired line height. textSetMode = .attributedText styleAttributedText(title, for: state) } /// Gets or sets the text alignment of the button's title label. /// Default value = `.natural` public var textAlignment: NSTextAlignment = .natural { didSet { if textAlignment != oldValue { // Text alignment can be part of our paragraph style, so we may need to // re-style when changed restyleText() } } } /// Gets or sets the line break mode of the button's title label. /// Default value = `.byTruncatingTail` public var lineBreakMode: NSLineBreakMode = .byTruncatingTail { didSet { if lineBreakMode != oldValue { // Line break mode can be part of our paragraph style, so we may need to // re-style when changed restyleText() } } } //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- private func restyleText() { if textSetMode == .text { styleText(text, for: state) } else { styleAttributedText(currentAttributedTitle, for: state) } } private func styleText(_ newValue: String!, for state: UIControl.State) { defer { invalidateIntrinsicContentSize() } guard let newValue else { // We don't need to use attributed text super.setAttributedTitle(nil, for: state) super.setTitle(newValue, for: state) return } accessibilityCustomActions = [] //create the primary string let mutableText = NSMutableAttributedString.mutableText(for: newValue, textStyle: textStyle, useScaledFont: useScaledFont, textColor: textColor, alignment: titleLabel?.textAlignment ?? .center, lineBreakMode: titleLabel?.lineBreakMode ?? .byTruncatingTail) //apply any attributes if let attributes = textAttributes { mutableText.apply(attributes: attributes) } // Set attributed text to match typography super.setAttributedTitle(mutableText, for: state) } private func styleAttributedText(_ newValue: NSAttributedString?, for state: UIControl.State) { defer { invalidateIntrinsicContentSize() } guard let newValue = newValue else { // We don't need any additional styling super.setAttributedTitle(newValue, for: state) return } //clear the arrays holding actions accessibilityCustomActions = [] let mutableText = NSMutableAttributedString(attributedString: newValue) //apply any attributes if let attributes = textAttributes { mutableText.apply(attributes: attributes) } // Modify attributed text to match typography super.setAttributedTitle(mutableText, for: state) } } // MARK: AppleGuidelinesTouchable extension ButtonBase: AppleGuidelinesTouchable { /// Overrides to ensure that the touch point meets a minimum of the minimumTappableArea. override open func point(inside point: CGPoint, with event: UIEvent?) -> Bool { Self.acceptablyOutsideBounds(point: point, bounds: bounds) } }