Merge branch 'feature/buttonGroupUpdate' into 'develop'

created basebutton

See merge request BPHV_MIPS/vds_ios!17
This commit is contained in:
Bruce, Matt R 2022-12-01 18:04:42 +00:00
commit f852ae5084
13 changed files with 610 additions and 213 deletions

View File

@ -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 = "<group>"; };
EAB5FEEC2927E1B200998C17 /* ButtonGroupPositionLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroupPositionLayout.swift; sourceTree = "<group>"; };
EAB5FEF02927F4AA00998C17 /* SelfSizingCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelfSizingCollectionView.swift; sourceTree = "<group>"; };
EAB5FEF4292D371F00998C17 /* ButtonBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonBase.swift; sourceTree = "<group>"; };
EAC9257C29119B5400091998 /* TextLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextLink.swift; sourceTree = "<group>"; };
EAC925822911B35300091998 /* TextLinkCaret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextLinkCaret.swift; sourceTree = "<group>"; };
EAC925872911C9DE00091998 /* TextEntryField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextEntryField.swift; sourceTree = "<group>"; };
@ -203,6 +205,7 @@
isa = PBXGroup;
children = (
5FC35BE228D51405004EBEAC /* Button.swift */,
EAB5FEF4292D371F00998C17 /* ButtonBase.swift */,
);
path = Button;
sourceTree = "<group>";
@ -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 */,

View File

@ -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()

View File

@ -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 {

View File

@ -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 {

View File

@ -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<Void, Never>()
public var subscribers = Set<AnyCancellable>()
//--------------------------------------------------
// 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
}

View File

@ -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<Void, Never>()
public var subscribers = Set<AnyCancellable>()
//--------------------------------------------------
// 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)
}
}

View File

@ -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
}
}

View File

@ -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.numberOfSections {
let section = 0
// Get the necessary data (if implemented) from the delegates else provide default values
let insets: UIEdgeInsets = delegate.collectionView(collectionView, insetsForItemsInSection: section)
let rowSpacing: CGFloat = rowSpacingButton
// Variables to track individual item width and cumultative height of all items as they are being laid out.
var itemSize: CGSize = .zero
layoutHeight += insets.top
let totalItems = collectionView.numberOfItems(inSection: section)
for item in 0..<totalItems {
// Get the necessary data (if implemented) from the delegates else provide default values
let insets: UIEdgeInsets = delegate?.collectionView(collectionView, insetsForItemsInSection: section) ?? UIEdgeInsets.zero
let interItemSpacing: CGFloat = delegate?.collectionView(collectionView, itemSpacingInSection: section) ?? 0.0
itemSpacing = interItemSpacing
// Variables to track individual item width and cumultative height of all items as they are being laid out.
var itemSize: CGSize = .zero
itemSpacing = 0.0
layoutHeight += insets.top
let indexPath = IndexPath(item: item, section: section)
for item in 0..<collectionView.numberOfItems(inSection: section) {
let indexPath = IndexPath(item: item, section: section)
itemSize = delegate?.collectionView(collectionView, sizeForItemAtIndexPath: indexPath) ?? .zero
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 + 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..<rows.count {
let row = rows[item]
var rowSpacing = 0.0
if item > 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]? {

View File

@ -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)
}
}
}

View File

@ -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()
}
}

View File

@ -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{}

View File

@ -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

View File

@ -1,13 +1,26 @@
# ``VDS``
<!--@START_MENU_TOKEN@-->Summary<!--@END_MENU_TOKEN@-->
The Verizon Design System is the single source of truth for Verizons 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
<!--@START_MENU_TOKEN@-->Text<!--@END_MENU_TOKEN@-->
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
### <!--@START_MENU_TOKEN@-->Group<!--@END_MENU_TOKEN@-->
### Components
- <!--@START_MENU_TOKEN@-->``Symbol``<!--@END_MENU_TOKEN@-->
- ``Badge``
- ``Button``
- ``TextLink``
- ``TextLinkCaret``
- ``CheckboxGroup``
- ``Checkbox``
- ``Label``
- ``RadioBoxGroup``
- ``RadioBox``
- ``RadioButtonGroup``
- ``RadioButton``
- ``RadioSwatchGroup``
- ``RadioSwatch``
- ``Toggle``