Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,58 +10,69 @@ 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
}
}
}

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