vds_ios/VDS/Components/Tilelet/Tilelet.swift
Matt Bruce 794f68a67f formatted
Signed-off-by: Matt Bruce <matt.bruce@verizon.com>
2023-08-30 16:57:15 -05:00

424 lines
15 KiB
Swift

//
// Tilet.swift
// VDS
//
// Created by Matt Bruce on 12/19/22.
//
import Foundation
import Foundation
import VDSColorTokens
import UIKit
import Combine
/// Tilelet can be configured with a background image and limited text to
/// support quick scanning and engagement. A Tilelet is fully clickable and
/// while it can include an arrow CTA, it does not require one in order to
/// function.
@objc(VDSTilelet)
open class Tilelet: TileContainer {
//--------------------------------------------------
// 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 to represent the Vertical Layout of the Text.
public enum TextPosition: String, CaseIterable {
case top
case bottom
}
/// Enum to represent the Width of the Text.
public enum TextWidth {
case value(CGFloat)
case percentage(CGFloat)
}
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
private var stackView = UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.axis = .vertical
$0.distribution = .fill
$0.spacing = 5
}
private var titleLockupContainerView = UIView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
}
private var badgeContainerView = UIView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
}
private let iconContainerView = UIView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.backgroundColor = .clear
}
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
public override var onClickSubscriber: AnyCancellable? {
didSet {
isAccessibilityElement = onClickSubscriber != nil
}
}
/// Title lockup positioned in the contentView.
open var titleLockup = TitleLockup().with {
$0.standardStyleConfiguration = .init(styleConfigurations: [
.init(deviceType: .iPhone,
titleStandardStyles: [.titleSmall],
spacingConfigurations: [
.init(otherStandardStyles: [.bodySmall, .bodyMedium],
topSpacing: VDSLayout.Spacing.space2X.value,
bottomSpacing: VDSLayout.Spacing.space2X.value)
]),
.init(deviceType: .iPhone,
titleStandardStyles: [.titleMedium, .titleLarge],
spacingConfigurations: [
.init(otherStandardStyles: [.bodySmall, .bodyMedium, .bodyLarge],
topSpacing: VDSLayout.Spacing.space2X.value,
bottomSpacing: VDSLayout.Spacing.space2X.value)
]),
.init(deviceType: .iPhone,
titleStandardStyles: [.titleXLarge],
spacingConfigurations: [
.init(otherStandardStyles: [.bodyLarge, .bodySmall, .bodyMedium, .titleMedium],
topSpacing: VDSLayout.Spacing.space3X.value,
bottomSpacing: VDSLayout.Spacing.space3X.value)
]),
.init(deviceType: .iPad,
titleStandardStyles: [.titleSmall, .titleMedium],
spacingConfigurations: [
.init(otherStandardStyles: [.bodySmall, .bodyMedium, .bodyLarge],
topSpacing: VDSLayout.Spacing.space2X.value,
bottomSpacing: VDSLayout.Spacing.space2X.value)
]),
.init(deviceType: .iPad,
titleStandardStyles: [.titleLarge],
spacingConfigurations: [
.init(otherStandardStyles: [.bodyLarge, .bodySmall, .bodyMedium, .titleSmall],
topSpacing: VDSLayout.Spacing.space3X.value,
bottomSpacing: VDSLayout.Spacing.space3X.value)
]),
.init(deviceType: .iPad,
titleStandardStyles: [.titleXLarge],
spacingConfigurations: [
.init(otherStandardStyles: [.titleMedium, .bodyLarge],
topSpacing: VDSLayout.Spacing.space3X.value,
bottomSpacing: VDSLayout.Spacing.space4X.value)
])
])
}
/// Badge positioned in the contentView.
open var badge = Badge().with {
$0.fillColor = .red
}
/// Descriptive Icon positioned in the contentView.
open var descriptiveIcon = Icon()
/// Directional Icon positioned in the contentView.
open var directionalIcon = Icon().with {
$0.name = .rightArrow
}
private var _textWidth: TextWidth?
/// If provided, width of Button components will be rendered based on this value. If omitted, default button widths are rendered.
open var textWidth: TextWidth? {
get { _textWidth }
set {
if let newValue {
switch newValue {
case .percentage(let percentage):
if percentage >= 10 && percentage <= 100.0 {
_textWidth = newValue
}
case .value(let value):
if value > 44.0 {
_textWidth = newValue
}
}
} else {
_textWidth = nil
}
setNeedsUpdate()
}
}
/// Determines where the text aligns vertically.
open var textPostion: TextPosition = .top { didSet { setNeedsUpdate() } }
/// If set, this is used to render the badge.
open var badgeModel: BadgeModel? { didSet { setNeedsUpdate() } }
/// If set, this is used to render the titleLabel of the TitleLockup.
open var titleModel: TitleModel? { didSet { setNeedsUpdate() } }
/// If set, this is used to render the subTitleLabel of the TitleLockup.
open var subTitleModel: SubTitleModel? { didSet { setNeedsUpdate() } }
//only 1 Icon can be active
private var _descriptiveIconModel: DescriptiveIcon?
/// If set, this is used to render the descriptive icon.
open var descriptiveIconModel: DescriptiveIcon? {
get { _descriptiveIconModel }
set {
_descriptiveIconModel = newValue;
_directionalIconModel = nil
setNeedsUpdate()
}
}
private var _directionalIconModel: DirectionalIcon?
/// If set, this is used to render the directional icon.
open var directionalIconModel: DirectionalIcon? {
get { _directionalIconModel }
set {
_directionalIconModel = newValue;
_descriptiveIconModel = nil
setNeedsUpdate()
}
}
//--------------------------------------------------
// MARK: - Constraints
//--------------------------------------------------
internal var titleLockupWidthConstraint: NSLayoutConstraint?
internal var titleLockupTrailingConstraint: NSLayoutConstraint?
//--------------------------------------------------
// 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()
width = 100
aspectRatio = .none
color = .black
addContentView(stackView)
accessibilityTraits = .link
accessibilityElements = [badge, titleLockup, descriptiveIcon, directionalIcon]
//badge
badgeContainerView.addSubview(badge)
badge
.pinTop()
.pinLeading()
.pinBottom()
badge.trailingAnchor.constraint(lessThanOrEqualTo: badgeContainerView.trailingAnchor).isActive = true
titleLockupContainerView.addSubview(titleLockup)
titleLockup
.pinTop()
.pinLeading()
.pinBottom()
titleLockupTrailingConstraint = titleLockup.trailingAnchor.constraint(equalTo: titleLockupContainerView.trailingAnchor)
titleLockupTrailingConstraint?.isActive = true
iconContainerView.addSubview(descriptiveIcon)
iconContainerView.addSubview(directionalIcon)
descriptiveIcon
.pinLeading()
.pinTop()
.pinBottom()
directionalIcon
.pinTrailing()
.pinTop()
.pinBottom()
}
/// Resets to default settings.
open override func reset() {
shouldUpdateView = false
aspectRatio = .none
color = .black
//models
badgeModel = nil
titleModel = nil
subTitleModel = nil
descriptiveIconModel = nil
directionalIconModel = nil
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()
updateBadge()
updateTitleLockup()
updateIcons()
layoutIfNeeded()
}
/// Used to update any Accessibility properties.
open override func updateAccessibility() {
super.updateAccessibility()
setAccessibilityLabel(for: [badge.label, titleLockup.eyebrowLabel, titleLockup.titleLabel, titleLockup.subTitleLabel])
}
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------
private func updateBadge() {
if let badgeModel {
badge.text = badgeModel.text
badge.fillColor = badgeModel.fillColor
badge.numberOfLines = badgeModel.numberOfLines
badge.surface = badgeModel.surface
badge.maxWidth = badgeModel.maxWidth
if badgeContainerView.superview == nil {
stackView.insertArrangedSubview(badgeContainerView, at: 0)
setNeedsLayout()
}
} else {
removeFromSuperview(badgeContainerView)
}
}
private func updateTitleLockup() {
var showTitleLockup = false
if let titleModel, !titleModel.text.isEmpty {
showTitleLockup = true
}
if let subTitleModel, !subTitleModel.text.isEmpty {
showTitleLockup = true
}
if showTitleLockup {
//flip the surface for the titleLockup
titleLockup.surface = color == .black ? Surface.dark : Surface.light
//titleLockup
if let textWidth {
titleLockupTrailingConstraint?.isActive = false
titleLockupTrailingConstraint = titleLockup.trailingAnchor.constraint(lessThanOrEqualTo: titleLockupContainerView.trailingAnchor)
titleLockupTrailingConstraint?.isActive = true
titleLockupWidthConstraint?.isActive = false
switch textWidth {
case .value(let value):
titleLockupWidthConstraint = titleLockup.widthAnchor.constraint(equalToConstant: value)
case .percentage(let percentage):
titleLockupWidthConstraint = NSLayoutConstraint(item: titleLockup,
attribute: .width,
relatedBy: .equal,
toItem: containerView,
attribute: .width,
multiplier: percentage / 100,
constant: 0.0)
}
titleLockupWidthConstraint?.isActive = true
} else {
titleLockupTrailingConstraint?.isActive = false
titleLockupTrailingConstraint = titleLockup.trailingAnchor.constraint(equalTo: titleLockupContainerView.trailingAnchor)
titleLockupTrailingConstraint?.isActive = true
titleLockupWidthConstraint?.isActive = false
}
//set models
titleLockup.titleModel = titleModel?.toTitleLockupTitleModel()
titleLockup.subTitleModel = subTitleModel?.toTitleLockupSubTitleModel()
if titleLockupContainerView.superview == nil {
stackView.insertArrangedSubview(titleLockupContainerView, at: badgeContainerView.superview == nil ? 0 : 1)
setNeedsLayout()
}
} else {
removeFromSuperview(titleLockupContainerView)
}
}
private func updateIcons() {
//icons
var showIconContainerView = false
if let descriptiveIconModel {
descriptiveIcon.name = descriptiveIconModel.name
descriptiveIcon.size = descriptiveIconModel.size
descriptiveIcon.surface = descriptiveIconModel.surface
showIconContainerView = true
}
if let directionalIconModel {
directionalIcon.size = directionalIconModel.size
directionalIcon.surface = directionalIconModel.surface
showIconContainerView = true
}
//iconContainer
descriptiveIcon.isHidden = descriptiveIconModel == nil
directionalIcon.isHidden = directionalIconModel == nil
if showIconContainerView {
//spacing before iconContainerView
var view: UIView?
if badgeContainerView.superview != nil {
view = badgeContainerView
}
if titleLockupContainerView.superview != nil {
view = titleLockupContainerView
}
if let view {
stackView.setCustomSpacing(padding.tiletSpacing, after: view)
}
if iconContainerView.superview == nil {
stackView.addArrangedSubview(iconContainerView)
setNeedsDisplay()
}
} else {
removeFromSuperview(iconContainerView)
}
}
}
extension TileContainer.Padding {
fileprivate var tiletSpacing: CGFloat {
switch self {
case .padding2X:
return 16
case .padding4X:
return 24
case .padding6X:
return 32
case .padding8X:
return 48
default:
return 16
}
}
}