initial work

Signed-off-by: Matt Bruce <matt.bruce@verizon.com>
This commit is contained in:
Matt Bruce 2024-10-09 08:20:13 -05:00
parent 1728592bff
commit b974e63931
3 changed files with 429 additions and 0 deletions

View File

@ -210,6 +210,8 @@
EAF7F11728A1475A00B287F5 /* RadioButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F11528A1475A00B287F5 /* RadioButtonItem.swift */; }; EAF7F11728A1475A00B287F5 /* RadioButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F11528A1475A00B287F5 /* RadioButtonItem.swift */; };
EAF7F13328A2A16500B287F5 /* AttachmentLabelAttributeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F13228A2A16500B287F5 /* AttachmentLabelAttributeModel.swift */; }; EAF7F13328A2A16500B287F5 /* AttachmentLabelAttributeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F13228A2A16500B287F5 /* AttachmentLabelAttributeModel.swift */; };
EAF978212A99035B00C2FEA9 /* Enabling.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF978202A99035B00C2FEA9 /* Enabling.swift */; }; EAF978212A99035B00C2FEA9 /* Enabling.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF978202A99035B00C2FEA9 /* Enabling.swift */; };
EAFD5AA02CB5CA5300C87DE1 /* TileletGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAFD5A9F2CB5CA5300C87DE1 /* TileletGroup.swift */; };
EAFD5AA22CB5CA7900C87DE1 /* TileletGroupPositionLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAFD5AA12CB5CA7900C87DE1 /* TileletGroupPositionLayout.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -459,6 +461,8 @@
EAF7F11528A1475A00B287F5 /* RadioButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadioButtonItem.swift; sourceTree = "<group>"; }; EAF7F11528A1475A00B287F5 /* RadioButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadioButtonItem.swift; sourceTree = "<group>"; };
EAF7F13228A2A16500B287F5 /* AttachmentLabelAttributeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentLabelAttributeModel.swift; sourceTree = "<group>"; }; EAF7F13228A2A16500B287F5 /* AttachmentLabelAttributeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentLabelAttributeModel.swift; sourceTree = "<group>"; };
EAF978202A99035B00C2FEA9 /* Enabling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Enabling.swift; sourceTree = "<group>"; }; EAF978202A99035B00C2FEA9 /* Enabling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Enabling.swift; sourceTree = "<group>"; };
EAFD5A9F2CB5CA5300C87DE1 /* TileletGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileletGroup.swift; sourceTree = "<group>"; };
EAFD5AA12CB5CA7900C87DE1 /* TileletGroupPositionLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileletGroupPositionLayout.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -957,6 +961,7 @@
EA5E3056295105930082B959 /* Tilelet */ = { EA5E3056295105930082B959 /* Tilelet */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
EAFD5A9E2CB5C9ED00C87DE1 /* TileletGroup */,
EA5E3057295105A40082B959 /* Tilelet.swift */, EA5E3057295105A40082B959 /* Tilelet.swift */,
EA985BE529688F6A00F2FF2E /* TileletBadgeModel.swift */, EA985BE529688F6A00F2FF2E /* TileletBadgeModel.swift */,
71ACE89D2BA1CC1700FB6ADC /* TiletEyebrowModel.swift */, 71ACE89D2BA1CC1700FB6ADC /* TiletEyebrowModel.swift */,
@ -1171,6 +1176,15 @@
path = RadioButton; path = RadioButton;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
EAFD5A9E2CB5C9ED00C87DE1 /* TileletGroup */ = {
isa = PBXGroup;
children = (
EAFD5A9F2CB5CA5300C87DE1 /* TileletGroup.swift */,
EAFD5AA12CB5CA7900C87DE1 /* TileletGroupPositionLayout.swift */,
);
path = TileletGroup;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */ /* Begin PBXHeadersBuildPhase section */
@ -1382,6 +1396,7 @@
EAB1D2CD28ABE76100DAE764 /* Withable.swift in Sources */, EAB1D2CD28ABE76100DAE764 /* Withable.swift in Sources */,
71FC86DC2B96F4C800700965 /* PaginationCellItem.swift in Sources */, 71FC86DC2B96F4C800700965 /* PaginationCellItem.swift in Sources */,
EAC846F3294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift in Sources */, EAC846F3294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift in Sources */,
EAFD5AA02CB5CA5300C87DE1 /* TileletGroup.swift in Sources */,
EAF7F0952899861000B287F5 /* CheckboxItem.swift in Sources */, EAF7F0952899861000B287F5 /* CheckboxItem.swift in Sources */,
EA985BE82968951C00F2FF2E /* TileletTitleModel.swift in Sources */, EA985BE82968951C00F2FF2E /* TileletTitleModel.swift in Sources */,
71FC86DE2B9738B900700965 /* SurfaceConfigurationValue.swift in Sources */, 71FC86DE2B9738B900700965 /* SurfaceConfigurationValue.swift in Sources */,
@ -1460,6 +1475,7 @@
EA336171288B19200071C351 /* VDS.docc in Sources */, EA336171288B19200071C351 /* VDS.docc in Sources */,
EA985BF02968A93600F2FF2E /* TitleLockupEyebrowModel.swift in Sources */, EA985BF02968A93600F2FF2E /* TitleLockupEyebrowModel.swift in Sources */,
EA5E30532950DDA60082B959 /* TitleLockup.swift in Sources */, EA5E30532950DDA60082B959 /* TitleLockup.swift in Sources */,
EAFD5AA22CB5CA7900C87DE1 /* TileletGroupPositionLayout.swift in Sources */,
EAD062B02A3B873E0015965D /* BadgeIndicator.swift in Sources */, EAD062B02A3B873E0015965D /* BadgeIndicator.swift in Sources */,
183B16F32C78CF7C00BA6A10 /* CarouselSlotCell.swift in Sources */, 183B16F32C78CF7C00BA6A10 /* CarouselSlotCell.swift in Sources */,
44A952DD2BE3DA820009F874 /* TableFlowLayout.swift in Sources */, 44A952DD2BE3DA820009F874 /* TableFlowLayout.swift in Sources */,

View File

@ -0,0 +1,165 @@
//
// TileletGroup.swift
// VDS
//
// Created by Matt Bruce on 10/8/24.
//
import Foundation
import UIKit
import VDSCoreTokens
import Combine
open class TileletGroup: View {
//--------------------------------------------------
// MARK: - Initializers
//--------------------------------------------------
required public init() {
super.init(frame: .zero)
}
public override init(frame: CGRect) {
super.init(frame: .zero)
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
}
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
/// An object containing number of Button components per row for iPhones
open var rowQuantityPhone: Int = 0 { didSet { setNeedsUpdate() } }
/// An object containing number of Button components per row for iPads
open var rowQuantityTablet: Int = 0 { didSet { setNeedsUpdate() } }
/// An object containing number of Button components per row
open var rowQuantity: Int { UIDevice.isIPad ? rowQuantityTablet : rowQuantityPhone }
/// Array of Buttonable Views that are shown in the group.
open var tilelets: [Tilelet] = [] { didSet { setNeedsUpdate() } }
/// Whether this object is enabled or not
override open var isEnabled: Bool {
didSet {
tilelets.forEach { $0.isEnabled = isEnabled }
}
}
/// Current Surface and this is used to pass down to child objects that implement Surfacable
override open var surface: Surface {
didSet {
tilelets.forEach { $0.surface = surface }
}
}
open var contentSizePublisher: AnyPublisher<CGSize, Never> { collectionView.contentSizePublisher }
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
fileprivate lazy var positionLayout = TileletGroupPositionLayout().with {
$0.delegate = self
}
/// CollectionView that renders the array of buttonBase obects.
fileprivate lazy var collectionView: SelfSizingCollectionView = {
return SelfSizingCollectionView(frame: .zero, collectionViewLayout: positionLayout).with {
$0.backgroundColor = .clear
$0.showsHorizontalScrollIndicator = false
$0.showsVerticalScrollIndicator = false
$0.isScrollEnabled = false
$0.translatesAutoresizingMaskIntoConstraints = false
$0.dataSource = self
$0.delegate = self
$0.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "collectionViewCell")
}
}()
//--------------------------------------------------
// MARK: - Public Methods
//--------------------------------------------------
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
open override func setup() {
super.setup()
addSubview(collectionView)
collectionView.pinToSuperView()
}
//--------------------------------------------------
// MARK: - Overrides
//--------------------------------------------------
/// Used to make changes to the View based off a change events or from local properties.
open override func updateView() {
super.updateView()
positionLayout.rowQuantity = rowQuantity
var width: CGFloat?
tilelets.forEach { tilelet in
if let width {
tilelet.width = width
}
}
collectionView.reloadData()
}
open override func setDefaults() {
super.setDefaults()
rowQuantityPhone = 0
rowQuantityTablet = 0
tilelets = []
}
open override func reset() {
tilelets.forEach { $0.reset() }
super.reset()
}
open override func layoutSubviews() {
super.layoutSubviews()
// Accounts for any collection size changes
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.collectionView.collectionViewLayout.invalidateLayout()
}
}
}
extension TileletGroup: UICollectionViewDataSource, UICollectionViewDelegate {
public func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return tilelets.count
}
open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let tilelet = tilelets[indexPath.row]
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionViewCell", for: indexPath)
cell.contentView.subviews.forEach { $0.removeFromSuperview() }
cell.contentView.addSubview(tilelet)
tilelet.pinToSuperView()
if hasDebugBorder {
cell.addDebugBorder()
} else {
cell.removeDebugBorder()
}
return cell
}
public func collectionView(_ collectionView: UICollectionView, sizeForItemAtIndexPath indexPath: IndexPath) -> CGSize {
tilelets[indexPath.row].intrinsicContentSize
}
}
extension TileletGroup : TileletGroupPositionLayoutDelegate {
func collectionView(_ collectionView: UICollectionView, tileletAtIndexPath indexPath: IndexPath) -> Tilelet {
tilelets[indexPath.row]
}
}

