diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 0244a9b3..18741ce7 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -61,6 +61,7 @@ EAB5FED429267EB300998C17 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB5FED329267EB300998C17 /* UIView.swift */; }; EAB5FEED2927E1B200998C17 /* ButtonGroupPositionLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB5FEEC2927E1B200998C17 /* ButtonGroupPositionLayout.swift */; }; EAB5FEF12927F4AA00998C17 /* SelfSizingCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB5FEF02927F4AA00998C17 /* SelfSizingCollectionView.swift */; }; + EAB5FEF5292D371F00998C17 /* ButtonBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB5FEF4292D371F00998C17 /* ButtonBase.swift */; }; EAC9257D29119B5400091998 /* TextLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC9257C29119B5400091998 /* TextLink.swift */; }; EAC925832911B35400091998 /* TextLinkCaret.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC925822911B35300091998 /* TextLinkCaret.swift */; }; EAC925842911C63100091998 /* Colorable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA5EEDF28F49DB3003B3210 /* Colorable.swift */; }; @@ -153,6 +154,7 @@ EAB5FED329267EB300998C17 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = ""; }; EAB5FEEC2927E1B200998C17 /* ButtonGroupPositionLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroupPositionLayout.swift; sourceTree = ""; }; EAB5FEF02927F4AA00998C17 /* SelfSizingCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelfSizingCollectionView.swift; sourceTree = ""; }; + EAB5FEF4292D371F00998C17 /* ButtonBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonBase.swift; sourceTree = ""; }; EAC9257C29119B5400091998 /* TextLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextLink.swift; sourceTree = ""; }; EAC925822911B35300091998 /* TextLinkCaret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextLinkCaret.swift; sourceTree = ""; }; EAC925872911C9DE00091998 /* TextEntryField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextEntryField.swift; sourceTree = ""; }; @@ -203,6 +205,7 @@ isa = PBXGroup; children = ( 5FC35BE228D51405004EBEAC /* Button.swift */, + EAB5FEF4292D371F00998C17 /* ButtonBase.swift */, ); path = Button; sourceTree = ""; @@ -654,6 +657,7 @@ EAF7F0AF289B144C00B287F5 /* UnderlineLabelAttribute.swift in Sources */, EAC925842911C63100091998 /* Colorable.swift in Sources */, EA3361C5289030FC0071C351 /* Accessable.swift in Sources */, + EAB5FEF5292D371F00998C17 /* ButtonBase.swift in Sources */, EA978EC5291D6AFE00ACC883 /* AnyLabelAttribute.swift in Sources */, EA33622C2891E73B0071C351 /* FontProtocol.swift in Sources */, EAF7F11728A1475A00B287F5 /* RadioButton.swift in Sources */, diff --git a/VDS/Classes/ColorConfiguration.swift b/VDS/Classes/ColorConfiguration.swift index 89cfe2c4..cafac5f9 100644 --- a/VDS/Classes/ColorConfiguration.swift +++ b/VDS/Classes/ColorConfiguration.swift @@ -68,7 +68,7 @@ extension DisabledSurfaceColorable { /// let textColor = config.getColor(model) //returns .white /// /// -final public class DisabledSurfaceColorConfiguration: DisabledSurfaceColorable { +open class DisabledSurfaceColorConfiguration: DisabledSurfaceColorable { public typealias ObjectType = Surfaceable & Disabling public var disabled = SurfaceColorConfiguration() public var enabled = SurfaceColorConfiguration() diff --git a/VDS/Classes/Control.swift b/VDS/Classes/Control.swift index bd37b435..90dae4e5 100644 --- a/VDS/Classes/Control.swift +++ b/VDS/Classes/Control.swift @@ -29,6 +29,8 @@ open class Control: UIControl, Handlerable, ViewProtocol, Resettable { open override var isSelected: Bool { didSet { didChange() } } + open override var isHighlighted: Bool { didSet { updateView() } } + open override var isEnabled: Bool { get { !disabled } set { diff --git a/VDS/Components/Badge/Badge.swift b/VDS/Components/Badge/Badge.swift index e61f11a9..9541a483 100644 --- a/VDS/Components/Badge/Badge.swift +++ b/VDS/Components/Badge/Badge.swift @@ -15,6 +15,8 @@ public enum BadgeFillColor: String, Codable, CaseIterable { case red, yellow, green, orange, blue, black, white } + +/// Badges are visual labels used to convey status or highlight supplemental information. @objc(VDSBadge) public class Badge: View, Accessable { diff --git a/VDS/Components/Buttons/Button/Button.swift b/VDS/Components/Buttons/Button/Button.swift index 89a9440b..72953bdb 100644 --- a/VDS/Components/Buttons/Button/Button.swift +++ b/VDS/Components/Buttons/Button/Button.swift @@ -11,26 +11,14 @@ import VDSColorTokens import VDSFormControlsTokens import Combine -public protocol Buttonable: UIControl, Surfaceable, Disabling { - var availableSizes: [ButtonSize] { get } - var text: String? { get set } - var intrinsicContentSize: CGSize { get } -} - public enum ButtonSize: String, Codable, CaseIterable { case large case small } @objc(VDSButton) -open class Button: UIButton, Buttonable, Handlerable, ViewProtocol, Resettable, Useable { +open class Button: ButtonBase, Useable { - //-------------------------------------------------- - // MARK: - Combine Properties - //-------------------------------------------------- - public var subject = PassthroughSubject() - public var subscribers = Set() - //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- @@ -42,31 +30,21 @@ open class Button: UIButton, Buttonable, Handlerable, ViewProtocol, Resettable, //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- - public var availableSizes: [ButtonSize] { [.large, .small] } + open override var availableSizes: [ButtonSize] { [.large, .small] } - open var text: String? { didSet { didChange() } } - open var use: Use = .primary { didSet { didChange() }} open var size: ButtonSize = .large { didSet { didChange() }} open var width: CGFloat? { didSet { didChange() }} - - open var surface: Surface = .light { didSet { didChange() }} - - open var disabled: Bool = false { didSet { isEnabled = !disabled } } - open override var isEnabled: Bool { - get { !disabled } - set { - if disabled != !newValue { - disabled = !newValue - } - isUserInteractionEnabled = isEnabled - didChange() - } + open override var textColor: UIColor { + buttonTitleColorConfiguration.getColor(self) + } + + open override var typograpicalStyle: TypographicalStyle { + size == .large ? TypographicalStyle.BoldBodyLarge : TypographicalStyle.BoldBodySmall } - //-------------------------------------------------- // MARK: - Configuration Properties @@ -76,23 +54,35 @@ open class Button: UIButton, Buttonable, Handlerable, ViewProtocol, Resettable, $0.primary.enabled.darkColor = VDSColor.backgroundPrimaryLight $0.primary.disabled.lightColor = VDSColor.interactiveDisabledOnlight $0.primary.disabled.darkColor = VDSColor.interactiveDisabledOndark + + $0.primaryHighlighted.lightColor = VDSColor.interactiveActiveOnlight + $0.primaryHighlighted.darkColor = VDSColor.interactiveActiveOndark $0.secondary.enabled.lightColor = UIColor.clear $0.secondary.enabled.darkColor = UIColor.clear $0.secondary.disabled.lightColor = UIColor.clear $0.secondary.disabled.darkColor = UIColor.clear + + $0.secondaryHighlighted.lightColor = UIColor.clear + $0.secondaryHighlighted.darkColor = UIColor.clear } - + private var buttonBorderColorConfiguration = UseableColorConfiguration().with { $0.primary.enabled.lightColor = VDSColor.elementsPrimaryOndark $0.primary.enabled.darkColor = VDSColor.elementsPrimaryOnlight $0.primary.disabled.lightColor = VDSColor.interactiveDisabledOnlight $0.primary.disabled.darkColor = VDSColor.interactiveDisabledOndark - + + $0.primaryHighlighted.lightColor = VDSColor.elementsPrimaryOndark + $0.primaryHighlighted.darkColor = VDSColor.elementsPrimaryOnlight + $0.secondary.enabled.lightColor = VDSColor.elementsPrimaryOnlight $0.secondary.enabled.darkColor = VDSColor.elementsPrimaryOndark $0.secondary.disabled.lightColor = VDSColor.interactiveDisabledOnlight $0.secondary.disabled.darkColor = VDSColor.interactiveDisabledOndark + + $0.secondaryHighlighted.lightColor = VDSColor.interactiveActiveOnlight + $0.secondaryHighlighted.darkColor = VDSColor.interactiveActiveOndark } private var buttonTitleColorConfiguration = UseableColorConfiguration().with { @@ -100,11 +90,17 @@ open class Button: UIButton, Buttonable, Handlerable, ViewProtocol, Resettable, $0.primary.enabled.darkColor = VDSColor.elementsPrimaryOnlight $0.primary.disabled.lightColor = VDSColor.elementsPrimaryOndark $0.primary.disabled.darkColor = VDSColor.elementsPrimaryOnlight - + + $0.primaryHighlighted.lightColor = VDSColor.elementsPrimaryOndark + $0.primaryHighlighted.darkColor = VDSColor.elementsPrimaryOnlight + $0.secondary.enabled.lightColor = VDSColor.elementsPrimaryOnlight $0.secondary.enabled.darkColor = VDSColor.elementsPrimaryOndark $0.secondary.disabled.lightColor = VDSColor.interactiveDisabledOnlight $0.secondary.disabled.darkColor = VDSColor.interactiveDisabledOndark + + $0.secondaryHighlighted.lightColor = VDSColor.interactiveActiveOnlight + $0.secondaryHighlighted.darkColor = VDSColor.interactiveActiveOndark } //-------------------------------------------------- @@ -112,91 +108,57 @@ open class Button: UIButton, Buttonable, Handlerable, ViewProtocol, Resettable, //-------------------------------------------------- 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: - Public Functions //-------------------------------------------------- - open func initialSetup() { - if !initialSetupPerformed { - backgroundColor = .clear - translatesAutoresizingMaskIntoConstraints = false - accessibilityCustomActions = [] - accessibilityTraits = .staticText - setup() - setupDidChangeEvent() - updateView() - } - } - open func setup() { - translatesAutoresizingMaskIntoConstraints = false - titleLabel?.adjustsFontSizeToFitWidth = false - titleLabel?.lineBreakMode = .byTruncatingTail - + open override func setup() { + super.setup() //only 1 of the 2 widths can be on at the same time widthConstraint = widthAnchor.constraint(equalToConstant: 0) minWidthConstraint = widthAnchor.constraint(greaterThanOrEqualToConstant: size.minimumWidth) //height - heightConstraint = heightAnchor.constraint(equalToConstant: size.height) + heightConstraint = heightAnchor.constraint(equalToConstant: 0) heightConstraint?.isActive = true } - open func reset() { - surface = .light - disabled = false + open override func reset() { + super.reset() use = .primary width = nil size = .large - accessibilityCustomActions = [] - accessibilityTraits = .staticText } //-------------------------------------------------- // 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) - } - - open func updateView() { - + open override func updateView() { + super.updateView() let bgColor = buttonBackgroundColorConfiguration.getColor(self) let borderColor = buttonBorderColorConfiguration.getColor(self) - let titleColor = buttonTitleColorConfiguration.getColor(self) let borderWidth = use == .secondary ? 1.0 : 0.0 let buttonHeight = size.height - let cornerRadius = buttonHeight / 2 + let cornerRadius = size.cornerRadius let minWidth = size.minimumWidth - let font = size == .large ? TypographicalStyle.BoldBodyLarge.font : TypographicalStyle.BoldBodySmall.font let edgeInsets = size.edgeInsets - - setTitle(text ?? "No Text", for: .normal) - titleLabel?.font = font + backgroundColor = bgColor - setTitleColor(titleColor, for: .normal) layer.borderColor = borderColor.cgColor layer.cornerRadius = cornerRadius layer.borderWidth = borderWidth contentEdgeInsets = edgeInsets - + minWidthConstraint?.constant = minWidth heightConstraint?.constant = buttonHeight @@ -215,17 +177,23 @@ open class Button: UIButton, Buttonable, Handlerable, ViewProtocol, Resettable, //-------------------------------------------------- private class UseableColorConfiguration: ObjectColorable { - typealias ObjectType = Disabling & Surfaceable & Useable + typealias ObjectType = Buttonable & Useable public var primary = DisabledSurfaceColorConfiguration() public var secondary = DisabledSurfaceColorConfiguration() + + public var primaryHighlighted = SurfaceColorConfiguration() + public var secondaryHighlighted = SurfaceColorConfiguration() required public init(){} public func getColor(_ object: ObjectType) -> UIColor { - return object.use == .primary ? primary.getColor(object) : secondary.getColor(object) + if object.isHighlighted { + return object.use == .primary ? primaryHighlighted.getColor(object) : secondaryHighlighted.getColor(object) + } else { + return object.use == .primary ? primary.getColor(object) : secondary.getColor(object) + } } - } - + } } extension ButtonSize { @@ -239,6 +207,10 @@ extension ButtonSize { } } + public var cornerRadius: CGFloat { + height / 2 + } + public var minimumWidth: CGFloat { switch self { case .large: @@ -267,7 +239,6 @@ extension ButtonSize { } extension Use { - public var color: UIColor { return self == .primary ? VDSColor.backgroundPrimaryDark : .clear } diff --git a/VDS/Components/Buttons/Button/ButtonBase.swift b/VDS/Components/Buttons/Button/ButtonBase.swift new file mode 100644 index 00000000..887fff2c --- /dev/null +++ b/VDS/Components/Buttons/Button/ButtonBase.swift @@ -0,0 +1,182 @@ +// +// BaseButton.swift +// VDS +// +// Created by Matt Bruce on 11/22/22. +// + +import Foundation +import UIKit +import VDSColorTokens +import VDSFormControlsTokens +import Combine + +public protocol Buttonable: UIControl, Surfaceable, Disabling { + var availableSizes: [ButtonSize] { get } + var text: String? { get set } + var intrinsicContentSize: CGSize { get } +} + +@objc(VDSButtonBase) +open class ButtonBase: UIButton, Buttonable, Handlerable, ViewProtocol, Resettable { + + //-------------------------------------------------- + // MARK: - Combine Properties + //-------------------------------------------------- + public var subject = PassthroughSubject() + public var subscribers = Set() + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + private var initialSetupPerformed = false + + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + open var availableSizes: [ButtonSize] { [] } + + open var text: String? { didSet { didChange() } } + + open var attributes: [any LabelAttributeModel]? { nil } + + open var surface: Surface = .light { didSet { didChange() }} + + open var disabled: Bool = false { didSet { isEnabled = !disabled } } + + open override var isHighlighted: Bool { didSet { if isHighlighted != oldValue { updateView() } } } + + open var typograpicalStyle: TypographicalStyle { .defaultStyle } + + open var textColor: UIColor { .black } + + open override var isEnabled: Bool { + get { !disabled } + set { + if disabled != !newValue { + disabled = !newValue + } + isUserInteractionEnabled = isEnabled + didChange() + } + } + + //-------------------------------------------------- + // 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: - Public Functions + //-------------------------------------------------- + open func initialSetup() { + if !initialSetupPerformed { + backgroundColor = .clear + translatesAutoresizingMaskIntoConstraints = false + accessibilityCustomActions = [] + accessibilityTraits = .staticText + setup() + setupDidChangeEvent() + updateView() + } + } + + open func setup() { + + translatesAutoresizingMaskIntoConstraints = false + + titleLabel?.adjustsFontSizeToFitWidth = false + titleLabel?.lineBreakMode = .byTruncatingTail + + } + + open func reset() { + surface = .light + disabled = false + text = nil + accessibilityCustomActions = [] + accessibilityTraits = .button + } + + //-------------------------------------------------- + // 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) + } + + open func updateView() { + updateLabel() + } + + //-------------------------------------------------- + // MARK: - PRIVATE + //-------------------------------------------------- + private func updateLabel() { + + let font = typograpicalStyle.font + + //clear the arrays holding actions + accessibilityCustomActions = [] + + //create the primary string + let startingAttributes = [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: textColor] + let mutableText = NSMutableAttributedString(string: text ?? "No Text", attributes: startingAttributes) + + //set the local lineHeight/lineSpacing attributes + //get the range + let entireRange = NSRange(location: 0, length: mutableText.length) + + //set letterSpacing + if typograpicalStyle.letterSpacing > 0.0 { + mutableText.addAttribute(.kern, value: typograpicalStyle.letterSpacing, range: entireRange) + } + + let paragraph = NSMutableParagraphStyle().with { + $0.alignment = titleLabel?.textAlignment ?? .center + $0.lineBreakMode = titleLabel?.lineBreakMode ?? .byTruncatingTail + } + + //set lineHeight + if typograpicalStyle.lineHeight > 0.0 { + let lineHeight = typograpicalStyle.lineHeight + let adjustment = lineHeight > font.lineHeight ? 2.0 : 1.0 + let baselineOffset = (lineHeight - font.lineHeight) / 2.0 / adjustment + paragraph.maximumLineHeight = lineHeight + paragraph.minimumLineHeight = lineHeight + mutableText.addAttribute(.baselineOffset, value: baselineOffset, range: entireRange) + mutableText.addAttribute( .paragraphStyle, value: paragraph, range: entireRange) + + } else { + mutableText.addAttribute( .paragraphStyle, value: paragraph, range: entireRange) + } + + if let attributes = attributes { + //loop through the models attributes + for attribute in attributes { + //add attribute on the string + attribute.setAttribute(on: mutableText) + } + } + + //set the attributed text + setAttributedTitle(mutableText, for: .normal) + setAttributedTitle(mutableText, for: .highlighted) + } +} diff --git a/VDS/Components/Buttons/ButtonGroup/ButtonGroup.swift b/VDS/Components/Buttons/ButtonGroup/ButtonGroup.swift index d15b34a3..28ddebd3 100644 --- a/VDS/Components/Buttons/ButtonGroup/ButtonGroup.swift +++ b/VDS/Components/Buttons/ButtonGroup/ButtonGroup.swift @@ -149,6 +149,8 @@ open class ButtonGroup: View, UICollectionViewDataSource, UICollectionViewDelega cell.subviews.forEach { $0.removeFromSuperview() } cell.addSubview(button) button.pinToSuperView() +// cell.layer.borderColor = UIColor.black.cgColor +// cell.layer.borderWidth = 1 return cell } @@ -156,11 +158,19 @@ open class ButtonGroup: View, UICollectionViewDataSource, UICollectionViewDelega buttons[indexPath.row].intrinsicContentSize } + public func collectionView(_ collectionView: UICollectionView, isButtonTypeForItemAtIndexPath indexPath: IndexPath) -> Bool { + if let _ = buttons[indexPath.row] as? Button { + return true + } else { + return false + } + } + + public func collectionView(_ collectionView: UICollectionView, buttonableAtIndexPath indexPath: IndexPath) -> Buttonable { + buttons[indexPath.row] + } + public func collectionView(_ collectionView: UICollectionView, insetsForItemsInSection section: Int) -> UIEdgeInsets { UIEdgeInsets.zero } - - public func collectionView(_ collectionView: UICollectionView, itemSpacingInSection section: Int) -> CGFloat { - itemSpacing - } } diff --git a/VDS/Components/Buttons/ButtonGroup/ButtonGroupPositionLayout.swift b/VDS/Components/Buttons/ButtonGroup/ButtonGroupPositionLayout.swift index 1a017135..64751ca2 100644 --- a/VDS/Components/Buttons/ButtonGroup/ButtonGroupPositionLayout.swift +++ b/VDS/Components/Buttons/ButtonGroup/ButtonGroupPositionLayout.swift @@ -8,27 +8,55 @@ import Foundation import UIKit +class ButtonLayoutAttributes: UICollectionViewLayoutAttributes{ + var spacing: CGFloat = 0 + var isButton: Bool = false + convenience init(spacing: CGFloat, + forCellWith indexPath: IndexPath) { + self.init(forCellWith: indexPath) + self.spacing = spacing + } +} + class ButtonCollectionViewRow { - var attributes = [UICollectionViewLayoutAttributes]() + var attributes = [ButtonLayoutAttributes]() var spacing: CGFloat = 0 init(spacing: CGFloat) { self.spacing = spacing } - func add(attribute: UICollectionViewLayoutAttributes) { + func add(attribute: ButtonLayoutAttributes) { attributes.append(attribute) } + + var hasButtons: Bool { + attributes.contains(where: { $0.isButton }) + } var rowWidth: CGFloat { return attributes.reduce(0, { result, attribute -> CGFloat in - return result + attribute.frame.width - }) + CGFloat(attributes.count - 1) * spacing + return result + attribute.frame.width + attribute.spacing + }) + } + + var rowHeight: CGFloat { + attributes.compactMap{$0.frame.height}.max() ?? 0 + } + + var rowY: CGFloat = 0 { + didSet { + for attribute in attributes { + attribute.frame.origin.y = rowY + } + } } func layout(for position: ButtonPosition, with collectionViewWidth: CGFloat){ var offset = 0.0 + attributes.last?.spacing = 0 + switch position { case .left: break @@ -40,7 +68,7 @@ class ButtonCollectionViewRow { for attribute in attributes { attribute.frame.origin.x = offset - offset += attribute.frame.width + spacing + offset += attribute.frame.width + attribute.spacing } } } @@ -51,8 +79,8 @@ public enum ButtonPosition: String, CaseIterable { protocol ButtongGroupPositionLayoutDelegate: AnyObject { func collectionView(_ collectionView: UICollectionView, sizeForItemAtIndexPath indexPath: IndexPath) -> CGSize + func collectionView(_ collectionView: UICollectionView, buttonableAtIndexPath indexPath: IndexPath) -> any Buttonable func collectionView(_ collectionView: UICollectionView, insetsForItemsInSection section: Int) -> UIEdgeInsets - func collectionView(_ collectionView: UICollectionView, itemSpacingInSection section: Int) -> CGFloat } class ButtonGroupPositionLayout: UICollectionViewLayout { @@ -62,58 +90,74 @@ class ButtonGroupPositionLayout: UICollectionViewLayout { // Total height of the content. Will be used to configure the scrollview content var layoutHeight: CGFloat = 0.0 var position: ButtonPosition = .left - private var itemCache: [UICollectionViewLayoutAttributes] = [] + private var itemCache: [ButtonLayoutAttributes] = [] override func prepare() { super.prepare() - + + let rowSpacingButton = 12.0 + let rowSpacingTextLink = 12.0 + itemCache.removeAll() layoutHeight = 0.0 - - guard let collectionView = collectionView else { + + guard let collectionView, let delegate else { return } var itemSpacing = 0.0 // Variable to track the width of the layout at the current state when the item is being drawn var layoutWidthIterator: CGFloat = 0.0 - for section in 0.. collectionView.frame.width { - // If the current row width (after this item being laid out) is exceeding the width of the collection view content, put it in the next line - layoutWidthIterator = 0.0 - layoutHeight += itemSize.height + interItemSpacing - } - - let frame = CGRect(x: layoutWidthIterator + insets.left, y: layoutHeight, width: itemSize.width, height: itemSize.height) - let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath) - attributes.frame = frame - itemCache.append(attributes) - layoutWidthIterator = layoutWidthIterator + frame.width + interItemSpacing + itemSize = delegate.collectionView(collectionView, sizeForItemAtIndexPath: indexPath) + + if (layoutWidthIterator + itemSize.width + insets.left + insets.right) > collectionView.frame.width { + // If the current row width (after this item being laid out) is exceeding the width of the collection view content, put it in the next line + layoutWidthIterator = 0.0 + layoutHeight += itemSize.height + rowSpacing + } + + let itemButtonable = delegate.collectionView(collectionView, buttonableAtIndexPath: indexPath) + + let nextItem = item + 1 + if nextItem < totalItems { + let neighbor = delegate.collectionView(collectionView, buttonableAtIndexPath: IndexPath(item: nextItem, section: section)) + itemSpacing = getHorizontalSpacing(for: itemButtonable, neighboring: neighbor) } - layoutHeight += itemSize.height + insets.bottom - layoutWidthIterator = 0.0 + let frame = CGRect(x: layoutWidthIterator + insets.left, y: layoutHeight, width: itemSize.width, height: itemSize.height) + //print(frame) + let attributes = ButtonLayoutAttributes(spacing: itemSpacing, forCellWith: indexPath) + attributes.frame = frame + attributes.isButton = isButton(buttonable: itemButtonable) + itemCache.append(attributes) + + layoutWidthIterator = layoutWidthIterator + frame.width + itemSpacing } + //print("*******") + layoutHeight += itemSize.height + insets.bottom + layoutWidthIterator = 0.0 + //Turn into rows and re-calculate var rows = [ButtonCollectionViewRow]() var currentRowY: CGFloat = -1 - + for attribute in itemCache { if currentRowY != attribute.frame.midY { currentRowY = attribute.frame.midY @@ -121,12 +165,156 @@ class ButtonGroupPositionLayout: UICollectionViewLayout { } rows.last?.add(attribute: attribute) } - + //recalculate rows based off of positions rows.forEach { $0.layout(for: position, with: collectionView.frame.width) } let rowAttributes = rows.flatMap { $0.attributes } + + layoutHeight = insets.top + for item in 0.. 0 && item < rows.count { + rowSpacing = row.hasButtons ? rowSpacingButton : rowSpacingTextLink + } + + if item > 0 { + row.rowY = layoutHeight + rowSpacing + layoutHeight += rowSpacing + } + + layoutHeight += row.rowHeight + } + layoutHeight += insets.bottom + + itemCache = rowAttributes - + + } + + func isButton(buttonable: Buttonable) -> Bool{ + if let _ = buttonable as? Button { + return true + } else { + return false + } + } + + func getHorizontalSpacing(for primary: Buttonable, neighboring: Buttonable) -> CGFloat { + let defaultSpace = 12.0 + //large button + if let button = primary as? Button, button.size == .large { + if let neighboringButton = neighboring as? Button, neighboringButton.size == .large { + return 12.0 + } else if let neighboringTextLink = neighboring as? TextLink, neighboringTextLink.size == .large { + return 16.0 + } else if let _ = neighboring as? TextLinkCaret { + return 24.0 + } else { + return defaultSpace + } + } + //large text link + else if let textLink = primary as? TextLink, textLink.size == .large { + if let neighboringButton = neighboring as? Button, neighboringButton.size == .large { + return 16.0 + } else if let _ = neighboring as? TextLinkCaret { + return 24.0 + } else if let neighboringTextLink = neighboring as? TextLink, neighboringTextLink.size == .large { + return 16.0 + } else { + return defaultSpace + } + } + //text link caret + else if let _ = primary as? TextLinkCaret { + if let _ = neighboring as? TextLinkCaret { + return 24.0 + } else { + return defaultSpace + } + } + //small button + else if let button = primary as? Button, button.size == .small { + if let neighboringButton = neighboring as? Button, neighboringButton.size == .small { + return 12.0 + } else if let neighboringTextLink = neighboring as? TextLink, neighboringTextLink.size == .small { + return 16.0 + } else { + return defaultSpace + } + } + //small text link + else if let textLink = primary as? TextLink, textLink.size == .small { + if let neighboringTextLink = neighboring as? TextLink, neighboringTextLink.size == .small { + return 16.0 + } else { + return defaultSpace + } + } + //return defaultSpace + else { + return defaultSpace + } + } + + func getVerticalSpacing(for primary: Buttonable, neighboring: Buttonable) -> CGFloat { + let defaultSpace = 12.0 + //large button + if let button = primary as? Button, button.size == .large { + if let neighboringButton = neighboring as? Button, neighboringButton.size == .large { + return 12.0 + } else if let neighboringTextLink = neighboring as? TextLink, neighboringTextLink.size == .large { + return 16.0 + } else if let _ = neighboring as? TextLinkCaret { + return 24.0 + } else { + return defaultSpace + } + } + //large text link + else if let textLink = primary as? TextLink, textLink.size == .large { + if let neighboringButton = neighboring as? Button, neighboringButton.size == .large { + return 16.0 + } else if let _ = neighboring as? TextLinkCaret { + return 24.0 + } else if let neighboringTextLink = neighboring as? TextLink, neighboringTextLink.size == .large { + return 24.0 + } else { + return defaultSpace + } + } + //text link caret + else if let _ = primary as? TextLinkCaret { + if let _ = neighboring as? TextLinkCaret { + return 24.0 + } else { + return defaultSpace + } + } + //small button + else if let button = primary as? Button, button.size == .small { + if let neighboringButton = neighboring as? Button, neighboringButton.size == .small { + return 12.0 + } else if let neighboringTextLink = neighboring as? TextLink, neighboringTextLink.size == .small { + return 24.0 + } else { + return defaultSpace + } + } + //small text link + else if let textLink = primary as? TextLink, textLink.size == .small { + if let neighboringTextLink = neighboring as? TextLink, neighboringTextLink.size == .small { + return 32.0 + } else { + return defaultSpace + } + } + //return defaultSpace + else { + return defaultSpace + } } override func layoutAttributesForElements(in rect: CGRect)-> [UICollectionViewLayoutAttributes]? { diff --git a/VDS/Components/Buttons/TextLink/TextLink.swift b/VDS/Components/Buttons/TextLink/TextLink.swift index f3e8a6f3..36e415ec 100644 --- a/VDS/Components/Buttons/TextLink/TextLink.swift +++ b/VDS/Components/Buttons/TextLink/TextLink.swift @@ -12,23 +12,40 @@ import VDSFormControlsTokens import Combine @objc(VDSTextLink) -open class TextLink: Control, Buttonable { +open class TextLink: ButtonBase { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- private var heightConstraint: NSLayoutConstraint? - private var label = Label() - + private var lineHeightConstraint: NSLayoutConstraint? + //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- - open var text: String? { didSet { didChange() } } - open var size: ButtonSize = .large { didSet { didChange() }} - public var availableSizes: [ButtonSize] { [.large, .small] } + open override var availableSizes: [ButtonSize] { [.large, .small] } + open override var typograpicalStyle: TypographicalStyle { + size == .large ? TypographicalStyle.BodyLarge : TypographicalStyle.BodySmall + } + + open override var textColor: UIColor { + textColorConfiguration.getColor(self) + } + + private var textColorConfiguration = HighlightDisabledSurfaceColorConfiguration().with { + $0.disabled.lightColor = VDSColor.elementsSecondaryOnlight + $0.disabled.darkColor = VDSColor.elementsSecondaryOndark + $0.enabled.lightColor = VDSColor.elementsPrimaryOnlight + $0.enabled.darkColor = VDSColor.elementsPrimaryOndark + + $0.highlighted.lightColor = VDSColor.interactiveActiveOnlight + $0.highlighted.darkColor = VDSColor.interactiveActiveOndark + + } + private var height: CGFloat { switch size { case .large: @@ -43,49 +60,42 @@ open class TextLink: Control, Buttonable { //-------------------------------------------------- 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() + } + + private var line = UIView().with { + $0.translatesAutoresizingMaskIntoConstraints = false } //-------------------------------------------------- // MARK: - Public Functions //-------------------------------------------------- - open override func initialSetup() { - super.initialSetup() - } - open override func setup() { super.setup() - addSubview(label) - - //add tapGesture to self - publisher(for: UITapGestureRecognizer()).sink { [weak self] _ in - self?.sendActions(for: .touchUpInside) - }.store(in: &subscribers) - - //pin stackview to edges - label.pinToSuperView() - label.numberOfLines = 1 + if let titleLabel { + addSubview(line) + line.pinLeading(titleLabel.leadingAnchor) + line.pinTrailing(titleLabel.trailingAnchor) + line.pinTop(titleLabel.bottomAnchor, 1) + lineHeightConstraint = line.heightAnchor.constraint(equalToConstant: 1.0) + lineHeightConstraint?.isActive = true + } + heightConstraint = heightAnchor.constraint(equalToConstant: height) heightConstraint?.isActive = true } open override func reset() { super.reset() - label.reset() - size = .large - text = nil - + size = .large accessibilityCustomActions = [] accessibilityTraits = .staticText } @@ -94,16 +104,36 @@ open class TextLink: Control, Buttonable { // MARK: - Overrides //-------------------------------------------------- override open var intrinsicContentSize: CGSize { - return CGSize(width: label.intrinsicContentSize.width, height: height) + var itemWidth = super.intrinsicContentSize.width + return CGSize(width: itemWidth, height: height) } open override func updateView() { - label.surface = surface - label.disabled = disabled - label.typograpicalStyle = size == .large ? TypographicalStyle.BodyLarge : TypographicalStyle.BodySmall - label.text = text ?? "" - label.attributes = [UnderlineLabelAttribute(location: 0, length: label.text!.count)] + //need to set the properties so the super class + //can render out the label correctly heightConstraint?.constant = height + lineHeightConstraint?.constant = isHighlighted ? 2.0 : 1.0 + line.backgroundColor = textColor + + //always call last so the label is rendered + super.updateView() } } + +class HighlightDisabledSurfaceColorConfiguration: ObjectColorable { + typealias ObjectType = Buttonable + public var highlighted = SurfaceColorConfiguration() + public var disabled = SurfaceColorConfiguration() + public var enabled = SurfaceColorConfiguration() + + required public init(){} + + public func getColor(_ object: any ObjectType) -> UIColor { + if object.isHighlighted { + return highlighted.getColor(object) + } else { + return object.disabled ? disabled.getColor(object) : enabled.getColor(object) + } + } +} diff --git a/VDS/Components/Buttons/TextLinkCaret/TextLinkCaret.swift b/VDS/Components/Buttons/TextLinkCaret/TextLinkCaret.swift index 577ff332..368e0c14 100644 --- a/VDS/Components/Buttons/TextLinkCaret/TextLinkCaret.swift +++ b/VDS/Components/Buttons/TextLinkCaret/TextLinkCaret.swift @@ -16,103 +16,103 @@ public enum TextLinkCaretPosition: String, CaseIterable { } @objc(VDSTextLinkCaret) -open class TextLinkCaret: Control, Buttonable { +open class TextLinkCaret: ButtonBase { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- private var heightConstraint: NSLayoutConstraint? - private var label = Label().with { - $0.typograpicalStyle = TypographicalStyle.BoldBodyLarge + open override var typograpicalStyle: TypographicalStyle { + TypographicalStyle.BoldBodyLarge } - + private var caretView = CaretView().with { $0.size = CaretView.CaretSize.small(.vertical) $0.lineWidth = 2 } + private var imageAttribute: ImageLabelAttribute? + + open override var attributes: [any LabelAttributeModel]? { + guard let imageAttribute else { return nil } + return [imageAttribute] + } //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- - public var availableSizes: [ButtonSize] { [.large] } - - open var text: String? { didSet { didChange() } } + public override var availableSizes: [ButtonSize] { [.large] } open var iconPosition: TextLinkCaretPosition = .right { didSet { didChange() } } private var height: CGFloat { 44 } + + private var _text: String? + + open override var text: String? { + get{ _text } + set { + var updatedText = newValue ?? "" + updatedText = iconPosition == .right ? "\(updatedText) " : " \(updatedText)" + _text = updatedText + didChange() + } + } + + open override var textColor: UIColor { + textColorConfiguration.getColor(self) + } + private var textColorConfiguration = HighlightDisabledSurfaceColorConfiguration().with { + $0.disabled.lightColor = VDSColor.elementsSecondaryOnlight + $0.disabled.darkColor = VDSColor.elementsSecondaryOndark + $0.enabled.lightColor = VDSColor.elementsPrimaryOnlight + $0.enabled.darkColor = VDSColor.elementsPrimaryOndark + + $0.highlighted.lightColor = VDSColor.interactiveActiveOnlight + $0.highlighted.darkColor = VDSColor.interactiveActiveOndark + } + //-------------------------------------------------- // 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: - Public Functions //-------------------------------------------------- - open override func initialSetup() { - super.initialSetup() - } - open override func setup() { super.setup() - //add tapGesture to self - publisher(for: UITapGestureRecognizer()).sink { [weak self] _ in - self?.sendActions(for: .touchUpInside) - }.store(in: &subscribers) //constraints heightAnchor.constraint(greaterThanOrEqualToConstant: height).isActive = true let size = caretView.size!.dimensions() caretView.frame = .init(x: 0, y: 0, width: size.width, height: size.height) - addSubview(label) - label.pinToSuperView() - - label.numberOfLines = 1 - } - //-------------------------------------------------- - // MARK: - Constraints - //-------------------------------------------------- - private var caretLeadingConstraint: NSLayoutConstraint? - private var caretTrailingConstraint: NSLayoutConstraint? - private var labelConstraint: NSLayoutConstraint? - open override func reset() { super.reset() - label.reset() - - label.typograpicalStyle = TypographicalStyle.BoldBodyLarge - text = nil iconPosition = .right - - accessibilityCustomActions = [] - accessibilityTraits = .staticText } //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- override open var intrinsicContentSize: CGSize { - var itemWidth = label.intrinsicContentSize.width + var itemWidth = super.intrinsicContentSize.width if let caretWidth = caretView.size?.dimensions().width { itemWidth += caretWidth } @@ -122,21 +122,18 @@ open class TextLinkCaret: Control, Buttonable { open override func updateView() { let updatedText = text ?? "" - caretView.surface = surface caretView.disabled = disabled caretView.direction = iconPosition == .right ? CaretView.Direction.right : CaretView.Direction.left let image = caretView.getImage() - let location = iconPosition == .right ? updatedText.count + 1 : 0 - let textColor = label.textColorConfiguration.getColor(self) - let imageAttribute = ImageLabelAttribute(location: location, + let location = iconPosition == .right ? updatedText.count : 0 + + imageAttribute = ImageLabelAttribute(location: location, image: image, tintColor: textColor) - label.surface = surface - label.disabled = disabled - label.text = iconPosition == .right ? "\(updatedText) " : " \(updatedText)" - label.attributes = [imageAttribute] + + super.updateView() } } diff --git a/VDS/Components/Checkbox/Checkbox.swift b/VDS/Components/Checkbox/Checkbox.swift index 22ff909b..21e31c9b 100644 --- a/VDS/Components/Checkbox/Checkbox.swift +++ b/VDS/Components/Checkbox/Checkbox.swift @@ -11,6 +11,7 @@ import VDSColorTokens import VDSFormControlsTokens import Combine +/// Checkboxes are a multi-select component through which a customer indicates a choice. If a binary choice, the component is a checkbox. If the choice has multiple options, the component is a ``CheckboxGroup``. @objc(VDSCheckbox) public class Checkbox: CheckboxBase{} diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index 7fcc931e..b1e9256f 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -11,10 +11,7 @@ import VDSColorTokens import Combine @objc(VDSLabel) -public class Label: LabelBase {} - -@objc(VDSLabelBase) -open class LabelBase: UILabel, Handlerable, ViewProtocol, Resettable { +public class Label: UILabel, Handlerable, ViewProtocol, Resettable { //-------------------------------------------------- // MARK: - Combine Properties diff --git a/VDS/VDS.docc/VDS.md b/VDS/VDS.docc/VDS.md index 27aecc21..0cf1e32e 100755 --- a/VDS/VDS.docc/VDS.md +++ b/VDS/VDS.docc/VDS.md @@ -1,13 +1,26 @@ # ``VDS`` -Summary +The Verizon Design System is the single source of truth for Verizon’s digital experiences. It aligns design and code resources to give designers and developers consistent, detailed documentation and standardized libraries of symbols and coded components. ## Overview -Text +Using the system allows designers and developers to collaborate more easily and efficiently on creating on-brand and accessible digital experiences. Spend more time improving our digital products for customers and less time redrawing or rebuilding basic user interface elements. ## Topics -### Group +### Components -- ``Symbol`` \ No newline at end of file +- ``Badge`` +- ``Button`` +- ``TextLink`` +- ``TextLinkCaret`` +- ``CheckboxGroup`` +- ``Checkbox`` +- ``Label`` +- ``RadioBoxGroup`` +- ``RadioBox`` +- ``RadioButtonGroup`` +- ``RadioButton`` +- ``RadioSwatchGroup`` +- ``RadioSwatch`` +- ``Toggle``