Merge branch 'feature/tabsContainer' into 'develop'

added more view extensions

See merge request BPHV_MIPS/vds_ios!72
This commit is contained in:
Bruce, Matt R 2023-05-25 19:35:47 +00:00
commit a780a27e18
11 changed files with 818 additions and 8 deletions

View File

@ -43,10 +43,15 @@
EA4DB18528CA967F00103EE3 /* SelectorGroupHandlerBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA4DB18428CA967F00103EE3 /* SelectorGroupHandlerBase.swift */; };
EA4DB2FD28D3D0CA00103EE3 /* AnyEquatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA4DB2FC28D3D0CA00103EE3 /* AnyEquatable.swift */; };
EA4DB30228DCBCA500103EE3 /* Badge.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA4DB30128DCBCA500103EE3 /* Badge.swift */; };
EA596ABD2A16B4EC00300C4B /* Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA596ABC2A16B4EC00300C4B /* Tab.swift */; };
EA596ABF2A16B4F500300C4B /* Tabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA596ABE2A16B4F500300C4B /* Tabs.swift */; };
EA5E304C294CBDD00082B959 /* TileContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5E304B294CBDD00082B959 /* TileContainer.swift */; };
EA5E30532950DDA60082B959 /* TitleLockup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5E30522950DDA60082B959 /* TitleLockup.swift */; };
EA5E3058295105A40082B959 /* Tilelet.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5E3057295105A40082B959 /* Tilelet.swift */; };
EA5E305A29510F8B0082B959 /* EnumSubset.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5E305929510F8B0082B959 /* EnumSubset.swift */; };
EA5F86C82A1BD99100BC83E4 /* TabModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5F86C72A1BD99100BC83E4 /* TabModel.swift */; };
EA5F86CC2A1D28B500BC83E4 /* ReleaseNotes.txt in Resources */ = {isa = PBXBuildFile; fileRef = EA5F86CB2A1D28B500BC83E4 /* ReleaseNotes.txt */; };
EA5F86D02A1F936100BC83E4 /* TabsContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5F86CF2A1F936100BC83E4 /* TabsContainer.swift */; };
EA81410B2A0E8E3C004F60D2 /* ButtonIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA81410A2A0E8E3C004F60D2 /* ButtonIcon.swift */; };
EA8141102A127066004F60D2 /* UIColor+VDSColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA81410F2A127066004F60D2 /* UIColor+VDSColor.swift */; };
EA89200428AECF4B006B9984 /* UITextField+Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA89200328AECF4B006B9984 /* UITextField+Publisher.swift */; };
@ -164,10 +169,15 @@
EA4DB18428CA967F00103EE3 /* SelectorGroupHandlerBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectorGroupHandlerBase.swift; sourceTree = "<group>"; };
EA4DB2FC28D3D0CA00103EE3 /* AnyEquatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyEquatable.swift; sourceTree = "<group>"; };
EA4DB30128DCBCA500103EE3 /* Badge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Badge.swift; sourceTree = "<group>"; };
EA596ABC2A16B4EC00300C4B /* Tab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tab.swift; sourceTree = "<group>"; };
EA596ABE2A16B4F500300C4B /* Tabs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tabs.swift; sourceTree = "<group>"; };
EA5E304B294CBDD00082B959 /* TileContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileContainer.swift; sourceTree = "<group>"; };
EA5E30522950DDA60082B959 /* TitleLockup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleLockup.swift; sourceTree = "<group>"; };
EA5E3057295105A40082B959 /* Tilelet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tilelet.swift; sourceTree = "<group>"; };
EA5E305929510F8B0082B959 /* EnumSubset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnumSubset.swift; sourceTree = "<group>"; };
EA5F86C72A1BD99100BC83E4 /* TabModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabModel.swift; sourceTree = "<group>"; };
EA5F86CB2A1D28B500BC83E4 /* ReleaseNotes.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ReleaseNotes.txt; sourceTree = "<group>"; };
EA5F86CF2A1F936100BC83E4 /* TabsContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabsContainer.swift; sourceTree = "<group>"; };
EA81410A2A0E8E3C004F60D2 /* ButtonIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIcon.swift; sourceTree = "<group>"; };
EA81410F2A127066004F60D2 /* UIColor+VDSColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+VDSColor.swift"; sourceTree = "<group>"; };
EA89200328AECF4B006B9984 /* UITextField+Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextField+Publisher.swift"; sourceTree = "<group>"; };
@ -384,6 +394,7 @@
EA89200B28B530F0006B9984 /* RadioBox */,
EAF7F11428A1470D00B287F5 /* RadioButton */,
EA1F265F28B945070033E859 /* RadioSwatch */,
EA596ABB2A16B4D500300C4B /* Tabs */,
EAC925852911C9DE00091998 /* TextFields */,
EA5E304A294CBDBB0082B959 /* TileContainer */,
EA5E3056295105930082B959 /* Tilelet */,
@ -478,6 +489,7 @@
children = (
EA3361FF2891E14C0071C351 /* Fonts */,
EAA5EEB828ECD24B003B3210 /* Icons.xcassets */,
EA5F86CB2A1D28B500BC83E4 /* ReleaseNotes.txt */,
);
path = SupportingFiles;
sourceTree = "<group>";
@ -511,6 +523,17 @@
path = Badge;
sourceTree = "<group>";
};
EA596ABB2A16B4D500300C4B /* Tabs */ = {
isa = PBXGroup;
children = (
EA596ABC2A16B4EC00300C4B /* Tab.swift */,
EA5F86C72A1BD99100BC83E4 /* TabModel.swift */,
EA596ABE2A16B4F500300C4B /* Tabs.swift */,
EA5F86CF2A1F936100BC83E4 /* TabsContainer.swift */,
);
path = Tabs;
sourceTree = "<group>";
};
EA5E304A294CBDBB0082B959 /* TileContainer */ = {
isa = PBXGroup;
children = (
@ -778,6 +801,7 @@
buildActionMask = 2147483647;
files = (
EA3362042891E14D0071C351 /* VerizonNHGeTX-Bold.otf in Resources */,
EA5F86CC2A1D28B500BC83E4 /* ReleaseNotes.txt in Resources */,
EA3362072891E14D0071C351 /* VerizonNHGeDS-Regular.otf in Resources */,
EA3362062891E14D0071C351 /* VerizonNHGeTX-Regular.otf in Resources */,
EA3362052891E14D0071C351 /* VerizonNHGeDS-Bold.otf in Resources */,
@ -805,6 +829,7 @@
EAF7F0A6289B0CE000B287F5 /* Resetable.swift in Sources */,
EA985C2D296F03FE00F2FF2E /* TileletIconModels.swift in Sources */,
EA89200428AECF4B006B9984 /* UITextField+Publisher.swift in Sources */,
EA5F86C82A1BD99100BC83E4 /* TabModel.swift in Sources */,
EA297A5729FB0A360031ED56 /* AppleGuidlinesTouchable.swift in Sources */,
EA3361C328902D960071C351 /* Toggle.swift in Sources */,
EAF7F0A0289AB7EC00B287F5 /* View.swift in Sources */,
@ -827,6 +852,7 @@
EAB5FEF5292D371F00998C17 /* ButtonBase.swift in Sources */,
EA978EC5291D6AFE00ACC883 /* AnyLabelAttribute.swift in Sources */,
EA33622C2891E73B0071C351 /* FontProtocol.swift in Sources */,
EA596ABD2A16B4EC00300C4B /* Tab.swift in Sources */,
EAF7F11728A1475A00B287F5 /* RadioButton.swift in Sources */,
EA985BEE2968A92400F2FF2E /* TitleLockupSubTitleModel.swift in Sources */,
EA985BF22968B5BB00F2FF2E /* TitleLockupTextStyle.swift in Sources */,
@ -841,6 +867,7 @@
EA4DB30228DCBCA500103EE3 /* Badge.swift in Sources */,
EA33624728931B050071C351 /* Initable.swift in Sources */,
EAF7F0A4289B017C00B287F5 /* LabelAttributeModel.swift in Sources */,
EA5F86D02A1F936100BC83E4 /* TabsContainer.swift in Sources */,
EAF7F0B1289B177F00B287F5 /* ColorLabelAttribute.swift in Sources */,
EAC9258F2911C9DE00091998 /* EntryField.swift in Sources */,
EAB1D2EA28AE84AA00DAE764 /* UIControlPublisher.swift in Sources */,
@ -893,6 +920,7 @@
EA3361A8288B23300071C351 /* UIColor.swift in Sources */,
EAC9257D29119B5400091998 /* TextLink.swift in Sources */,
EA1F266628B945070033E859 /* RadioSwatchGroup.swift in Sources */,
EA596ABF2A16B4F500300C4B /* Tabs.swift in Sources */,
EA985BEC2968A91200F2FF2E /* TitleLockupTitleModel.swift in Sources */,
5FC35BE328D51405004EBEAC /* Button.swift in Sources */,
);
@ -1042,7 +1070,7 @@
buildSettings = {
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 18;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = "";
DYLIB_COMPATIBILITY_VERSION = 1;
@ -1075,7 +1103,7 @@
buildSettings = {
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 18;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = "";
DYLIB_COMPATIBILITY_VERSION = 1;

View File

@ -81,7 +81,7 @@ public class ControlColorConfiguration: KeyColorConfigurable {
public typealias KeyType = UIControl.State
public typealias ObjectType = Surfaceable & UIControl
public var keyColors: [KeyColorConfiguration<KeyType>] = []
private var lastKeyColor: KeyColorConfiguration<KeyType>?
public required init() { }
public func setSurfaceColors(_ lightColor: UIColor, _ darkColor: UIColor, forState state: KeyType) {
@ -94,16 +94,23 @@ public class ControlColorConfiguration: KeyColorConfigurable {
// find the exact match
if let keyColor = keyColors.first(where: {$0.key == state }) {
lastKeyColor = keyColor
return keyColor.surfaceConfig.getColor(surface)
} else if state.contains(.disabled), let keyColor = keyColors.first(where: {$0.key == .disabled }) {
lastKeyColor = keyColor
return keyColor.surfaceConfig.getColor(surface)
} else if state.contains(.highlighted), let keyColor = keyColors.first(where: {$0.key == .highlighted }) {
lastKeyColor = keyColor
return keyColor.surfaceConfig.getColor(surface)
} else {
return .clear
if let lastKeyColor {
return lastKeyColor.surfaceConfig.getColor(surface)
} else {
return .clear
}
}
}
}

View File

@ -54,10 +54,13 @@ open class Badge: View {
super.setup()
accessibilityElements = [label]
layer.cornerRadius = VDSFormControls.borderradius
layer.cornerRadius = 2
addSubview(label)
label.pinToSuperView(.init(top: 2, left: 4, bottom: 2, right: 4))
label.pinToSuperView(.init(top: 2,
left: VDSLayout.Spacing.space1X.value,
bottom: 2,
right: VDSLayout.Spacing.space1X.value))
maxWidthConstraint = label.widthAnchor.constraint(lessThanOrEqualToConstant: 100)
minWidthConstraint = label.widthAnchor.constraint(greaterThanOrEqualToConstant: 23)

View File

@ -0,0 +1,174 @@
//
// Tab.swift
// VDS
//
// Created by Matt Bruce on 5/18/23.
//
import Foundation
import VDSColorTokens
import Combine
extension Tabs {
@objc(VDSTab)
open class Tab: View {
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
///position of the tab
open var index: Int = 0
///label to write out the text
open var label: Label = Label()
///orientation of the tabs
open var orientation: Tabs.Orientation = .horizontal { didSet { setNeedsUpdate() } }
///Size for tab
open var size: Tabs.Size = .medium { didSet { setNeedsUpdate() } }
///Text position left or center
open var textPosition: TextPosition = .left { didSet { setNeedsUpdate() } }
///Sets the Position of the Selected/Hover Border Accent for All Tabs.
open var indicatorPosition: Tabs.IndicatorPosition = .bottom { didSet { setNeedsUpdate() } }
///An optional callback that is called when this Tab is clicked. Passes parameters (tabIndex).
open var onClick: ((Int) -> Void)? { didSet { setNeedsUpdate() } }
///If provided, it will set fixed width for this Tab.
open var width: CGFloat? { didSet { setNeedsUpdate() } }
///If provided, it will set this Tab to the Active Tab on render.
open var selected: Bool = false { didSet { setNeedsUpdate() } }
///The text label of the tab.
open var text: String = "Tab" { didSet { setNeedsUpdate() } }
///Minimum width for the tab
open var minWidth: CGFloat = 44.0 { didSet { setNeedsUpdate() } }
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
private var labelWidthConstraint: NSLayoutConstraint?
private var labelLeadingConstraint: NSLayoutConstraint?
private var labelTopConstraint: NSLayoutConstraint?
private var labelBottomConstraint: NSLayoutConstraint?
//--------------------------------------------------
// MARK: - Configuration
//--------------------------------------------------
private var textColorConfiguration: SurfaceColorConfiguration { selected ? textColorSelectedConfiguration : textColorNonSelectedConfiguration }
private var textColorNonSelectedConfiguration = SurfaceColorConfiguration(VDSColor.elementsSecondaryOnlight , VDSColor.elementsSecondaryOnlight)
private var textColorSelectedConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark)
private var indicatorColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteRed, VDSColor.elementsPrimaryOndark)
private var indicatorWidth: CGFloat = 4.0
//--------------------------------------------------
// MARK: - Initializers
//--------------------------------------------------
public override init(frame: CGRect) {
super.init(frame: frame)
}
public required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
required public convenience init() {
self.init(frame: .zero)
}
//--------------------------------------------------
// MARK: - Overrides
//--------------------------------------------------
open override func setup() {
super.setup()
addSubview(label)
backgroundColor = .clear
accessibilityTraits = .button
label.backgroundColor = .clear
label.translatesAutoresizingMaskIntoConstraints = false
label.pinTrailing()
labelTopConstraint = label.topAnchor.constraint(equalTo: topAnchor)
labelTopConstraint?.isActive = true
labelBottomConstraint = label.bottomAnchor.constraint(equalTo: bottomAnchor)
labelBottomConstraint?.isActive = true
labelLeadingConstraint = label.leadingAnchor.constraint(equalTo: leadingAnchor)
labelLeadingConstraint?.isActive = true
let layoutGuide = UILayoutGuide()
addLayoutGuide(layoutGuide)
labelWidthConstraint = layoutGuide.widthAnchor.constraint(greaterThanOrEqualToConstant: minWidth)
labelWidthConstraint?.isActive = true
//activate the constraints
NSLayoutConstraint.activate([layoutGuide.topAnchor.constraint(equalTo: topAnchor),
layoutGuide.bottomAnchor.constraint(equalTo: bottomAnchor),
layoutGuide.leadingAnchor.constraint(equalTo: leadingAnchor),
layoutGuide.trailingAnchor.constraint(equalTo: trailingAnchor)])
publisher(for: UITapGestureRecognizer())
.sink { [weak self] _ in
guard let self else { return }
self.onClick?(self.index)
}.store(in: &subscribers)
}
open override func updateView() {
label.text = text
label.textPosition = textPosition
label.textStyle = size.textStyle
label.textColor = textColorConfiguration.getColor(self)
labelWidthConstraint?.isActive = false
if let width, orientation == .vertical {
labelWidthConstraint = label.widthAnchor.constraint(equalToConstant: width)
} else {
labelWidthConstraint = label.widthAnchor.constraint(greaterThanOrEqualToConstant: minWidth)
}
labelWidthConstraint?.isActive = true
var leadingSpace: CGFloat
if orientation == .horizontal {
leadingSpace = 0
} else {
leadingSpace = size == .medium ? VDSLayout.Spacing.space4X.value : VDSLayout.Spacing.space6X.value
}
labelLeadingConstraint?.constant = leadingSpace
var otherSpace: CGFloat
if orientation == .horizontal {
otherSpace = size == .medium ? VDSLayout.Spacing.space3X.value : VDSLayout.Spacing.space4X.value
} else {
otherSpace = VDSLayout.Spacing.space2X.value
}
labelTopConstraint?.constant = otherSpace
labelBottomConstraint?.constant = -otherSpace
setNeedsLayout()
}
open override func layoutSubviews() {
super.layoutSubviews()
removeBorders()
if selected {
var indicator: UIRectEdge = .left
if orientation == .horizontal {
indicator = indicatorPosition.value
}
addBorder(side: indicator, width: indicatorWidth, color: indicatorColorConfiguration.getColor(self), offset: 1)
}
}
}
}

View File

@ -0,0 +1,28 @@
//
// TabModel.swift
// VDS
//
// Created by Matt Bruce on 5/22/23.
//
import Foundation
extension Tabs {
public struct TabModel {
///Text that goes in the Tab
public var text: String
///Click event when you click on a tab
public var onClick: ((Int) -> Void)?
///Width of the tab
public var width: CGFloat?
public init(text: String, onClick: ((Int) -> Void)? = nil, width: CGFloat? = nil) {
self.text = text
self.onClick = onClick
self.width = width
}
}
}

View File

@ -0,0 +1,276 @@
//
// Tabs.swift
// VDS
//
// Created by Matt Bruce on 5/18/23.
//
import Foundation
import UIKit
import VDSColorTokens
@objc(VDSTabs)
open class Tabs: View {
//--------------------------------------------------
// MARK: - Enums
//--------------------------------------------------
public enum Orientation: String, CaseIterable{
case vertical
case horizontal
}
public enum IndicatorPosition: String, CaseIterable {
case top
case bottom
var value: UIRectEdge {
if self == .top {
return .top
} else {
return .bottom
}
}
}
public enum Overflow: String, CaseIterable {
case scroll
case none
}
public enum Size: String, CaseIterable {
case medium
case large
public var textStyle: TextStyle {
if self == .medium {
return .boldBodyLarge
} else {
//specs show that the font size shouldn't change however boldTitleSmall does
//change point size between iPad/iPhone. This is a "fix" so each device will
//load the correct pointSize
return UIDevice.isIPad ? .boldTitleSmall : .boldTitleMedium
}
}
}
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
///An
/// callback that is called when the selectedIndex changes. Passes parameters (event, tabIndex).
open var onTabChange: ((Int) -> Void)?
//Determines the layout of the Tabs, defaults to horizontal
open var orientation: Orientation = .horizontal { didSet { if oldValue != orientation { updateTabItems() } } }
///When true, Tabs will have border line. If false is passed then the border line won't be visible.
open var borderLine: Bool = true { didSet { setNeedsUpdate() } }
///It will fill the Tabs to the width of the compoent and all Tabs will be in equal width when orientation is horizontal. This is recommended when there are no more than 2-3 tabs.
open var fillContainer: Bool = false { didSet { updateTabItems() } }
///When true, Tabs will be sticky to top of page, when orientation is vertical.
open var indicatorFillTab: Bool = false { didSet { setNeedsUpdate() } }
///Sets the Position of the Selected/Hover Border Accent for All Tabs, only for Horizontal Orientation
open var indicatorPosition: IndicatorPosition = .bottom { didSet { setNeedsUpdate() } }
///Minimum Width for All Tabs, when orientation is horizontal.
open var minWidth: CGFloat = 44.0 { didSet { setNeedsUpdate() } }
///If set to 'scroll', Tabs can be overflow and scrollable. With 'none', tabs will not overflow and labels will be wrapped to multiple lines if the label text is long.
open var overflow: Overflow = .scroll { didSet { updateTabItems() } }
///The initial Selected Tab's index and is set once a Tab is clicked
open var selectedIndex: Int = 0 { didSet { setNeedsUpdate() } }
///Determines the size of the Tabs TextStyle
open var size: Size = .medium { didSet { updateTabItems() } }
///When true, Tabs will be sticky to top of page, when orientation is vertical.
open var sticky: Bool = false { didSet { setNeedsUpdate() } }
///Model of the Tabs you are wanting to show.
open var tabModels: [TabModel] = [] { didSet { updateTabItems() } }
open var tabSpacing: CGFloat { tabStackView.spacing }
open var tabViews: [Tab] = []
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
private var tabStackView: UIStackView!
private var scrollView: UIScrollView!
private var contentView: View!
private var borderlineColorConfig = SurfaceColorConfiguration(VDSColor.elementsLowcontrastOnlight, VDSColor.elementsLowcontrastOndark)
private var contentViewWidthConstraint: NSLayoutConstraint?
//--------------------------------------------------
// MARK: - Initializers
//--------------------------------------------------
public override init(frame: CGRect) {
super.init(frame: frame)
}
public convenience required init() {
self.init(frame: .zero)
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
}
//--------------------------------------------------
// MARK: - Overrides
//--------------------------------------------------
open override func setup() {
super.setup()
scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.showsHorizontalScrollIndicator = false
scrollView.showsVerticalScrollIndicator = false
addSubview(scrollView)
contentView = View()
contentView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(contentView)
tabStackView = UIStackView()
tabStackView.axis = .horizontal
tabStackView.distribution = .fill
tabStackView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(tabStackView)
scrollView.pinToSuperView()
contentView.pinToSuperView()
tabStackView.pinToSuperView()
contentView.heightAnchor.constraint(equalTo: scrollView.heightAnchor).isActive = true
}
private func updateTabItems() {
updateTabItems(with: tabModels)
}
private func updateTabItems(with models: [TabModel]) {
// Clear existing tab items
for tabItem in tabViews {
tabItem.removeFromSuperview()
}
tabViews.removeAll()
// Create new tab items from the models
for model in models {
let tabItem = Tab()
tabItem.text = model.text
tabItem.onClick = model.onClick
tabItem.width = model.width
tabViews.append(tabItem)
tabStackView.addArrangedSubview(tabItem)
tabItem
.publisher(for: UITapGestureRecognizer())
.sink { [weak self] gesture in
guard let self, let tabItem = gesture.view as? Tab else { return }
if let selectedIndex = tabViews.firstIndex(of: tabItem) {
self.selectedIndex = selectedIndex
self.onTabChange?(selectedIndex)
}
}.store(in: &tabItem.subscribers)
}
setNeedsUpdate()
scrollToSelectedIndex(animated: false)
}
private func scrollToSelectedIndex(animated: Bool) {
if orientation == .horizontal && self.overflow == .scroll, selectedIndex < tabViews.count {
let selectedTab = tabViews[selectedIndex]
scrollView.scrollRectToVisible(selectedTab.frame, animated: animated)
}
}
open override func updateView() {
super.updateView()
// Update the stackview properties
if orientation == .horizontal && fillContainer {
tabStackView.distribution = .fillEqually
} else {
tabStackView.distribution = orientation == .horizontal ? .fillProportionally : .fill
}
tabStackView.axis = orientation == .horizontal ? .horizontal : .vertical
tabStackView.alignment = orientation == .horizontal ? .fill : .leading
tabStackView.spacing = orientation == .horizontal ? VDSLayout.Spacing.space6X.value : VDSLayout.Spacing.space4X.value
// Update tab appearance based on properties
for (index, tabItem) in tabViews.enumerated() {
tabItem.selected = selectedIndex == index
tabItem.index = index
tabItem.minWidth = minWidth
tabItem.size = size
tabItem.textPosition = orientation == .horizontal && fillContainer ? .center : .left
tabItem.orientation = orientation
tabItem.surface = surface
tabItem.indicatorPosition = indicatorPosition
tabItem.accessibilityLabel = "\(tabItem.text) \(tabItem.selected ? "selected" : "unselected") \(index+1) of \(tabViews.count)"
}
// Deactivate old constraint
contentViewWidthConstraint?.isActive = false
// Apply width
if orientation == .vertical {
scrollView.isScrollEnabled = false
contentViewWidthConstraint = contentView.widthAnchor.constraint(equalTo: widthAnchor)
} else {
// Apply overflow
if overflow == .scroll && !fillContainer {
let contentWidth = tabStackView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).width
contentViewWidthConstraint = contentView.widthAnchor.constraint(equalToConstant: contentWidth)
// Enable scrolling if necessary
scrollView.contentSize = CGSize(width: contentWidth, height: scrollView.bounds.height)
} else {
contentViewWidthConstraint = contentView.widthAnchor.constraint(equalTo: widthAnchor)
}
}
scrollView.isScrollEnabled = orientation == .horizontal && overflow == .scroll
// Activate old constraint
contentViewWidthConstraint?.isActive = true
setNeedsLayout()
layoutIfNeeded()
}
open override func layoutSubviews() {
super.layoutSubviews()
//scrollView Contentsize
if orientation == .horizontal && overflow == .scroll && !fillContainer {
let contentWidth = tabStackView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).width
scrollView.contentSize = CGSize(width: contentWidth, height: scrollView.bounds.height)
} else {
scrollView.contentSize = bounds.size
}
// Apply border line
layer.remove(layerName: "borderLineLayer")
if borderLine {
let borderLineLayer = CALayer()
borderLineLayer.name = "borderLineLayer"
borderLineLayer.backgroundColor = borderlineColorConfig.getColor(self).cgColor
if orientation == .horizontal {
borderLineLayer.frame = CGRect(x: 0, y: bounds.height - 1, width: bounds.width, height: 1)
} else {
borderLineLayer.frame = CGRect(x: 0, y: 0, width: 1, height: bounds.height)
}
layer.addSublayer(borderLineLayer)
}
scrollToSelectedIndex(animated: true)
}
}

View File

@ -0,0 +1,217 @@
//
// TabsContainer.swift
// VDS
//
// Created by Matt Bruce on 5/25/23.
//
import Foundation
import UIKit
@objc(VDSTabsContainer)
open class TabsContainer: View {
//--------------------------------------------------
// MARK: - Enums
//--------------------------------------------------
public enum Width {
case percentage(CGFloat)
case value(CGFloat)
}
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
open var tabMenu = Tabs()
///An optional callback that is called when the selectedIndex changes. Passes parameters (event, tabIndex).
open var onTabChange: ((Int) -> Void)?
///Determines the layout of the Tabs, defaults to horizontal
open var orientation: Tabs.Orientation = .horizontal { didSet { setNeedsUpdate() } }
///When true, Tabs will have border line. If false is passed then the border line won't be visible.
open var borderLine: Bool = true { didSet { setNeedsUpdate() } }
///It will fill the Tabs to the width of the compoent and all Tabs will be in equal width when orientation is horizontal. This is recommended when there are no more than 2-3 tabs.
open var fillContainer: Bool = false { didSet { setNeedsUpdate() } }
///When true, Tabs will be sticky to top of page, when orientation is vertical.
open var indicatorFillTab: Bool = false { didSet { setNeedsUpdate() } }
///Sets the Position of the Selected/Hover Border Accent for All Tabs, only for Horizontal Orientation
open var indicatorPosition: Tabs.IndicatorPosition = .bottom { didSet { setNeedsUpdate() } }
///Minimum Width for All Tabs, when orientation is horizontal.
open var minWidth: CGFloat = 44.0 { didSet { setNeedsUpdate() } }
///If set to 'scroll', Tabs can be overflow and scrollable. With 'none', tabs will not overflow and labels will be wrapped to multiple lines if the label text is long.
open var overflow: Tabs.Overflow = .scroll { didSet { setNeedsUpdate() } }
///The initial Selected Tab's index and is set once a Tab is clicked
open var selectedIndex: Int = 0 { didSet { setNeedsUpdate() } }
///Determines the size of the Tabs TextStyle
open var size: Tabs.Size = .medium { didSet { setNeedsUpdate() } }
///Space between the Tabs and Contentl.
open var space: CGFloat = 5.0 { didSet { setNeedsUpdate() } }
///When true, Tabs will be sticky to top of page, when orientation is vertical.
open var sticky: Bool = false { didSet { setNeedsUpdate() } }
///rules for width
private var _width: Width = .percentage(0.25)
///Width of all Tabs when orientation is vertical, defaults to 25%.
open var width: Width {
get {
return _width
}
set {
switch newValue {
case .percentage(let percentage):
if percentage >= 0 && percentage <= 1 {
_width = newValue
setNeedsUpdate()
} else {
print("Invalid percentage value. It should be between 0 and 1.")
}
case .value(let value):
if value >= minWidth {
_width = newValue
setNeedsUpdate()
} else {
print("Invalid value. It should be greater than or equal to \(minWidth).")
}
}
}
}
///Model of the Tabs you are wanting to show.
open var tabModels: [TabModel] = [] {
didSet {
tabMenu.tabModels = tabModels.compactMap{ $0.model }
contentView.arrangedSubviews.forEach{ $0.removeFromSuperview() }
tabModels.forEach {
let view = $0.view
view.isHidden = true
contentView.addArrangedSubview(view)
}
setNeedsUpdate()
}
}
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
private var contentViewWidthConstraint: NSLayoutConstraint?
private var stackView = UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.axis = .vertical
$0.alignment = .fill
$0.distribution = .fill
}
private var contentView = UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.alignment = .fill
$0.distribution = .fillProportionally
$0.axis = .vertical
$0.spacing = 10
}
private var tabMenuLayoutGuide = UILayoutGuide()
open override func setup() {
super.setup()
tabMenu.addLayoutGuide(tabMenuLayoutGuide)
addSubview(stackView)
stackView.pinToSuperView()
stackView.addArrangedSubview(tabMenu)
stackView.addArrangedSubview(contentView)
NSLayoutConstraint.activate([
tabMenuLayoutGuide.topAnchor.constraint(equalTo: topAnchor),
tabMenuLayoutGuide.bottomAnchor.constraint(equalTo: bottomAnchor),
tabMenuLayoutGuide.leadingAnchor.constraint(equalTo: leadingAnchor),
tabMenuLayoutGuide.trailingAnchor.constraint(equalTo: trailingAnchor)
])
}
open override func updateView() {
super.updateView()
stackView.alignment = orientation == .horizontal ? .fill : .top
stackView.axis = orientation == .horizontal ? .vertical : .horizontal
stackView.spacing = space
tabMenu.onTabChange = { [weak self] index in
guard let self else { return }
self.tabClicked(index: index)
}
//update tabs width constraints
contentViewWidthConstraint?.isActive = false
if orientation == .vertical {
switch width {
case .percentage(let amount):
contentViewWidthConstraint = tabMenu.widthAnchor.constraint(equalTo: tabMenuLayoutGuide.widthAnchor, multiplier: amount)
case .value(let amount):
contentViewWidthConstraint = tabMenu.widthAnchor.constraint(equalToConstant: amount)
}
} else {
contentViewWidthConstraint = tabMenu.widthAnchor.constraint(equalTo: widthAnchor)
}
contentViewWidthConstraint?.isActive = true
tabMenu.surface = surface
tabMenu.disabled = disabled
tabMenu.orientation = orientation
tabMenu.borderLine = borderLine
tabMenu.fillContainer = fillContainer
tabMenu.indicatorFillTab = indicatorFillTab
tabMenu.indicatorPosition = indicatorPosition
tabMenu.minWidth = minWidth
tabMenu.overflow = overflow
tabMenu.selectedIndex = selectedIndex
tabMenu.size = size
tabMenu.sticky = sticky
setSelected(index: selectedIndex)
tabModels.forEach {
var view = $0.view
view.surface = surface
}
}
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------
private func tabClicked(index: Int) {
onTabChange?(index)
setSelected(index: index)
}
private func setSelected(index: Int) {
for (modelIndex, model) in tabModels.enumerated() {
let view = model.view
let shouldShow = index == modelIndex
view.isHidden = !shouldShow
}
}
}
extension TabsContainer {
public struct TabModel {
public typealias AnySurfaceableView = UIView & Surfaceable
public var model: Tabs.TabModel
public var view: AnySurfaceableView
public init(model: Tabs.TabModel, view: AnySurfaceableView) {
self.model = model
self.view = view
}
}
}

