diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 2cc99ec2..f5dc7e25 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -211,7 +211,7 @@ EAF7F13328A2A16500B287F5 /* AttachmentLabelAttributeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F13228A2A16500B287F5 /* AttachmentLabelAttributeModel.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 */; }; + EAFD5AA22CB5CA7900C87DE1 /* TileletGroupFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAFD5AA12CB5CA7900C87DE1 /* TileletGroupFlowLayout.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -462,7 +462,7 @@ EAF7F13228A2A16500B287F5 /* AttachmentLabelAttributeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentLabelAttributeModel.swift; sourceTree = ""; }; EAF978202A99035B00C2FEA9 /* Enabling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Enabling.swift; sourceTree = ""; }; EAFD5A9F2CB5CA5300C87DE1 /* TileletGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileletGroup.swift; sourceTree = ""; }; - EAFD5AA12CB5CA7900C87DE1 /* TileletGroupPositionLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileletGroupPositionLayout.swift; sourceTree = ""; }; + EAFD5AA12CB5CA7900C87DE1 /* TileletGroupFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileletGroupFlowLayout.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1180,7 +1180,7 @@ isa = PBXGroup; children = ( EAFD5A9F2CB5CA5300C87DE1 /* TileletGroup.swift */, - EAFD5AA12CB5CA7900C87DE1 /* TileletGroupPositionLayout.swift */, + EAFD5AA12CB5CA7900C87DE1 /* TileletGroupFlowLayout.swift */, ); path = TileletGroup; sourceTree = ""; @@ -1475,7 +1475,7 @@ EA336171288B19200071C351 /* VDS.docc in Sources */, EA985BF02968A93600F2FF2E /* TitleLockupEyebrowModel.swift in Sources */, EA5E30532950DDA60082B959 /* TitleLockup.swift in Sources */, - EAFD5AA22CB5CA7900C87DE1 /* TileletGroupPositionLayout.swift in Sources */, + EAFD5AA22CB5CA7900C87DE1 /* TileletGroupFlowLayout.swift in Sources */, EAD062B02A3B873E0015965D /* BadgeIndicator.swift in Sources */, 183B16F32C78CF7C00BA6A10 /* CarouselSlotCell.swift in Sources */, 44A952DD2BE3DA820009F874 /* TableFlowLayout.swift in Sources */, diff --git a/VDS/Components/Tilelet/TileletGroup/TileletGroup.swift b/VDS/Components/Tilelet/TileletGroup/TileletGroup.swift index b4fc2ab2..12165efe 100644 --- a/VDS/Components/Tilelet/TileletGroup/TileletGroup.swift +++ b/VDS/Components/Tilelet/TileletGroup/TileletGroup.swift @@ -10,156 +10,178 @@ import UIKit import VDSCoreTokens import Combine -open class TileletGroup: View { +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 { collectionView.contentSizePublisher } - //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- - fileprivate lazy var positionLayout = TileletGroupPositionLayout().with { + /// CollectionView to show the rows and columns + private lazy var matrixView = SelfSizingCollectionView(frame: .zero, collectionViewLayout: flowLayout).with { + $0.register(TileletGroupCellItem.self, forCellWithReuseIdentifier: TileletGroupCellItem.Identifier) + $0.dataSource = self $0.delegate = self + $0.translatesAutoresizingMaskIntoConstraints = false + $0.allowsSelection = false + $0.showsVerticalScrollIndicator = false + $0.showsHorizontalScrollIndicator = false + $0.isAccessibilityElement = true + $0.backgroundColor = .clear } - /// CollectionView that renders the array of buttonBase obects. - fileprivate lazy var collectionView: SelfSizingCollectionView = { + /// Custom flow layout to manage the height of the cells + private lazy var flowLayout = TileletGroupFlowLayout().with { + $0.delegate = self + $0.scrollDirection = .horizontal + } + + /// Parameter to show the all table rows + private var rows: [[Tilelet]] = [] { didSet { setNeedsUpdate() } } + //-------------------------------------------------- + // MARK: - Enums + //-------------------------------------------------- + + /// Enums used to define the padding for the cell edge spacing. + public enum Padding: String, CaseIterable { + case standard, compact - 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") + private func value() -> CGFloat { + switch self { + case .standard: + return UIDevice.isIPad ? 40.0 : VDSLayout.space3X + case .compact: + return UIDevice.isIPad ? VDSLayout.space6X : VDSLayout.space3X + } } - }() - - //-------------------------------------------------- - // 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() + + func horizontalValue() -> CGFloat { value() } + + func verticalValue() -> CGFloat { value() } } + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + /// Parameter to set the padding for the cell + open var padding: Padding = .standard { didSet { setNeedsUpdate() } } + + /// An object containing number of Button components per row for iPhones + open var rowQuantityPhone: Int = 2 { didSet { updateRows() } } + + /// An object containing number of Button components per row for iPads + open var rowQuantityTablet: Int = 4 { didSet { updateRows() } } + + /// An object containing number of Button components per row + open var rowQuantity: Int { UIDevice.isIPad ? rowQuantityTablet : rowQuantityPhone } + + open var tilelets: [Tilelet] = [] { didSet { updateRows() } } + //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- - /// Used to make changes to the View based off a change events or from local properties. + + ///Called upon initializing the table view + open override func setup() { + super.setup() + addSubview(matrixView) + matrixView.pinToSuperView() + } + + /// Will update the table view, when called becasue of any changes in component parameters open override func updateView() { super.updateView() - positionLayout.rowQuantity = rowQuantity - - var width: CGFloat? - tilelets.forEach { tilelet in - if let width { - tilelet.width = width - } - } - collectionView.reloadData() + flowLayout.verticalPadding = padding.verticalValue() + flowLayout.horizontalPadding = padding.horizontalValue() + matrixView.reloadData() + matrixView.collectionViewLayout.invalidateLayout() } open override func setDefaults() { super.setDefaults() - rowQuantityPhone = 0 - rowQuantityTablet = 0 - tilelets = [] - } - - open override func reset() { - tilelets.forEach { $0.reset() } - super.reset() + padding = .standard + rows = [[]] } - 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() + func updateRows() { + var tempRows: [[Tilelet]] = [] + for i in stride(from: 0, to: tilelets.count, by: rowQuantity) { + let endIndex = min(i + rowQuantity, tilelets.count) + let row = Array(tilelets[i.. Int { - return 1 + return rows.count } - public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return tilelets.count + return rows[section].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() - } + public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TileletGroupCellItem.Identifier, for: indexPath) as? TileletGroupCellItem else { return UICollectionViewCell() } + let currentItem = rows[indexPath.section][indexPath.row] + cell.updateCell(component: currentItem) +// let rowColors: (UIColor,UIColor) = indexPath.section % 2 == 0 ? (.green, .yellow) : (.blue, .brown) +// cell.backgroundColor = indexPath.row % 2 == 0 ? rowColors.0 : rowColors.1 return cell } - public func collectionView(_ collectionView: UICollectionView, sizeForItemAtIndexPath indexPath: IndexPath) -> CGSize { - tilelets[indexPath.row].intrinsicContentSize + //-------------------------------------------------- + // MARK: - TableCollectionViewLayoutDataDelegate + //-------------------------------------------------- + + internal func collectionView(_ collectionView: UICollectionView, dataForItemAt indexPath: IndexPath) -> Tilelet { + return rows[indexPath.section][indexPath.row] } } -extension TileletGroup : TileletGroupPositionLayoutDelegate { - func collectionView(_ collectionView: UICollectionView, tileletAtIndexPath indexPath: IndexPath) -> Tilelet { - tilelets[indexPath.row] +final class TileletGroupCellItem: UICollectionViewCell { + + /// Identifier for TableCellItem + static let Identifier: String = String(describing: TileletGroupCellItem.self) + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + + /// Main view which holds the content of the cell + private let containerView = View() + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + override init(frame: CGRect) { + super.init(frame: frame) + setupCell() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupCell() + } + + private func setupCell() { + contentView.backgroundColor = .clear + addSubview(containerView) + containerView.pinToSuperView() + } + + //-------------------------------------------------- + // MARK: - Public Methods + //-------------------------------------------------- + + public func updateCell(component: Tilelet) { + containerView.subviews.forEach({ $0.removeFromSuperview() }) + containerView.addSubview(component) + component.pinToSuperView() } } diff --git a/VDS/Components/Tilelet/TileletGroup/TileletGroupFlowLayout.swift b/VDS/Components/Tilelet/TileletGroup/TileletGroupFlowLayout.swift new file mode 100644 index 00000000..cc03d719 --- /dev/null +++ b/VDS/Components/Tilelet/TileletGroup/TileletGroupFlowLayout.swift @@ -0,0 +1,142 @@ +// +// TileletGroupPositionLayout.swift +// VDS +// +// Created by Matt Bruce on 10/8/24. +// + +import Foundation +import UIKit +import VDSCoreTokens + +protocol TiletGroupCollectionViewLayoutDataDelegate: AnyObject { + func collectionView(_ collectionView: UICollectionView, dataForItemAt indexPath: IndexPath) -> Tilelet +} + +class TileletGroupFlowLayout : UICollectionViewFlowLayout { + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + + /// Parameter to store the layout attributes of cell, while calculating the size & position of the cell + private var itemCache: [UICollectionViewLayoutAttributes] = [] + + /// Parameter to store the total height of the collectionView + private var layoutHeight: CGFloat = 0.0 + + /// Parameter to store the total width of the collectionView + private var layoutWidth: CGFloat = 0.0 + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + /// Spacing between items in the row + public var horizontalPadding: CGFloat = VDSLayout.space1X + + /// Spacing between rows (sections) + public var verticalPadding: CGFloat = VDSLayout.space1X + + //-------------------------------------------------- + // MARK: - Internal Properties + //-------------------------------------------------- + + weak var delegate: TiletGroupCollectionViewLayoutDataDelegate? + + //-------------------------------------------------- + // MARK: - Overrides + //-------------------------------------------------- + + /// Calculates the layout attribute properties & total height of the collectionView + override func prepare() { + super.prepare() + + itemCache.removeAll() + layoutHeight = 0.0 + + guard let collectionView, let delegate else { return } + + let sections = collectionView.numberOfSections + var yPos: CGFloat = 0.0 + + /// Calculate the available width for items (subtract the left and right padding) + let collectionViewWidth = collectionView.bounds.width + + /// Determine the number of columns and calculate item width based on available space + /// + let columns = collectionView.numberOfItems(inSection: 0) + let totalPadding = horizontalPadding * CGFloat(columns - 1) + let availableWidth = collectionViewWidth - totalPadding + let itemWidth = availableWidth / CGFloat(columns) + + for currentSection in 0.. 0 ? horizontalPadding : 0) + + let attribute = UICollectionViewLayoutAttributes(forCellWith: indexPath) + attribute.frame = CGRect(x: xPos, y: yPos, width: itemWidth, height: itemHeight) + itemCache.append(attribute) + + /// Update x position for the next item + xPos += itemWidth + horizontalPadding + } + + /// Determines the highest height from all the cells (columns) in the row + let highestHeightForSection = itemCache + .filter({ $0.indexPath.section == currentSection }) + .sorted(by: { $0.frame.size.height > $1.frame.size.height }) + .first?.frame.size.height ?? 0.0 + + /// Set the highest height as height to all the cells in the row for uniform height + itemCache + .filter({ $0.indexPath.section == currentSection }) + .forEach { attributes in + attributes.frame.size.height = highestHeightForSection + } + + /// Adds the height to y position for the next section, including vertical padding + yPos += highestHeightForSection + verticalPadding + } + + layoutHeight = yPos - verticalPadding /// Adjust bottom padding to total height + } + + /// Fetches estimated height by calling the cell's component estimated height and adding padding + private func estimateHeightFor(item: Tilelet, with width: CGFloat, index: IndexPath) -> CGFloat { + let itemWidth = width + let maxSize = CGSize(width: itemWidth, height: CGFloat.greatestFiniteMagnitude) + let estItemSize = item.systemLayoutSizeFitting(maxSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel) + return estItemSize.height + } + + /// This will return the layout attributes for the elements in the defined rect + override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { + var visibleLayoutAttributes: [UICollectionViewLayoutAttributes] = [] + for attributes in itemCache { + if attributes.frame.intersects(rect) { + visibleLayoutAttributes.append(attributes) + } + } + return visibleLayoutAttributes + } + + /// This will return the layout attributes at particular indexPath + override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { + return itemCache.first(where: { $0.indexPath == indexPath }) + } + + /// Returns the collectionView content size + override var collectionViewContentSize: CGSize { + return CGSize(width: collectionView?.bounds.width ?? layoutWidth, height: layoutHeight) + } +} diff --git a/VDS/Components/Tilelet/TileletGroup/TileletGroupPositionLayout.swift b/VDS/Components/Tilelet/TileletGroup/TileletGroupPositionLayout.swift deleted file mode 100644 index 00088ed3..00000000 --- a/VDS/Components/Tilelet/TileletGroup/TileletGroupPositionLayout.swift +++ /dev/null @@ -1,248 +0,0 @@ -// -// 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.. 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.. 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 - } -} - - -