diff --git a/CHANGELOG.md b/CHANGELOG.md index 116557c..be932bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.0] - 2026-01-28 + +### Added +- `ColumnBehavior` enum to control how items are assigned to columns + - `lowestHeight`: Items placed in column with lowest height (default, existing behavior) + - `sequential`: Items placed sequentially across columns (0→col0, 1→col1, 2→col0, etc.) + - `custom(ColumnIndexProvider)`: Custom column assignment via closure +- `columnBehavior` parameter in `Configuration` struct +- `ColumnIndexProvider` typealias for custom column assignment closures + +### Fixed +- Resolved issue where height changes in one column would affect adjacent columns (#3) + ## [1.0.0] - 2022-11-24 ### Added @@ -21,4 +34,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support for iOS 14+ - Swift Package Manager support +[1.1.0]: https://github.com/eeshishko/WaterfallTrueCompositionalLayout/releases/tag/1.1.0 [1.0.0]: https://github.com/eeshishko/WaterfallTrueCompositionalLayout/releases/tag/1.0.0 diff --git a/Sources/WaterfallTrueCompositionalLayout/WaterfallTrueCompositionalLayout+Config.swift b/Sources/WaterfallTrueCompositionalLayout/WaterfallTrueCompositionalLayout+Config.swift index 45af423..e299b46 100644 --- a/Sources/WaterfallTrueCompositionalLayout/WaterfallTrueCompositionalLayout+Config.swift +++ b/Sources/WaterfallTrueCompositionalLayout/WaterfallTrueCompositionalLayout+Config.swift @@ -10,31 +10,46 @@ import UIKit public extension WaterfallTrueCompositionalLayout { typealias ItemHeightProvider = (_ index: Int, _ itemWidth: CGFloat) -> CGFloat typealias ItemCountProvider = () -> Int - + typealias ColumnIndexProvider = (_ index: Int, _ columnCount: Int) -> Int + + /// Determines how items are assigned to columns in the waterfall layout + enum ColumnBehavior { + /// Items are placed in the column with the lowest current height (default waterfall behavior) + case lowestHeight + /// Items are placed sequentially across columns (0→col0, 1→col1, 2→col0, etc.) + case sequential + /// Custom column assignment via closure + case custom(ColumnIndexProvider) + } + struct Configuration { public let columnCount: Int public let interItemSpacing: CGFloat public let contentInsetsReference: UIContentInsetsReference + public let columnBehavior: ColumnBehavior public let itemHeightProvider: ItemHeightProvider public let itemCountProvider: ItemCountProvider - + /// Initialization for configuration of waterfall compositional layout section /// - Parameters: /// - columnCount: a number of columns /// - interItemSpacing: a spacing between columns and rows /// - contentInsetsReference: a reference point for content insets for a section + /// - columnBehavior: determines how items are assigned to columns /// - itemCountProvider: closure providing a number of items in a section /// - itemHeightProvider: closure for providing an item height at a specific index public init( columnCount: Int = 2, interItemSpacing: CGFloat = 8, contentInsetsReference: UIContentInsetsReference = .automatic, + columnBehavior: ColumnBehavior = .lowestHeight, itemCountProvider: @escaping ItemCountProvider, itemHeightProvider: @escaping ItemHeightProvider ) { self.columnCount = columnCount self.interItemSpacing = interItemSpacing self.contentInsetsReference = contentInsetsReference + self.columnBehavior = columnBehavior self.itemCountProvider = itemCountProvider self.itemHeightProvider = itemHeightProvider } diff --git a/Sources/WaterfallTrueCompositionalLayout/WaterfallTrueCompositionalLayout+LayoutBuilder.swift b/Sources/WaterfallTrueCompositionalLayout/WaterfallTrueCompositionalLayout+LayoutBuilder.swift index 757109c..ef9fc7f 100644 --- a/Sources/WaterfallTrueCompositionalLayout/WaterfallTrueCompositionalLayout+LayoutBuilder.swift +++ b/Sources/WaterfallTrueCompositionalLayout/WaterfallTrueCompositionalLayout+LayoutBuilder.swift @@ -10,28 +10,30 @@ import UIKit extension WaterfallTrueCompositionalLayout { final class LayoutBuilder { private var columnHeights: [CGFloat] - private let columnCount: CGFloat + private let columnCount: Int + private let columnBehavior: ColumnBehavior private let itemHeightProvider: ItemHeightProvider private let interItemSpacing: CGFloat private let collectionWidth: CGFloat - + init( configuration: Configuration, collectionWidth: CGFloat ) { self.columnHeights = [CGFloat](repeating: 0, count: configuration.columnCount) - self.columnCount = CGFloat(configuration.columnCount) + self.columnCount = configuration.columnCount + self.columnBehavior = configuration.columnBehavior self.itemHeightProvider = configuration.itemHeightProvider self.interItemSpacing = configuration.interItemSpacing self.collectionWidth = collectionWidth } - + func makeLayoutItem(for row: Int) -> NSCollectionLayoutGroupCustomItem { let frame = frame(for: row) - columnHeights[columnIndex()] = frame.maxY + interItemSpacing + columnHeights[columnIndex(for: row)] = frame.maxY + interItemSpacing return NSCollectionLayoutGroupCustomItem(frame: frame) } - + func maxColumnHeight() -> CGFloat { return columnHeights.max() ?? 0 } @@ -39,29 +41,38 @@ extension WaterfallTrueCompositionalLayout { } private extension WaterfallTrueCompositionalLayout.LayoutBuilder { - private var columnWidth: CGFloat { - let spacing = (columnCount - 1) * interItemSpacing - return (collectionWidth - spacing) / columnCount + var columnWidth: CGFloat { + let spacing = CGFloat(columnCount - 1) * interItemSpacing + return (collectionWidth - spacing) / CGFloat(columnCount) } - + func frame(for row: Int) -> CGRect { let width = columnWidth let height = itemHeightProvider(row, width) let size = CGSize(width: width, height: height) - let origin = itemOrigin(width: size.width) + let origin = itemOrigin(for: row, width: size.width) return CGRect(origin: origin, size: size) } - - private func itemOrigin(width: CGFloat) -> CGPoint { - let y = columnHeights[columnIndex()].rounded() - let x = (width + interItemSpacing) * CGFloat(columnIndex()) + + func itemOrigin(for row: Int, width: CGFloat) -> CGPoint { + let column = columnIndex(for: row) + let y = columnHeights[column].rounded() + let x = (width + interItemSpacing) * CGFloat(column) return CGPoint(x: x, y: y) } - - private func columnIndex() -> Int { - columnHeights - .enumerated() - .min(by: { $0.element < $1.element })? - .offset ?? 0 + + func columnIndex(for row: Int) -> Int { + switch columnBehavior { + case .lowestHeight: + return columnHeights + .enumerated() + .min(by: { $0.element < $1.element })? + .offset ?? 0 + case .sequential: + return row % columnCount + case .custom(let provider): + let index = provider(row, columnCount) + return max(0, min(index, columnCount - 1)) + } } }