View File

@ -47,7 +47,8 @@ open class TooltipAlertViewController: UIViewController, Surfaceable {
open var surface: Surface = .light { didSet { updateView() }}
open var titleText: String = "" { didSet { updateView() }}
open var titleLabel = Label().with { label in
label.textStyle = .boldTitleMedium
//use the same font/pointsize for both Title upsizes font in iPad
label.textStyle = UIDevice.isIPad ? .boldTitleSmall : .boldTitleMedium
}
open var contentText: String = "" { didSet { updateView() }}

View File

@ -176,10 +176,66 @@ extension UIView {
extension CALayer {
func remove(layerName: String) {
sublayers?.forEach({ layer in
guard let sublayers = sublayers else {
return
}
sublayers.forEach({ layer in
if layer.name?.hasPrefix(layerName) ?? false {
layer.removeFromSuperlayer()
}
})
}
}
extension UIView {
public func addBorder(side: UIRectEdge, width: CGFloat, color: UIColor, offset: CGFloat = 0) {
let layerName = borderLayerName(for: side)
layer.remove(layerName: layerName)
let borderLayer = CALayer()
borderLayer.backgroundColor = color.cgColor
borderLayer.name = layerName
switch side {
case .left:
borderLayer.frame = CGRect(x: 0, y: 0, width: width, height: frame.height)
case .right:
borderLayer.frame = CGRect(x: frame.width - width - offset, y: 0, width: width, height: frame.height)
case .top:
borderLayer.frame = CGRect(x: 0, y: 0, width: frame.width, height: width)
case .bottom:
borderLayer.frame = CGRect(x: 0, y: frame.height - width - offset, width: frame.width, height: width)
default:
break
}
layer.addSublayer(borderLayer)
}
public func removeBorders() {
layer.borderWidth = 0
layer.borderColor = nil
layer.remove(layerName: borderLayerName(for: .top))
layer.remove(layerName: borderLayerName(for: .left))
layer.remove(layerName: borderLayerName(for: .right))
layer.remove(layerName: borderLayerName(for: .bottom))
}
private func borderLayerName(for side: UIRectEdge) -> String {
switch side {
case .left:
return "leftBorderLayer"
case .right:
return "rightBorderLayer"
case .top:
return "topBorderLayer"
case .bottom:
return "bottomBorderLayer"
default:
return ""
}
}
}

View File

@ -0,0 +1,14 @@
1.0.18
=======
- CXTDT-412383 - Badge Corner Radius / Color issue (resolved in previous build)
- Completed Tabs
- Started ButtonIcon
1.0.17
=======
- Color Tokens Update
- Changes to Badge Colors
- updated trailing tooltip label
- updated entry field colors, tooltip
- update toggle size
- added touchable protocol to button

View File

@ -329,3 +329,9 @@ extension TextStyle {
}
}
extension TextStyle: CustomDebugStringConvertible {
public var debugDescription: String {
"Name: \(self.rawValue) FontFace: \(font.fontName) FontWeight: \(self.rawValue.hasPrefix("bold") ? "bold" : "normal") PointSize: \(font.pointSize) LetterSpacing: \(letterSpacing) LineHeight: \(lineHeight)"
}
}