// // VDSLabel.swift // VDS // // Created by Matt Bruce on 7/28/22. // import Foundation import UIKit import VDSColorTokens import Combine open class Label: UILabel, ModelHandlerable, Initable, Resettable { //-------------------------------------------------- // MARK: - Combine Properties //-------------------------------------------------- @Published public var model: LabelModel = DefaultLabelModel() private var cancellable: AnyCancellable? //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @Proxy(\.model.fontSize) public var fontSize: FontSize @Proxy(\.model.textPosition) public var textPosition: TextPosition @Proxy(\.model.fontWeight) public var fontWeight: FontWeight @Proxy(\.model.fontCategory) public var fontCategory: FontCategory @Proxy(\.model.surface) public var surface: Surface //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- required public convenience init() { self.init(frame: .zero) } public required convenience init(with model: LabelModel) { self.init() self.model = model set(with: model) } public override init(frame: CGRect) { super.init(frame: frame) setup() } required public init?(coder: NSCoder) { super.init(coder: coder) setup() } //-------------------------------------------------- // MARK: - Public Functions //-------------------------------------------------- open func setup() { backgroundColor = .clear numberOfLines = 0 lineBreakMode = .byWordWrapping translatesAutoresizingMaskIntoConstraints = false accessibilityCustomActions = [] accessibilityTraits = .staticText cancellable = $model.debounce(for: .seconds(Constants.ModelStateDebounce), scheduler: RunLoop.main).sink { [weak self] viewModel in self?.onStateChange(viewModel: viewModel) } } public func reset() { text = nil attributedText = nil textColor = .black font = FontStyle.RegularBodyLarge.font textAlignment = .left accessibilityCustomActions = [] accessibilityTraits = .staticText numberOfLines = 0 } //Modelable open func set(with model: LabelModel) { self.model = model } //-------------------------------------------------- // MARK: - State //-------------------------------------------------- /// Follow the SwiftUI View paradigm /// - Parameter viewModel: state open func onStateChange(viewModel: LabelModel) { textAlignment = viewModel.textPosition.textAlignment textColor = getTextColor(for: viewModel.disabled, surface: viewModel.surface) if let vdsFont = try? FontStyle.font(for: viewModel.fontCategory, fontWeight: viewModel.fontWeight, fontSize: viewModel.fontSize) { font = vdsFont } else { font = FontStyle.RegularBodyLarge.font } if let attributes = viewModel.attributes, let text = model.text, let font = font, let textColor = textColor { let startingAttributes = [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: textColor] let mutableText = NSMutableAttributedString(string: text, attributes: startingAttributes) for attribute in attributes { attribute.setAttribute(on: mutableText) if let actionable = attribute as? LabelAttributeActionable{ actions.append(actionable) } } attributedText = mutableText } else { text = viewModel.text } } //-------------------------------------------------- // MARK: - Private Functions //-------------------------------------------------- private func getTextColor(for disabled: Bool, surface: Surface) -> UIColor { if disabled { if surface == .light { return VDSColor.elementsSecondaryOnlight } else { return VDSColor.elementsSecondaryOndark } } else { if surface == .light { return VDSColor.elementsPrimaryOnlight } else { return VDSColor.elementsPrimaryOndark } } } //-------------------------------------------------- // MARK: - Actionable //-------------------------------------------------- private var tapGesture: UITapGestureRecognizer? private var actions: [LabelAttributeActionable] = [] { didSet { isUserInteractionEnabled = !actions.isEmpty if actions.isEmpty { if let tapGesture = tapGesture { removeGestureRecognizer(tapGesture) } } else { //add tap gesture if tapGesture == nil { let singleTap = UITapGestureRecognizer(target: self, action: #selector(textLinkTapped)) singleTap.numberOfTapsRequired = 1 addGestureRecognizer(singleTap) tapGesture = singleTap } if actions.count > 1 { actions.sort { first, second in return first.range.location < second.range.location } } } } } @objc private func textLinkTapped(_ gesture: UITapGestureRecognizer) { for actionable in actions { // This determines if we tapped on the desired range of text. if gesture.didTapAttributedTextInLabel(self, inRange: actionable.range) { actionable.action() return } } } }