diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3b29812
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+.DS_Store
+/.build
+/Packages
+/*.xcodeproj
+xcuserdata/
+DerivedData/
+.swiftpm/config/registries.json
+.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
+.netrc
diff --git a/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/Package.swift b/Package.swift
new file mode 100644
index 0000000..db1d853
--- /dev/null
+++ b/Package.swift
@@ -0,0 +1,18 @@
+// swift-tools-version: 5.8
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let package = Package(
+ name: "MijickGridView",
+ platforms: [
+ .iOS(.v14)
+ ],
+ products: [
+ .library(name: "MijickGridView", targets: ["MijickGridView"]),
+ ],
+ targets: [
+ .target(name: "MijickGridView", dependencies: [], path: "Sources"),
+ .testTarget(name: "MijickGridViewTests", dependencies: ["MijickGridView"], path: "Tests")
+ ]
+)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..bef01be
--- /dev/null
+++ b/README.md
@@ -0,0 +1,154 @@
+
+
+
+
+
+
+
+
+ Layouts made simple
+
+
+
+ Lay out your data in the blink of an eye. Keep your code clean
+
+
+
+ Try demo we prepared
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+GridView is a free, and open-source library for SwiftUI that makes creating grids easier and much cleaner.
+* **Improves code quality.** Create a grid using `GridView` constructor - simply pass your data and we'll deal with the rest. Simple as never!
+* **Designed for SwiftUI.** While developing the library, we have used the power of SwiftUI to give you powerful tool to speed up your implementation process.
+
+
+
+# Getting Started
+### ✋ Requirements
+
+| **Platforms** | **Minimum Swift Version** |
+|:----------|:----------|
+| iOS 14+ | 5.0 |
+
+### ⏳ Installation
+
+#### [Swift Package Manager][spm]
+Swift Package Manager is a tool for automating the distribution of Swift code and is integrated into the Swift compiler.
+
+Once you have your Swift package set up, adding Navigattie as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`.
+
+```Swift
+dependencies: [
+ .package(url: "https://github.com/Mijick/GridView", branch(“main”))
+]
+```
+
+
+
+# Usage
+### 1. Call initialiser
+To declare a Grid for your data set, call the constructor:
+
+```Swift
+struct ContentView: View {
+ private let data = [SomeData]()
+
+ var body: some View {
+ GridView(data, id: \.self) { element in
+ SomeItem(element: element)
+ }
+ }
+}
+```
+
+### 2. Customise Grid
+Your GridView can be customised by calling `configBuilder` inside the initialiser:
+
+```Swift
+struct ContentView: View {
+ private let data = [SomeData]()
+
+ var body: some View {
+ GridView(data, id: \.self, content: SomeItem.init, configBuilder: { $0
+ .insertionPolicy(.fill)
+ .columns(4)
+ .verticalSpacing(12)
+ })
+ }
+}
+```
+
+
+### 3. Declare number of columns
+You can change the number of columns of an item by calling .columns of Item:
+```Swift
+struct ContentView: View { ... }
+struct SomeItem: View {
+ ...
+
+ var body: some View {
+ ...
+ .columns(2)
+ }
+}
+```
+
+
+
+
+# Try our demo
+See for yourself how does it work by cloning [project][Demo] we created
+
+# License
+Navigattie is released under the MIT license. See [LICENSE][License] for details.
+
+
+
+# Our other open source SwiftUI libraries
+[PopupView] - The most powerful popup library that allows you to present any popup
+
+[Navigattie] - Easier and cleaner way of navigating through your app
+
+
+[MIT]: https://en.wikipedia.org/wiki/MIT_License
+[SPM]: https://www.swift.org/package-manager
+
+[Demo]: https://github.com/Mijick/GridView-Demo
+[License]: https://github.com/Mijick/GridView/blob/main/LICENSE
+
+[PopupView]: https://github.com/Mijick/PopupView
+[Navigattie]: https://github.com/Mijick/Navigattie
diff --git a/Sources/Internal/Extensions/Array++.swift b/Sources/Internal/Extensions/Array++.swift
new file mode 100644
index 0000000..e4b95dd
--- /dev/null
+++ b/Sources/Internal/Extensions/Array++.swift
@@ -0,0 +1,17 @@
+//
+// Array++.swift of MijickGridView
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+// - GitHub: https://github.com/FulcrumOne
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+import SwiftUI
+
+// MARK: - Removing Duplicates
+extension Array where Element: Hashable {
+ func removingDuplicates() -> Self { Array(Set(self)) }
+}
diff --git a/Sources/Internal/Extensions/Int++.swift b/Sources/Internal/Extensions/Int++.swift
new file mode 100644
index 0000000..8373ddf
--- /dev/null
+++ b/Sources/Internal/Extensions/Int++.swift
@@ -0,0 +1,17 @@
+//
+// Int++.swift of MijickGridView
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+// - GitHub: https://github.com/FulcrumOne
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+import SwiftUI
+
+extension Int {
+ func toDouble() -> Double { .init(self) }
+ func toCGFloat() -> CGFloat { .init(self) }
+}
diff --git a/Sources/Internal/Matrix/Matrix.Item.swift b/Sources/Internal/Matrix/Matrix.Item.swift
new file mode 100644
index 0000000..8107fe3
--- /dev/null
+++ b/Sources/Internal/Matrix/Matrix.Item.swift
@@ -0,0 +1,33 @@
+//
+// Matrix.Item.swift of MijickGridView
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+// - GitHub: https://github.com/FulcrumOne
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+import SwiftUI
+
+extension Matrix { struct Item {
+ var index: Int
+ var value: CGFloat
+ var columns: Int
+}}
+extension Matrix.Item: Hashable {
+ func hash(into hasher: inout Hasher) { hasher.combine(index) }
+}
+extension Matrix.Item {
+ var isEmpty: Bool { value == 0 }
+}
+
+
+// MARK: - Array
+extension [[Matrix.Item]] {
+ subscript(_ position: Matrix.Position) -> Matrix.Item {
+ get { self[position.row][position.column] }
+ set { self[position.row][position.column] = newValue }
+ }
+}
diff --git a/Sources/Internal/Matrix/Matrix.Position.swift b/Sources/Internal/Matrix/Matrix.Position.swift
new file mode 100644
index 0000000..a01fdaa
--- /dev/null
+++ b/Sources/Internal/Matrix/Matrix.Position.swift
@@ -0,0 +1,45 @@
+//
+// Matrix.Position.swift of MijickGridView
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+// - GitHub: https://github.com/FulcrumOne
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+import SwiftUI
+
+extension Matrix { struct Position {
+ let row: Int
+ let column: Int
+}}
+extension Matrix.Position: Comparable {
+ static func < (lhs: Matrix.Position, rhs: Matrix.Position) -> Bool {
+ if lhs.row == rhs.row { return lhs.column < rhs.column }
+ return lhs.row < rhs.row
+ }
+}
+
+// MARK: - Creating Item Range
+extension Matrix.Position {
+ func createItemRange(_ item: Matrix.Item) -> Matrix.Range { .init(from: self, to: withColumn(column + item.columns - 1)) }
+}
+
+// MARK: - Helpers
+extension Matrix.Position {
+ func withRow(_ rowIndex: Int) -> Self { .init(row: rowIndex, column: column) }
+ func withColumn(_ columnIndex: Int) -> Self { .init(row: row, column: columnIndex) }
+
+ func nextRow() -> Self { withRow(row + 1) }
+ func nextColumn() -> Self { withColumn(column + 1) }
+
+ func previousRow() -> Self { withRow(row - 1) }
+ func previousColumn() -> Self { withColumn(column - 1) }
+}
+
+// MARK: - Objects
+extension Matrix.Position {
+ static var zero: Self { .init(row: 0, column: 0) }
+}
diff --git a/Sources/Internal/Matrix/Matrix.Range.swift b/Sources/Internal/Matrix/Matrix.Range.swift
new file mode 100644
index 0000000..e878d6a
--- /dev/null
+++ b/Sources/Internal/Matrix/Matrix.Range.swift
@@ -0,0 +1,41 @@
+//
+// Matrix.Range.swift of MijickGridView
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+// - GitHub: https://github.com/FulcrumOne
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+import SwiftUI
+
+extension Matrix { struct Range {
+ var start: Position
+ var end: Position
+
+ init(from startPosition: Matrix.Position, to endPosition: Matrix.Position) {
+ if startPosition > endPosition { fatalError("Start position must precede end position") }
+
+ start = startPosition
+ end = endPosition
+ }
+}}
+
+extension Matrix.Range {
+ func updating(newStart: Matrix.Position) -> Matrix.Range {
+ let endIndexOffset = newStart.column + columns.count - 1
+
+ var range = self
+ range.start = newStart
+ range.end = newStart.withColumn(endIndexOffset)
+ return range
+ }
+}
+
+// MARK: - Helpers
+extension Matrix.Range {
+ var rows: ClosedRange { start.row...end.row }
+ var columns: ClosedRange { start.column...end.column }
+}
diff --git a/Sources/Internal/Matrix/Matrix.swift b/Sources/Internal/Matrix/Matrix.swift
new file mode 100644
index 0000000..32c6312
--- /dev/null
+++ b/Sources/Internal/Matrix/Matrix.swift
@@ -0,0 +1,274 @@
+//
+// Matrix.swift of MijickGridView
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+// - GitHub: https://github.com/FulcrumOne
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+import SwiftUI
+
+struct Matrix {
+ let config: GridView.Config
+ private var items: [[Item]] = []
+ private var matrixInitialised: Bool = false
+
+
+ init(_ config: GridView.Config) { self.items = .init(numberOfColumns: config.numberOfColumns); self.config = config }
+}
+
+// MARK: - Inserting Items
+extension Matrix {
+ mutating func insert(_ item: Item, isLast: Bool) { if !matrixInitialised {
+ let position = findPositionForItem(item)
+ let range = position.createItemRange(item)
+
+ checkItem(item)
+ addNewRowIfNeeded(position)
+ insertItem(item, range)
+ sortMatrix(isLast)
+ }}
+}
+
+// MARK: Checking Item
+private extension Matrix {
+ func checkItem(_ item: Item) {
+ if item.columns > config.numberOfColumns { fatalError("Single element cannot have more columns than the entire view.") }
+ }
+}
+
+// MARK: Finding Position
+private extension Matrix {
+ func findPositionForItem(_ item: Item) -> Position {
+ guard item.index > 0 else { return .zero }
+
+ let previousItemRangeEnd = getRange(for: item.index - 1).end
+ return canInsertItemInRow(item, previousItemRangeEnd) ?
+ previousItemRangeEnd.nextColumn() : previousItemRangeEnd.nextRow().withColumn(0)
+ }
+}
+private extension Matrix {
+ func canInsertItemInRow(_ item: Item, _ previousItemRangeEnd: Position) -> Bool { item.columns + previousItemRangeEnd.column < numberOfColumns }
+}
+
+// MARK: Inserting New Row
+private extension Matrix {
+ mutating func addNewRowIfNeeded(_ position: Position) { if position.row >= items.count {
+ items.insertEmptyRow(numberOfColumns: numberOfColumns)
+ }}
+}
+
+// MARK: Filling Array
+private extension Matrix {
+ mutating func insertItem(_ item: Item, _ range: Range, _ isLast: Bool = false) {
+ let columnHeight = getHeights()
+ let range = recalculateRange(range, columnHeight, isLast)
+
+ fillInMatrixWithEmptySpaces(range, columnHeight)
+ fillInMatrixWithItem(item, range)
+ }
+}
+private extension Matrix {
+ func recalculateRange(_ range: Range, _ columnHeights: [[CGFloat]], _ isLast: Bool) -> Range {
+ guard isLast, range.end.row > 0 else { return range }
+
+ let columnsRangeStart = range.columns.lowerBound,
+ columnsRangeEnd = config.numberOfColumns - range.columns.count + 1,
+ columnsRange = columnsRangeStart.. 0 {
+ range.columns.forEach { column in
+ let currentPosition = range.start.withColumn(column), previousPosition = currentPosition.previousRow()
+ let difference = columnHeights[currentPosition] - columnHeights[previousPosition]
+
+ if shouldFillInEmptySpace(difference, items[previousPosition]) {
+ items[previousPosition] = .init(index: -1, value: difference, columns: 1)
+ }
+ }
+ }}
+ mutating func fillInMatrixWithItem(_ item: Item, _ range: Range) { range.columns.forEach { column in
+ items[range.start.withColumn(column)] = item
+ }}
+}
+private extension Matrix {
+ func shouldFillInEmptySpace(_ difference: CGFloat, _ item: Item) -> Bool { difference > 0 && item.isEmpty }
+}
+
+// MARK: Sorting Matrix
+private extension Matrix {
+ mutating func sortMatrix(_ isLastItem: Bool) { if policy == .fill && isLastItem {
+ let items = getUniqueSortedItems()
+ let proposedSortedMatrix = getProposedSortedMatrix(items)
+
+ eraseTemporaryMatrix()
+ insertSortedMatrix(proposedSortedMatrix)
+ setMatrixAsInitialised()
+ }}
+}
+private extension Matrix {
+ func getUniqueSortedItems() -> [Item] { items
+ .flatMap { $0 }
+ .filter { $0.index != -1 }
+ .removingDuplicates()
+ .sorted(by: { $0.index < $1.index })
+ .sorted(by: { $0.columns > $1.columns })
+ }
+ func getProposedSortedMatrix(_ items: [Item]) -> [[Item]] {
+ var array: [[Item]] = []
+
+ for item in items where !array.contains(item) {
+ let bestRow = getBestRow(array, items, item)
+ array.append(bestRow)
+ }
+
+ return array
+ }
+ mutating func eraseTemporaryMatrix() {
+ items = .init(numberOfColumns: numberOfColumns)
+ }
+ mutating func insertSortedMatrix(_ proposedSortedMatrix: [[Item]]) {
+ for row in 0.. [Matrix.Item] {
+ var proposedRows = [[item1]]
+
+ for item2 in getRemainingItems(results, items, item1) {
+ switch proposedRows.lastItem.columns + item2.columns <= numberOfColumns {
+ case true: proposedRows.lastItem.append(item2)
+ case false: proposedRows.append([])
+ }
+ }
+
+ return proposedRows.pickingBest()
+ }
+}
+private extension Matrix {
+ func getRemainingItems(_ results: [[Item]], _ items: [Item], _ item1: Item) -> [Item] { items
+ .filter { $0.columns + item1.columns <= numberOfColumns }
+ .filter { !results.contains($0) }
+ .filter { item1 != $0 }
+ }
+}
+
+// MARK: - Getting Item Position
+extension Matrix {
+ func getRange(for index: Int) -> Range {
+ let startPosition = getStartPosition(for: index)
+ return startPosition.createItemRange(items[startPosition])
+ }
+}
+private extension Matrix {
+ func getStartPosition(for index: Int) -> Position {
+ let rowIndex = items.firstIndex(where: { $0.contains(where: { $0.index == index }) }) ?? 0
+ let columnIndex = items[rowIndex].firstIndex(where: { $0.index == index }) ?? 0
+ return .init(row: rowIndex, column: columnIndex)
+ }
+}
+
+// MARK: - Getting Column Heights
+extension Matrix {
+ func getHeights() -> [[CGFloat]] {
+ var array: [[CGFloat]] = .init(repeating: .init(repeating: 0, count: numberOfColumns), count: items.count)
+
+ for row in 0.. 0 ? array[position.previousRow()] : 0
+
+ array[position] = currentValue + previousRowPositionValue
+ }
+ func updateValuesForMultigridItem(_ position: Position, _ item: Item, _ array: inout [[CGFloat]]) { if item.columns > 1 {
+ let range = getStartPosition(for: item.index).createItemRange(item)
+
+ guard range.end.column == position.column else { return }
+
+ let maxValue = array[position.row][range.columns].max() ?? 0
+ range.columns.forEach { array[position.row][$0] = maxValue }
+ }}
+}
+
+// MARK: - Others
+extension Matrix {
+ var itemsSpacing: CGFloat { config.spacing.vertical }
+ var policy: InsertionPolicy { config.insertionPolicy }
+ var numberOfColumns: Int { config.numberOfColumns }
+}
+
+
+// MARK: - Helpers
+fileprivate extension [[Matrix.Item]] {
+ init(numberOfColumns: Int) { self = [.empty(numberOfColumns)] }
+ mutating func insertEmptyRow(numberOfColumns: Int) { append(.empty(numberOfColumns)) }
+}
+fileprivate extension [[Matrix.Item]] {
+ func contains(_ element: Matrix.Item) -> Bool { joined().contains(where: { $0.index == element.index }) }
+ func pickingBest() -> [Matrix.Item] { self.min(by: { $0.heightsDifference < $1.heightsDifference }) ?? [] }
+}
+fileprivate extension [[Matrix.Item]] {
+ var lastItem: [Matrix.Item] {
+ get { last ?? [] }
+ set { self[count - 1] = newValue }
+ }
+}
+
+fileprivate extension [Matrix.Item] {
+ static func empty(_ numberOfColumns: Int) -> Self { .init(repeating: .init(index: -1, value: 0, columns: 1), count: numberOfColumns) }
+}
+fileprivate extension [Matrix.Item] {
+ var heightsDifference: CGFloat {
+ let min = self.min(by: { $0.value < $1.value })?.value ?? 0
+ let max = self.max(by: { $0.value > $1.value })?.value ?? 0
+
+ return max - min
+ }
+ var columns: Int { reduce(0, { $0 + $1.columns }) }
+}
+
+fileprivate extension [[CGFloat]] {
+ subscript(position: Matrix.Position) -> CGFloat {
+ get { self[position.row][position.column] }
+ set { self[position.row][position.column] = newValue }
+ }
+}
diff --git a/Sources/Internal/Other/AnyGridElement.swift b/Sources/Internal/Other/AnyGridElement.swift
new file mode 100644
index 0000000..d3af690
--- /dev/null
+++ b/Sources/Internal/Other/AnyGridElement.swift
@@ -0,0 +1,31 @@
+//
+// AnyGridElement.swift of MijickGridView
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+// - GitHub: https://github.com/FulcrumOne
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+import SwiftUI
+
+struct AnyGridElement: GridElement {
+ @State var height: CGFloat? = nil
+ let columns: Int
+ private let _body: AnyView
+
+
+ var body: some View { _body }
+ init(_ element: some View, numberOfColumns: Int? = nil) {
+ self.columns = Self.getNumberOfColumns(element, numberOfColumns)
+ self._body = AnyView(element)
+ }
+}
+private extension AnyGridElement {
+ static func getNumberOfColumns(_ element: some View, _ numberOfColumns: Int?) -> Int {
+ if let element = element as? any GridElement { return element.columns }
+ return numberOfColumns ?? 1
+ }
+}
diff --git a/Sources/Internal/Protocols/Configurable.swift b/Sources/Internal/Protocols/Configurable.swift
new file mode 100644
index 0000000..39a6b1d
--- /dev/null
+++ b/Sources/Internal/Protocols/Configurable.swift
@@ -0,0 +1,19 @@
+//
+// Configurable.swift of MijickGridView
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+// - GitHub: https://github.com/FulcrumOne
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+public protocol Configurable { init() }
+extension Configurable {
+ func changing(path: WritableKeyPath, to value: T) -> Self {
+ var clone = self
+ clone[keyPath: path] = value
+ return clone
+ }
+}
diff --git a/Sources/Internal/View Modifiers/HeightReader.swift b/Sources/Internal/View Modifiers/HeightReader.swift
new file mode 100644
index 0000000..a7087a8
--- /dev/null
+++ b/Sources/Internal/View Modifiers/HeightReader.swift
@@ -0,0 +1,30 @@
+//
+// HeightReader.swift of MijickGridView
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+// - GitHub: https://github.com/FulcrumOne
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+import SwiftUI
+
+extension View {
+ func readHeight(onChange action: @escaping (CGFloat) -> ()) -> some View { modifier(Modifier(onHeightChange: action)) }
+}
+
+// MARK: - Implementation
+fileprivate struct Modifier: ViewModifier {
+ let onHeightChange: (CGFloat) -> ()
+
+ func body(content: Content) -> some View { content
+ .background(
+ GeometryReader { geo -> Color in
+ DispatchQueue.main.async { onHeightChange(geo.size.height) }
+ return Color.clear
+ }
+ )
+ }
+}
diff --git a/Sources/Internal/Views/GridView.swift b/Sources/Internal/Views/GridView.swift
new file mode 100644
index 0000000..127d82b
--- /dev/null
+++ b/Sources/Internal/Views/GridView.swift
@@ -0,0 +1,115 @@
+//
+// GridView.swift of MijickGridView
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+// - GitHub: https://github.com/FulcrumOne
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+import SwiftUI
+
+public struct GridView: View {
+ @State var matrix: Matrix
+ var elements: [AnyGridElement]
+ var config: Config
+
+
+ public var body: some View {
+ GeometryReader { reader in
+ ZStack(alignment: .topLeading) {
+ ForEach(0.. some View {
+ elements[index]
+ .readHeight { saveHeight($0, index) }
+ .fixedSize(horizontal: false, vertical: true)
+ .alignmentGuide(.top) { handleTopAlignmentGuide(index, $0, reader) }
+ .alignmentGuide(.leading) { handleLeadingAlignmentGuide(index, $0, reader) }
+ .frame(width: calculateItemWidth(index, reader.size.width), height: elements[index].height)
+ }
+}
+
+// MARK: - Reading Height
+private extension GridView {
+ func saveHeight(_ value: CGFloat, _ index: Int) {
+ elements[index].height = value
+ }
+}
+
+// MARK: - Vertical Alignment
+private extension GridView {
+ func handleTopAlignmentGuide(_ index: Int, _ dimensions: ViewDimensions, _ reader: GeometryProxy) -> CGFloat {
+ insertItem(index, dimensions.height)
+
+ let topPadding = getTopPaddingValue(index)
+ return topPadding
+ }
+}
+private extension GridView {
+ func insertItem(_ index: Int, _ value: CGFloat) { DispatchQueue.main.async {
+ let item = Matrix.Item(index: index, value: value, columns: elements[index].columns)
+ matrix.insert(item, isLast: index == elements.count - 1)
+ }}
+ func getTopPaddingValue(_ index: Int) -> CGFloat {
+ let range = matrix.getRange(for: index)
+
+ guard range.start.row > 0 else { return 0 }
+
+ let heights = matrix.getHeights()[range.start.row - 1]
+ let itemTopPadding = heights[range.columns].max() ?? 0
+ return -itemTopPadding
+ }
+}
+
+// MARK: - Horizontal Alignment
+private extension GridView {
+ func handleLeadingAlignmentGuide(_ index: Int, _ dimensions: ViewDimensions, _ reader: GeometryProxy) -> CGFloat {
+ let availableWidth = reader.size.width
+ let itemPadding = calculateItemLeadingPadding(index, availableWidth)
+ return itemPadding
+ }
+}
+private extension GridView {
+ func calculateItemLeadingPadding(_ index: Int, _ availableWidth: CGFloat) -> CGFloat {
+ let columnNumber = matrix.getRange(for: index).start.column
+ let singleColumnWidth = calculateSingleColumnWidth(availableWidth)
+
+ let rawColumnsPaddingValue = singleColumnWidth * columnNumber.toCGFloat()
+ let rawSpacingPaddingValue = (columnNumber.toCGFloat() - 1) * config.spacing.horizontal
+
+ let rawPaddingValue = rawColumnsPaddingValue + rawSpacingPaddingValue
+ return -rawPaddingValue
+ }
+}
+
+// MARK: - Dimensions
+private extension GridView {
+ func calculateItemWidth(_ index: Int, _ availableWidth: CGFloat) -> CGFloat {
+ let itemColumns = elements[index].columns
+ let singleColumnWidth = calculateSingleColumnWidth(availableWidth)
+
+ let fixedItemWidth = singleColumnWidth * itemColumns.toCGFloat()
+ let additionalHorizontalSpacing = (itemColumns.toCGFloat() - 1) * config.spacing.horizontal
+ return fixedItemWidth + additionalHorizontalSpacing
+ }
+ func calculateContentHeight() -> CGFloat { matrix.getHeights().flatMap { $0 }.max() ?? 0 }
+}
+private extension GridView {
+ func calculateSingleColumnWidth(_ availableWidth: CGFloat) -> CGFloat {
+ let totalSpacingValue = getHorizontalSpacingTotalValue()
+ let itemsWidth = availableWidth - totalSpacingValue
+ return itemsWidth / matrix.numberOfColumns.toCGFloat()
+ }
+ func getHorizontalSpacingTotalValue() -> CGFloat {
+ let numberOfSpaces = matrix.numberOfColumns - 1
+ return numberOfSpaces.toCGFloat() * config.spacing.horizontal
+ }
+}
diff --git a/Sources/Public/Configurables/Public+GridViewConfig.swift b/Sources/Public/Configurables/Public+GridViewConfig.swift
new file mode 100644
index 0000000..5cae0b9
--- /dev/null
+++ b/Sources/Public/Configurables/Public+GridViewConfig.swift
@@ -0,0 +1,33 @@
+//
+// Public+GridViewConfig.swift of MijickGridView
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+// - GitHub: https://github.com/FulcrumOne
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+import SwiftUI
+
+// MARK: - Policies
+public extension GridView.Config {
+ func insertionPolicy(_ value: InsertionPolicy) -> Self { changing(path: \.insertionPolicy, to: value) }
+}
+
+// MARK: - Composition
+public extension GridView.Config {
+ func columns(_ value: Int) -> Self { changing(path: \.numberOfColumns, to: value) }
+ func verticalSpacing(_ value: CGFloat) -> Self { changing(path: \.spacing.vertical, to: value) }
+ func horizontalSpacing(_ value: CGFloat) -> Self { changing(path: \.spacing.horizontal, to: value) }
+}
+
+
+// MARK: - Internal
+extension GridView { public struct Config: Configurable { public init() {}
+ private(set) var insertionPolicy: InsertionPolicy = .ordered
+
+ private(set) var numberOfColumns: Int = 2
+ private(set) var spacing: (vertical: CGFloat, horizontal: CGFloat) = (8, 8)
+}}
diff --git a/Sources/Public/Enumerations/Public+InsertionPolicy.swift b/Sources/Public/Enumerations/Public+InsertionPolicy.swift
new file mode 100644
index 0000000..c65f5d6
--- /dev/null
+++ b/Sources/Public/Enumerations/Public+InsertionPolicy.swift
@@ -0,0 +1,12 @@
+//
+// Public+InsertionPolicy.swift of MijickGridView
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+// - GitHub: https://github.com/FulcrumOne
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+public enum InsertionPolicy { case ordered, fill }
diff --git a/Sources/Public/Extensions/Public+GridView.swift b/Sources/Public/Extensions/Public+GridView.swift
new file mode 100644
index 0000000..d044980
--- /dev/null
+++ b/Sources/Public/Extensions/Public+GridView.swift
@@ -0,0 +1,21 @@
+//
+// Public+GridView.swift of MijickGridView
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+// - GitHub: https://github.com/FulcrumOne
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+import SwiftUI
+
+// MARK: - Initialisers
+extension GridView {
+ public init(_ data: Data, id: KeyPath, @ViewBuilder content: @escaping (Data.Element) -> any View, configBuilder: (Config) -> Config = { $0 }) { self.init(
+ matrix: .init(configBuilder(.init())),
+ elements: data.map { .init(content($0)) },
+ config: configBuilder(.init())
+ )}
+}
diff --git a/Sources/Public/Extensions/Public+View.swift b/Sources/Public/Extensions/Public+View.swift
new file mode 100644
index 0000000..2a5747c
--- /dev/null
+++ b/Sources/Public/Extensions/Public+View.swift
@@ -0,0 +1,16 @@
+//
+// Public+View.swift of MijickGridView
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+// - GitHub: https://github.com/FulcrumOne
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+import SwiftUI
+
+public extension View {
+ func columns(_ value: Int) -> some GridElement { AnyGridElement(self, numberOfColumns: value) }
+}
diff --git a/Sources/Public/Protocols/Public+GridElement.swift b/Sources/Public/Protocols/Public+GridElement.swift
new file mode 100644
index 0000000..7d7483a
--- /dev/null
+++ b/Sources/Public/Protocols/Public+GridElement.swift
@@ -0,0 +1,16 @@
+//
+// Public+GridElement.swift of MijickGridView
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+// - GitHub: https://github.com/FulcrumOne
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+import SwiftUI
+
+public protocol GridElement: View {
+ var columns: Int { get }
+}
diff --git a/Tests/Cases/Matrix_Test.swift b/Tests/Cases/Matrix_Test.swift
new file mode 100644
index 0000000..2042067
--- /dev/null
+++ b/Tests/Cases/Matrix_Test.swift
@@ -0,0 +1,83 @@
+//
+// Matrix_Test.swift of MijickGridView
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+// - GitHub: https://github.com/FulcrumOne
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+import XCTest
+@testable import GridView
+
+final class Matrix_Test: XCTestCase {
+ var matrix: Matrix = .init(columns: 4, itemsSpacing: 8, policy: .fill)
+}
+
+// MARK: - Inserting
+extension Matrix_Test {
+ func testInsertValue_WhenMatrixIsEmpty() {
+ matrix.insert(.init(index: 0, value: 100))
+
+ let result: [[CGFloat]] = matrix.items.map { $0.map(\.value) }
+ let expectedResult: [[CGFloat]] = [[100, 0, 0, 0]]
+
+ XCTAssertEqual(result, expectedResult)
+ }
+ func testInsertValue_WhenItemWithIndexIsPresent_ShouldNotInsertItem() {
+ matrix.insert(.init(index: 0, value: 100))
+ matrix.insert(.init(index: 0, value: 200))
+
+ let result = matrix.items.flatMap { $0 }.filter { !$0.isEmpty }.count
+ let expectedResult = 1
+
+ XCTAssertEqual(result, expectedResult)
+ }
+ func testInsertValue_InsertManyItemsOfDifferentIndexes_ShouldInsertItems() {
+ for index in 0..<5 { matrix.insert(.init(index: index, value: 100)) }
+
+ let result = matrix.items.flatMap { $0 }.filter { !$0.isEmpty }.count
+ let expectedResult = 5
+
+ XCTAssertEqual(result, expectedResult)
+ }
+ func testInsertValue_InsertManyItemsOfDifferentIndexes_ShouldAddNewRow() {
+ for index in 0..<5 { matrix.insert(.init(index: index, value: 100)) }
+
+ let result = matrix.items.count
+ let expectedResult = 2
+
+ XCTAssertEqual(result, expectedResult)
+ }
+ func testInsertValue_WhenMatrixHasOneRowFilled_OrderedPolicy_ShouldMatchPattern() {
+ matrix = .init(columns: 4, itemsSpacing: 8, policy: .ordered)
+ for index in 0..<10 { matrix.insert(.init(index: index, value: entryValues[index])) }
+
+ let result: [[CGFloat]] = matrix.items.map { $0.map { $0.value } }
+ let expectedResult: [[CGFloat]] = [
+ [100, 200, 50, 100],
+ [150, 100, 50, 100],
+ [100, 150, 0, 0]
+ ]
+
+ XCTAssertEqual(result, expectedResult)
+ }
+ func testInsertValue_WhenMatrixHasOneRowFilled_FillPolicy_ShouldMatchPattern() {
+ for index in 0..<10 { matrix.insert(.init(index: index, value: entryValues[index])) }
+
+ let result: [[CGFloat]] = matrix.items.map { $0.map { $0.value } }
+ let expectedResult: [[CGFloat]] = [
+ [100, 200, 50, 100],
+ [100, 150, 150, 50],
+ [100, 0, 0, 100]
+ ]
+
+ XCTAssertEqual(result, expectedResult)
+ }
+}
+
+private extension Matrix_Test {
+ var entryValues: [CGFloat] { [100, 200, 50, 100, 150, 100, 50, 100, 100, 150] }
+}
diff --git a/Tests/Helpers/Array++.swift b/Tests/Helpers/Array++.swift
new file mode 100644
index 0000000..330a63f
--- /dev/null
+++ b/Tests/Helpers/Array++.swift
@@ -0,0 +1,29 @@
+//
+// Array++.swift of MijickGridView
+//
+// Created by Tomasz Kurylik
+// - Twitter: https://twitter.com/tkurylik
+// - Mail: tomasz.kurylik@mijick.com
+// - GitHub: https://github.com/FulcrumOne
+//
+// Copyright ©2023 Mijick. Licensed under MIT License.
+
+
+import SwiftUI
+
+extension [[CGFloat]] {
+ func toString() -> String {
+ var text = ""
+
+ for row in 0..