View File

@ -0,0 +1,248 @@
//
// TileletGroupPositionLayout.swift
// VDS
//
// Created by Matt Bruce on 10/8/24.
//
import Foundation
import UIKit
class TileletCollectionViewRow {
var attributes = [TileletLayoutAttributes]()
init() {}
func add(attribute: TileletLayoutAttributes) {
attributes.append(attribute)
}
var rowWidth: CGFloat {
return attributes.reduce(0, { result, attribute -> CGFloat in
return result + attribute.frame.width + attribute.spacing
})
}
var rowHeight: CGFloat {
attributes.compactMap{$0.frame.height}.max() ?? 0
}
var maxHeightIndexPath: IndexPath {
let maxHeight = rowHeight
return attributes.first(where: {$0.frame.height == maxHeight})!.indexPath
}
var rowY: CGFloat = 0 {
didSet {
for attribute in attributes {
attribute.frame.origin.y = rowY
}
}
}
func layout(with collectionViewWidth: CGFloat){
var offset = 0.0
let height = rowHeight
attributes.last?.spacing = 0
for attribute in attributes {
attribute.frame.origin.x = offset
if attribute.frame.height < height {
//recalibrate the y to vertically center align rect
attribute.frame.origin.y += (height - attribute.frame.size.height) / 2
}
offset += attribute.frame.width + attribute.spacing
}
}
}
protocol TileletGroupPositionLayoutDelegate: AnyObject {
func collectionView(_ collectionView: UICollectionView, sizeForItemAtIndexPath indexPath: IndexPath) -> CGSize
func collectionView(_ collectionView: UICollectionView, tileletAtIndexPath indexPath: IndexPath) -> Tilelet
}
class TileletLayoutAttributes: UICollectionViewLayoutAttributes{
var spacing: CGFloat = 0
var tilelet: Tilelet?
convenience init(spacing: CGFloat,
tilelet: Tilelet,
forCellWith indexPath: IndexPath) {
self.init(forCellWith: indexPath)
self.spacing = spacing
self.tilelet = tilelet
}
}
class TileletGroupPositionLayout: UICollectionViewLayout {
weak var delegate: TileletGroupPositionLayoutDelegate?
var verticalSpacer: ((TileletCollectionViewRow, TileletCollectionViewRow?) -> CGFloat)?
var axisSpacer: ((NSLayoutConstraint.Axis, Tilelet, Tilelet) -> CGFloat)?
// Total height of the content. Will be used to configure the scrollview content
var layoutHeight: CGFloat = 0.0
var rowQuantity: Int = 0
var tileletPercentage: CGFloat?
private var itemCache: [TileletLayoutAttributes] = []
override func prepare() {
super.prepare()
itemCache.removeAll()
layoutHeight = 0.0
guard let collectionView, let delegate else { return }
// Variable to track the width of the layout at the current state when the item is being drawn
var layoutWidthIterator: CGFloat = 0.0
// Only 1 section in the TileletGroup
let section = 0
// Variables to track individual item width and cumultative height of all items as they are being laid out.
var itemSize: CGSize = .zero
// get number of tilelets
let totalItems = collectionView.numberOfItems(inSection: section)
//create rows
var rows = [TileletCollectionViewRow]()
rows.append(TileletCollectionViewRow())
let collectionViewWidth = collectionView.horizontalPinnedWidth() ?? collectionView.frame.width
for item in 0..<totalItems {
// start out with no spacing after the item
var itemSpacing = 0.0
// create the indexPath
let indexPath = IndexPath(item: item, section: section)
// get the rect size of the tilelet
itemSize = delegate.collectionView(collectionView, sizeForItemAtIndexPath: indexPath)
// ensure the width is not greater than the collectionViewWidth
itemSize.width = min(itemSize.width, collectionViewWidth)
// determine if the current tilelet will fit in the row
let rowItemCount = rows.last?.attributes.count ?? 0
if (layoutWidthIterator + itemSize.width) > collectionViewWidth && rowQuantity == 0
|| (rowQuantity > 0 && rowItemCount == rowQuantity) {
// 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
// set the spacing of the last item of the current row to 0
rows.last?.attributes.last?.spacing = 0
// add a new row
rows.append(TileletCollectionViewRow())
}
// get the tilelet
let itemTilelet = delegate.collectionView(collectionView, tileletAtIndexPath: indexPath)
// see if there is another item in the array
let nextItem = item + 1
// if so, get the tilelet
// and get the spacing based of the
// current tilelet and the next tilelet
if nextItem < totalItems {
//get the next tilelet
let neighbor = delegate.collectionView(collectionView, tileletAtIndexPath: IndexPath(item: nextItem, section: section))
// get the spacing to go between the current and next tilelet
itemSpacing = getAxisSpacing(for: .horizontal, with: itemTilelet, neighboring: neighbor)
}
// create the custom layout attribute
let attributes = TileletLayoutAttributes(spacing: itemSpacing, tilelet: itemTilelet, forCellWith: indexPath)
attributes.frame = CGRect(x: 0, y: 0, width: min(itemSize.width, collectionViewWidth), height: itemSize.height)
// add it to the array
rows.last?.add(attribute: attributes)
// update the current width
// add the current frame width + the found spacing
layoutWidthIterator = layoutWidthIterator + attributes.frame.width + itemSpacing
}
layoutWidthIterator = 0.0
// calculate the
layoutHeight = 0.0
// loop through the rows and set
// the row y position for each element
// also add to the layoutHeight
for item in 0..<rows.count {
let row = rows[item]
var rowSpacing = 0.0
if item > 0 {
let prevRow = rows[item - 1]
rowSpacing = getVerticalSpacing(for: prevRow, neighboringRow: row)
row.rowY = layoutHeight + rowSpacing
layoutHeight += rowSpacing
}
layoutHeight += row.rowHeight
}
// recalculate rows x based off of positions
rows.forEach {
$0.layout(with: collectionViewWidth)
}
let rowAttributes = rows.flatMap { $0.attributes }
itemCache = rowAttributes
}
override func layoutAttributesForElements(in rect: CGRect)-> [UICollectionViewLayoutAttributes]? {
var visibleLayoutAttributes: [UICollectionViewLayoutAttributes] = []
for attributes in itemCache {
if attributes.frame.intersects(rect) {
visibleLayoutAttributes.append(attributes)
}
}
return visibleLayoutAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return itemCache[indexPath.row]
}
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: layoutHeight)
}
private var contentWidth: CGFloat {
guard let collectionView = collectionView else {
return 0
}
return collectionView.bounds.width
}
private func getAxisSpacing(for axis: NSLayoutConstraint.Axis, with primary: Tilelet, neighboring: Tilelet) -> CGFloat {
20
}
private func getVerticalSpacing(for row: TileletCollectionViewRow, neighboringRow: TileletCollectionViewRow?) -> CGFloat {
20
}
}