├── .github
└── FUNDING.yml
├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── LICENSE
├── Package.swift
├── README.md
├── Sources
├── Internal
│ ├── Extensions
│ │ ├── Array++.swift
│ │ └── Int++.swift
│ ├── Matrix
│ │ ├── Matrix.Item.swift
│ │ ├── Matrix.Position.swift
│ │ ├── Matrix.Range.swift
│ │ └── Matrix.swift
│ ├── Other
│ │ └── AnyGridElement.swift
│ ├── Protocols
│ │ └── Configurable.swift
│ ├── View Modifiers
│ │ └── HeightReader.swift
│ └── Views
│ │ └── GridView.swift
└── Public
│ ├── Configurables
│ └── Public+GridViewConfig.swift
│ ├── Enumerations
│ └── Public+InsertionPolicy.swift
│ ├── Extensions
│ ├── Public+GridView.swift
│ └── Public+View.swift
│ └── Protocols
│ └── Public+GridElement.swift
└── Tests
├── Cases
└── Matrix_Test.swift
└── Helpers
└── Array++.swift
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: mijick
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: mijick
14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/config/registries.json
8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9 | .netrc
10 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Mijick
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.8
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "MijickGridView",
8 | platforms: [
9 | .iOS(.v14)
10 | ],
11 | products: [
12 | .library(name: "MijickGridView", targets: ["MijickGridView"]),
13 | ],
14 | targets: [
15 | .target(name: "MijickGridView", dependencies: [], path: "Sources"),
16 | .testTarget(name: "MijickGridViewTests", dependencies: ["MijickGridView"], path: "Tests")
17 | ]
18 | )
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Layouts made simple
14 |
15 |
16 |
17 | Lay out your data in the blink of an eye. Keep your code clean
18 |
19 |
20 |
21 | Try demo we prepared
22 | |
23 | Roadmap
24 | |
25 | Propose a new feature
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | GridView is a free, and open-source library for SwiftUI that makes creating grids easier and much cleaner.
60 | * **Improves code quality.** Create a grid using `GridView` constructor - simply pass your data and we'll deal with the rest. Simple as never!
61 | * **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.
62 |
63 |
64 |
65 | # Getting Started
66 | ### ✋ Requirements
67 |
68 | | **Platforms** | **Minimum Swift Version** |
69 | |:----------|:----------|
70 | | iOS 14+ | 5.0 |
71 |
72 | ### ⏳ Installation
73 |
74 | #### [Swift Package Manager][spm]
75 | Swift Package Manager is a tool for automating the distribution of Swift code and is integrated into the Swift compiler.
76 |
77 | Once you have your Swift package set up, adding GridView as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`.
78 |
79 | ```Swift
80 | dependencies: [
81 | .package(url: "https://github.com/Mijick/GridView", branch(“main”))
82 | ]
83 | ```
84 |
85 |
86 |
87 | # Usage
88 | ### 1. Call initialiser
89 | To declare a Grid for your data set, call the constructor:
90 |
91 | ```Swift
92 | struct ContentView: View {
93 | private let data = [SomeData]()
94 |
95 | var body: some View {
96 | GridView(data, id: \.self) { element in
97 | SomeItem(element: element)
98 | }
99 | }
100 | }
101 | ```
102 |
103 | ### 2. Customise Grid
104 | Your GridView can be customised by calling `configBuilder` inside the initialiser:
105 |
106 | ```Swift
107 | struct ContentView: View {
108 | private let data = [SomeData]()
109 |
110 | var body: some View {
111 | GridView(data, id: \.self, content: SomeItem.init, configBuilder: { $0
112 | .insertionPolicy(.fill)
113 | .columns(4)
114 | .verticalSpacing(12)
115 | })
116 | }
117 | }
118 | ```
119 |
120 |
121 | ### 3. Declare number of columns
122 | You can change the number of columns of an item by calling .columns of Item:
123 | ```Swift
124 | struct ContentView: View { ... }
125 | struct SomeItem: View {
126 | ...
127 |
128 | var body: some View {
129 | ...
130 | .columns(2)
131 | }
132 | }
133 | ```
134 |
135 |
136 |
137 |
138 | # Try our demo
139 | See for yourself how does it work by cloning [project][Demo] we created
140 |
141 | # License
142 | GridView is released under the MIT license. See [LICENSE][License] for details.
143 |
144 |
145 |
146 | # Our other open source SwiftUI libraries
147 | [PopupView] - The most powerful popup library that allows you to present any popup
148 |
149 | [NavigationView] - Easier and cleaner way of navigating through your app
150 |
151 | [CalendarView] - Create your own calendar object in no time
152 |
153 | [CameraView] - The most powerful CameraController. Designed for SwiftUI
154 |
155 | [Timer] - Modern API for Timer
156 |
157 |
158 | [MIT]: https://en.wikipedia.org/wiki/MIT_License
159 | [SPM]: https://www.swift.org/package-manager
160 |
161 | [Demo]: https://github.com/Mijick/GridView-Demo
162 | [License]: https://github.com/Mijick/GridView/blob/main/LICENSE
163 |
164 | [PopupView]: https://github.com/Mijick/PopupView
165 | [NavigationView]: https://github.com/Mijick/NavigationView
166 | [CalendarView]: https://github.com/Mijick/CalendarView
167 | [CameraView]: https://github.com/Mijick/CameraView
168 | [Timer]: https://github.com/Mijick/Timer
169 |
--------------------------------------------------------------------------------
/Sources/Internal/Extensions/Array++.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Array++.swift of MijickGridView
3 | //
4 | // Created by Tomasz Kurylik
5 | // - Twitter: https://twitter.com/tkurylik
6 | // - Mail: tomasz.kurylik@mijick.com
7 | // - GitHub: https://github.com/FulcrumOne
8 | //
9 | // Copyright ©2023 Mijick. Licensed under MIT License.
10 |
11 |
12 | import SwiftUI
13 |
14 | // MARK: - Removing Duplicates
15 | extension Array where Element: Hashable {
16 | func removingDuplicates() -> Self { Array(Set(self)) }
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/Internal/Extensions/Int++.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Int++.swift of MijickGridView
3 | //
4 | // Created by Tomasz Kurylik
5 | // - Twitter: https://twitter.com/tkurylik
6 | // - Mail: tomasz.kurylik@mijick.com
7 | // - GitHub: https://github.com/FulcrumOne
8 | //
9 | // Copyright ©2023 Mijick. Licensed under MIT License.
10 |
11 |
12 | import SwiftUI
13 |
14 | extension Int {
15 | func toDouble() -> Double { .init(self) }
16 | func toCGFloat() -> CGFloat { .init(self) }
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/Internal/Matrix/Matrix.Item.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Matrix.Item.swift of MijickGridView
3 | //
4 | // Created by Tomasz Kurylik
5 | // - Twitter: https://twitter.com/tkurylik
6 | // - Mail: tomasz.kurylik@mijick.com
7 | // - GitHub: https://github.com/FulcrumOne
8 | //
9 | // Copyright ©2023 Mijick. Licensed under MIT License.
10 |
11 |
12 | import SwiftUI
13 |
14 | extension Matrix { struct Item {
15 | var index: Int
16 | var value: CGFloat
17 | var columns: Int
18 | }}
19 | extension Matrix.Item: Hashable {
20 | func hash(into hasher: inout Hasher) { hasher.combine(index) }
21 | }
22 | extension Matrix.Item {
23 | var isEmpty: Bool { value == 0 }
24 | }
25 |
26 |
27 | // MARK: - Array
28 | extension [[Matrix.Item]] {
29 | subscript(_ position: Matrix.Position) -> Matrix.Item {
30 | get { self[position.row][position.column] }
31 | set { self[position.row][position.column] = newValue }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/Internal/Matrix/Matrix.Position.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Matrix.Position.swift of MijickGridView
3 | //
4 | // Created by Tomasz Kurylik
5 | // - Twitter: https://twitter.com/tkurylik
6 | // - Mail: tomasz.kurylik@mijick.com
7 | // - GitHub: https://github.com/FulcrumOne
8 | //
9 | // Copyright ©2023 Mijick. Licensed under MIT License.
10 |
11 |
12 | import SwiftUI
13 |
14 | extension Matrix { struct Position {
15 | let row: Int
16 | let column: Int
17 | }}
18 | extension Matrix.Position: Comparable {
19 | static func < (lhs: Matrix.Position, rhs: Matrix.Position) -> Bool {
20 | if lhs.row == rhs.row { return lhs.column < rhs.column }
21 | return lhs.row < rhs.row
22 | }
23 | }
24 |
25 | // MARK: - Creating Item Range
26 | extension Matrix.Position {
27 | func createItemRange(_ item: Matrix.Item) -> Matrix.Range { .init(from: self, to: withColumn(column + item.columns - 1)) }
28 | }
29 |
30 | // MARK: - Helpers
31 | extension Matrix.Position {
32 | func withRow(_ rowIndex: Int) -> Self { .init(row: rowIndex, column: column) }
33 | func withColumn(_ columnIndex: Int) -> Self { .init(row: row, column: columnIndex) }
34 |
35 | func nextRow() -> Self { withRow(row + 1) }
36 | func nextColumn() -> Self { withColumn(column + 1) }
37 |
38 | func previousRow() -> Self { withRow(row - 1) }
39 | func previousColumn() -> Self { withColumn(column - 1) }
40 | }
41 |
42 | // MARK: - Objects
43 | extension Matrix.Position {
44 | static var zero: Self { .init(row: 0, column: 0) }
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/Internal/Matrix/Matrix.Range.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Matrix.Range.swift of MijickGridView
3 | //
4 | // Created by Tomasz Kurylik
5 | // - Twitter: https://twitter.com/tkurylik
6 | // - Mail: tomasz.kurylik@mijick.com
7 | // - GitHub: https://github.com/FulcrumOne
8 | //
9 | // Copyright ©2023 Mijick. Licensed under MIT License.
10 |
11 |
12 | import SwiftUI
13 |
14 | extension Matrix { struct Range {
15 | var start: Position
16 | var end: Position
17 |
18 | init(from startPosition: Matrix.Position, to endPosition: Matrix.Position) {
19 | if startPosition > endPosition { fatalError("Start position must precede end position") }
20 |
21 | start = startPosition
22 | end = endPosition
23 | }
24 | }}
25 |
26 | extension Matrix.Range {
27 | func updating(newStart: Matrix.Position) -> Matrix.Range {
28 | let endIndexOffset = newStart.column + columns.count - 1
29 |
30 | var range = self
31 | range.start = newStart
32 | range.end = newStart.withColumn(endIndexOffset)
33 | return range
34 | }
35 | }
36 |
37 | // MARK: - Helpers
38 | extension Matrix.Range {
39 | var rows: ClosedRange { start.row...end.row }
40 | var columns: ClosedRange { start.column...end.column }
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/Internal/Matrix/Matrix.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Matrix.swift of MijickGridView
3 | //
4 | // Created by Tomasz Kurylik
5 | // - Twitter: https://twitter.com/tkurylik
6 | // - Mail: tomasz.kurylik@mijick.com
7 | // - GitHub: https://github.com/FulcrumOne
8 | //
9 | // Copyright ©2023 Mijick. Licensed under MIT License.
10 |
11 |
12 | import SwiftUI
13 |
14 | struct Matrix {
15 | let config: GridView.Config
16 | private var items: [[Item]] = []
17 | private var matrixInitialised: Bool = false
18 |
19 |
20 | init(_ config: GridView.Config) { self.items = .init(numberOfColumns: config.numberOfColumns); self.config = config }
21 | }
22 |
23 | // MARK: - Inserting Items
24 | extension Matrix {
25 | mutating func insert(_ item: Item, isLast: Bool) { if !matrixInitialised {
26 | let position = findPositionForItem(item)
27 | let range = position.createItemRange(item)
28 |
29 | checkItem(item)
30 | addNewRowIfNeeded(position)
31 | insertItem(item, range)
32 | sortMatrix(isLast)
33 | }}
34 | }
35 |
36 | // MARK: Checking Item
37 | private extension Matrix {
38 | func checkItem(_ item: Item) {
39 | if item.columns > config.numberOfColumns { fatalError("Single element cannot have more columns than the entire view.") }
40 | }
41 | }
42 |
43 | // MARK: Finding Position
44 | private extension Matrix {
45 | func findPositionForItem(_ item: Item) -> Position {
46 | guard item.index > 0 else { return .zero }
47 |
48 | let previousItemRangeEnd = getRange(for: item.index - 1).end
49 | return canInsertItemInRow(item, previousItemRangeEnd) ?
50 | previousItemRangeEnd.nextColumn() : previousItemRangeEnd.nextRow().withColumn(0)
51 | }
52 | }
53 | private extension Matrix {
54 | func canInsertItemInRow(_ item: Item, _ previousItemRangeEnd: Position) -> Bool { item.columns + previousItemRangeEnd.column < numberOfColumns }
55 | }
56 |
57 | // MARK: Inserting New Row
58 | private extension Matrix {
59 | mutating func addNewRowIfNeeded(_ position: Position) { if position.row >= items.count {
60 | items.insertEmptyRow(numberOfColumns: numberOfColumns)
61 | }}
62 | }
63 |
64 | // MARK: Filling Array
65 | private extension Matrix {
66 | mutating func insertItem(_ item: Item, _ range: Range, _ isLast: Bool = false) {
67 | let columnHeight = getHeights()
68 | let range = recalculateRange(range, columnHeight, isLast)
69 |
70 | fillInMatrixWithEmptySpaces(range, columnHeight)
71 | fillInMatrixWithItem(item, range)
72 | }
73 | }
74 | private extension Matrix {
75 | func recalculateRange(_ range: Range, _ columnHeights: [[CGFloat]], _ isLast: Bool) -> Range {
76 | guard isLast, range.end.row > 0 else { return range }
77 |
78 | let columnsRangeStart = range.columns.lowerBound,
79 | columnsRangeEnd = config.numberOfColumns - range.columns.count + 1,
80 | columnsRange = columnsRangeStart.. 0 {
91 | range.columns.forEach { column in
92 | let currentPosition = range.start.withColumn(column), previousPosition = currentPosition.previousRow()
93 | let difference = columnHeights[currentPosition] - columnHeights[previousPosition]
94 |
95 | if shouldFillInEmptySpace(difference, items[previousPosition]) {
96 | items[previousPosition] = .init(index: -1, value: difference, columns: 1)
97 | }
98 | }
99 | }}
100 | mutating func fillInMatrixWithItem(_ item: Item, _ range: Range) { range.columns.forEach { column in
101 | items[range.start.withColumn(column)] = item
102 | }}
103 | }
104 | private extension Matrix {
105 | func shouldFillInEmptySpace(_ difference: CGFloat, _ item: Item) -> Bool { difference > 0 && item.isEmpty }
106 | }
107 |
108 | // MARK: Sorting Matrix
109 | private extension Matrix {
110 | mutating func sortMatrix(_ isLastItem: Bool) { if policy == .fill && isLastItem {
111 | let items = getUniqueSortedItems()
112 | let proposedSortedMatrix = getProposedSortedMatrix(items)
113 |
114 | eraseTemporaryMatrix()
115 | insertSortedMatrix(proposedSortedMatrix)
116 | setMatrixAsInitialised()
117 | }}
118 | }
119 | private extension Matrix {
120 | func getUniqueSortedItems() -> [Item] { items
121 | .flatMap { $0 }
122 | .filter { $0.index != -1 }
123 | .removingDuplicates()
124 | .sorted(by: { $0.index < $1.index })
125 | .sorted(by: { $0.columns > $1.columns })
126 | }
127 | func getProposedSortedMatrix(_ items: [Item]) -> [[Item]] {
128 | var array: [[Item]] = []
129 |
130 | for item in items where !array.contains(item) {
131 | let bestRow = getBestRow(array, items, item)
132 | array.append(bestRow)
133 | }
134 |
135 | return array
136 | }
137 | mutating func eraseTemporaryMatrix() {
138 | items = .init(numberOfColumns: numberOfColumns)
139 | }
140 | mutating func insertSortedMatrix(_ proposedSortedMatrix: [[Item]]) {
141 | for row in 0.. [Matrix.Item] {
161 | var proposedRows = [[item1]]
162 |
163 | for item2 in getRemainingItems(results, items, item1) {
164 | switch proposedRows.lastItem.columns + item2.columns <= numberOfColumns {
165 | case true: proposedRows.lastItem.append(item2)
166 | case false: proposedRows.append([])
167 | }
168 | }
169 |
170 | return proposedRows.pickingBest()
171 | }
172 | }
173 | private extension Matrix {
174 | func getRemainingItems(_ results: [[Item]], _ items: [Item], _ item1: Item) -> [Item] { items
175 | .filter { $0.columns + item1.columns <= numberOfColumns }
176 | .filter { !results.contains($0) }
177 | .filter { item1 != $0 }
178 | }
179 | }
180 |
181 | // MARK: - Getting Item Position
182 | extension Matrix {
183 | func getRange(for index: Int) -> Range {
184 | let startPosition = getStartPosition(for: index)
185 | return startPosition.createItemRange(items[startPosition])
186 | }
187 | }
188 | private extension Matrix {
189 | func getStartPosition(for index: Int) -> Position {
190 | let rowIndex = items.firstIndex(where: { $0.contains(where: { $0.index == index }) }) ?? 0
191 | let columnIndex = items[rowIndex].firstIndex(where: { $0.index == index }) ?? 0
192 | return .init(row: rowIndex, column: columnIndex)
193 | }
194 | }
195 |
196 | // MARK: - Getting Column Heights
197 | extension Matrix {
198 | func getHeights() -> [[CGFloat]] {
199 | var array: [[CGFloat]] = .init(repeating: .init(repeating: 0, count: numberOfColumns), count: items.count)
200 |
201 | for row in 0.. 0 ? array[position.previousRow()] : 0
219 |
220 | array[position] = currentValue + previousRowPositionValue
221 | }
222 | func updateValuesForMultigridItem(_ position: Position, _ item: Item, _ array: inout [[CGFloat]]) { if item.columns > 1 {
223 | let range = getStartPosition(for: item.index).createItemRange(item)
224 |
225 | guard range.end.column == position.column else { return }
226 |
227 | let maxValue = array[position.row][range.columns].max() ?? 0
228 | range.columns.forEach { array[position.row][$0] = maxValue }
229 | }}
230 | }
231 |
232 | // MARK: - Others
233 | extension Matrix {
234 | var itemsSpacing: CGFloat { config.spacing.vertical }
235 | var policy: InsertionPolicy { config.insertionPolicy }
236 | var numberOfColumns: Int { config.numberOfColumns }
237 | }
238 |
239 |
240 | // MARK: - Helpers
241 | fileprivate extension [[Matrix.Item]] {
242 | init(numberOfColumns: Int) { self = [.empty(numberOfColumns)] }
243 | mutating func insertEmptyRow(numberOfColumns: Int) { append(.empty(numberOfColumns)) }
244 | }
245 | fileprivate extension [[Matrix.Item]] {
246 | func contains(_ element: Matrix.Item) -> Bool { joined().contains(where: { $0.index == element.index }) }
247 | func pickingBest() -> [Matrix.Item] { self.min(by: { $0.heightsDifference < $1.heightsDifference }) ?? [] }
248 | }
249 | fileprivate extension [[Matrix.Item]] {
250 | var lastItem: [Matrix.Item] {
251 | get { last ?? [] }
252 | set { self[count - 1] = newValue }
253 | }
254 | }
255 |
256 | fileprivate extension [Matrix.Item] {
257 | static func empty(_ numberOfColumns: Int) -> Self { .init(repeating: .init(index: -1, value: 0, columns: 1), count: numberOfColumns) }
258 | }
259 | fileprivate extension [Matrix.Item] {
260 | var heightsDifference: CGFloat {
261 | let min = self.min(by: { $0.value < $1.value })?.value ?? 0
262 | let max = self.max(by: { $0.value > $1.value })?.value ?? 0
263 |
264 | return max - min
265 | }
266 | var columns: Int { reduce(0, { $0 + $1.columns }) }
267 | }
268 |
269 | fileprivate extension [[CGFloat]] {
270 | subscript(position: Matrix.Position) -> CGFloat {
271 | get { self[position.row][position.column] }
272 | set { self[position.row][position.column] = newValue }
273 | }
274 | }
275 |
--------------------------------------------------------------------------------
/Sources/Internal/Other/AnyGridElement.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnyGridElement.swift of MijickGridView
3 | //
4 | // Created by Tomasz Kurylik
5 | // - Twitter: https://twitter.com/tkurylik
6 | // - Mail: tomasz.kurylik@mijick.com
7 | // - GitHub: https://github.com/FulcrumOne
8 | //
9 | // Copyright ©2023 Mijick. Licensed under MIT License.
10 |
11 |
12 | import SwiftUI
13 |
14 | struct AnyGridElement: GridElement {
15 | @State var height: CGFloat? = nil
16 | let columns: Int
17 | private let _body: AnyView
18 |
19 |
20 | var body: some View { _body }
21 | init(_ element: some View, numberOfColumns: Int? = nil) {
22 | self.columns = Self.getNumberOfColumns(element, numberOfColumns)
23 | self._body = AnyView(element)
24 | }
25 | }
26 | private extension AnyGridElement {
27 | static func getNumberOfColumns(_ element: some View, _ numberOfColumns: Int?) -> Int {
28 | if let element = element as? any GridElement { return element.columns }
29 | return numberOfColumns ?? 1
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/Internal/Protocols/Configurable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Configurable.swift of MijickGridView
3 | //
4 | // Created by Tomasz Kurylik
5 | // - Twitter: https://twitter.com/tkurylik
6 | // - Mail: tomasz.kurylik@mijick.com
7 | // - GitHub: https://github.com/FulcrumOne
8 | //
9 | // Copyright ©2023 Mijick. Licensed under MIT License.
10 |
11 |
12 | public protocol Configurable { init() }
13 | extension Configurable {
14 | func changing(path: WritableKeyPath, to value: T) -> Self {
15 | var clone = self
16 | clone[keyPath: path] = value
17 | return clone
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/Internal/View Modifiers/HeightReader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HeightReader.swift of MijickGridView
3 | //
4 | // Created by Tomasz Kurylik
5 | // - Twitter: https://twitter.com/tkurylik
6 | // - Mail: tomasz.kurylik@mijick.com
7 | // - GitHub: https://github.com/FulcrumOne
8 | //
9 | // Copyright ©2023 Mijick. Licensed under MIT License.
10 |
11 |
12 | import SwiftUI
13 |
14 | extension View {
15 | func readHeight(onChange action: @escaping (CGFloat) -> ()) -> some View { modifier(Modifier(onHeightChange: action)) }
16 | }
17 |
18 | // MARK: - Implementation
19 | fileprivate struct Modifier: ViewModifier {
20 | let onHeightChange: (CGFloat) -> ()
21 |
22 | func body(content: Content) -> some View { content
23 | .background(
24 | GeometryReader { geo -> Color in
25 | DispatchQueue.main.async { onHeightChange(geo.size.height) }
26 | return Color.clear
27 | }
28 | )
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/Internal/Views/GridView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GridView.swift of MijickGridView
3 | //
4 | // Created by Tomasz Kurylik
5 | // - Twitter: https://twitter.com/tkurylik
6 | // - Mail: tomasz.kurylik@mijick.com
7 | // - GitHub: https://github.com/FulcrumOne
8 | //
9 | // Copyright ©2023 Mijick. Licensed under MIT License.
10 |
11 |
12 | import SwiftUI
13 |
14 | public struct GridView: View {
15 | @State var matrix: Matrix
16 | var elements: [AnyGridElement]
17 | var config: Config
18 |
19 |
20 | public var body: some View {
21 | GeometryReader { reader in
22 | ZStack(alignment: .topLeading) {
23 | ForEach(0.. some View {
31 | elements[index]
32 | .readHeight { saveHeight($0, index) }
33 | .fixedSize(horizontal: false, vertical: true)
34 | .alignmentGuide(.top) { handleTopAlignmentGuide(index, $0, reader) }
35 | .alignmentGuide(.leading) { handleLeadingAlignmentGuide(index, $0, reader) }
36 | .frame(width: calculateItemWidth(index, reader.size.width), height: elements[index].height)
37 | }
38 | }
39 |
40 | // MARK: - Reading Height
41 | private extension GridView {
42 | func saveHeight(_ value: CGFloat, _ index: Int) {
43 | elements[index].height = value
44 | }
45 | }
46 |
47 | // MARK: - Vertical Alignment
48 | private extension GridView {
49 | func handleTopAlignmentGuide(_ index: Int, _ dimensions: ViewDimensions, _ reader: GeometryProxy) -> CGFloat {
50 | insertItem(index, dimensions.height)
51 |
52 | let topPadding = getTopPaddingValue(index)
53 | return topPadding
54 | }
55 | }
56 | private extension GridView {
57 | func insertItem(_ index: Int, _ value: CGFloat) { DispatchQueue.main.async {
58 | let item = Matrix.Item(index: index, value: value, columns: elements[index].columns)
59 | matrix.insert(item, isLast: index == elements.count - 1)
60 | }}
61 | func getTopPaddingValue(_ index: Int) -> CGFloat {
62 | let range = matrix.getRange(for: index)
63 |
64 | guard range.start.row > 0 else { return 0 }
65 |
66 | let heights = matrix.getHeights()[range.start.row - 1]
67 | let itemTopPadding = heights[range.columns].max() ?? 0
68 | return -itemTopPadding
69 | }
70 | }
71 |
72 | // MARK: - Horizontal Alignment
73 | private extension GridView {
74 | func handleLeadingAlignmentGuide(_ index: Int, _ dimensions: ViewDimensions, _ reader: GeometryProxy) -> CGFloat {
75 | let availableWidth = reader.size.width
76 | let itemPadding = calculateItemLeadingPadding(index, availableWidth)
77 | return itemPadding
78 | }
79 | }
80 | private extension GridView {
81 | func calculateItemLeadingPadding(_ index: Int, _ availableWidth: CGFloat) -> CGFloat {
82 | let columnNumber = matrix.getRange(for: index).start.column
83 | let singleColumnWidth = calculateSingleColumnWidth(availableWidth)
84 |
85 | let rawColumnsPaddingValue = singleColumnWidth * columnNumber.toCGFloat()
86 | let rawSpacingPaddingValue = (columnNumber.toCGFloat() - 1) * config.spacing.horizontal
87 |
88 | let rawPaddingValue = rawColumnsPaddingValue + rawSpacingPaddingValue
89 | return -rawPaddingValue
90 | }
91 | }
92 |
93 | // MARK: - Dimensions
94 | private extension GridView {
95 | func calculateItemWidth(_ index: Int, _ availableWidth: CGFloat) -> CGFloat {
96 | let itemColumns = elements[index].columns
97 | let singleColumnWidth = calculateSingleColumnWidth(availableWidth)
98 |
99 | let fixedItemWidth = singleColumnWidth * itemColumns.toCGFloat()
100 | let additionalHorizontalSpacing = (itemColumns.toCGFloat() - 1) * config.spacing.horizontal
101 | return fixedItemWidth + additionalHorizontalSpacing
102 | }
103 | func calculateContentHeight() -> CGFloat { matrix.getHeights().flatMap { $0 }.max() ?? 0 }
104 | }
105 | private extension GridView {
106 | func calculateSingleColumnWidth(_ availableWidth: CGFloat) -> CGFloat {
107 | let totalSpacingValue = getHorizontalSpacingTotalValue()
108 | let itemsWidth = availableWidth - totalSpacingValue
109 | return itemsWidth / matrix.numberOfColumns.toCGFloat()
110 | }
111 | func getHorizontalSpacingTotalValue() -> CGFloat {
112 | let numberOfSpaces = matrix.numberOfColumns - 1
113 | return numberOfSpaces.toCGFloat() * config.spacing.horizontal
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/Sources/Public/Configurables/Public+GridViewConfig.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Public+GridViewConfig.swift of MijickGridView
3 | //
4 | // Created by Tomasz Kurylik
5 | // - Twitter: https://twitter.com/tkurylik
6 | // - Mail: tomasz.kurylik@mijick.com
7 | // - GitHub: https://github.com/FulcrumOne
8 | //
9 | // Copyright ©2023 Mijick. Licensed under MIT License.
10 |
11 |
12 | import SwiftUI
13 |
14 | // MARK: - Policies
15 | public extension GridView.Config {
16 | func insertionPolicy(_ value: InsertionPolicy) -> Self { changing(path: \.insertionPolicy, to: value) }
17 | }
18 |
19 | // MARK: - Composition
20 | public extension GridView.Config {
21 | func columns(_ value: Int) -> Self { changing(path: \.numberOfColumns, to: value) }
22 | func verticalSpacing(_ value: CGFloat) -> Self { changing(path: \.spacing.vertical, to: value) }
23 | func horizontalSpacing(_ value: CGFloat) -> Self { changing(path: \.spacing.horizontal, to: value) }
24 | }
25 |
26 |
27 | // MARK: - Internal
28 | extension GridView { public struct Config: Configurable { public init() {}
29 | private(set) var insertionPolicy: InsertionPolicy = .ordered
30 |
31 | private(set) var numberOfColumns: Int = 2
32 | private(set) var spacing: (vertical: CGFloat, horizontal: CGFloat) = (8, 8)
33 | }}
34 |
--------------------------------------------------------------------------------
/Sources/Public/Enumerations/Public+InsertionPolicy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Public+InsertionPolicy.swift of MijickGridView
3 | //
4 | // Created by Tomasz Kurylik
5 | // - Twitter: https://twitter.com/tkurylik
6 | // - Mail: tomasz.kurylik@mijick.com
7 | // - GitHub: https://github.com/FulcrumOne
8 | //
9 | // Copyright ©2023 Mijick. Licensed under MIT License.
10 |
11 |
12 | public enum InsertionPolicy { case ordered, fill }
13 |
--------------------------------------------------------------------------------
/Sources/Public/Extensions/Public+GridView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Public+GridView.swift of MijickGridView
3 | //
4 | // Created by Tomasz Kurylik
5 | // - Twitter: https://twitter.com/tkurylik
6 | // - Mail: tomasz.kurylik@mijick.com
7 | // - GitHub: https://github.com/FulcrumOne
8 | //
9 | // Copyright ©2023 Mijick. Licensed under MIT License.
10 |
11 |
12 | import SwiftUI
13 |
14 | // MARK: - Initialisers
15 | extension GridView {
16 | public init(_ data: Data, id: KeyPath, @ViewBuilder content: @escaping (Data.Element) -> any View, configBuilder: (Config) -> Config = { $0 }) { self.init(
17 | matrix: .init(configBuilder(.init())),
18 | elements: data.map { .init(content($0)) },
19 | config: configBuilder(.init())
20 | )}
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/Public/Extensions/Public+View.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Public+View.swift of MijickGridView
3 | //
4 | // Created by Tomasz Kurylik
5 | // - Twitter: https://twitter.com/tkurylik
6 | // - Mail: tomasz.kurylik@mijick.com
7 | // - GitHub: https://github.com/FulcrumOne
8 | //
9 | // Copyright ©2023 Mijick. Licensed under MIT License.
10 |
11 |
12 | import SwiftUI
13 |
14 | public extension View {
15 | func columns(_ value: Int) -> some GridElement { AnyGridElement(self, numberOfColumns: value) }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/Public/Protocols/Public+GridElement.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Public+GridElement.swift of MijickGridView
3 | //
4 | // Created by Tomasz Kurylik
5 | // - Twitter: https://twitter.com/tkurylik
6 | // - Mail: tomasz.kurylik@mijick.com
7 | // - GitHub: https://github.com/FulcrumOne
8 | //
9 | // Copyright ©2023 Mijick. Licensed under MIT License.
10 |
11 |
12 | import SwiftUI
13 |
14 | public protocol GridElement: View {
15 | var columns: Int { get }
16 | }
17 |
--------------------------------------------------------------------------------
/Tests/Cases/Matrix_Test.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Matrix_Test.swift of MijickGridView
3 | //
4 | // Created by Tomasz Kurylik
5 | // - Twitter: https://twitter.com/tkurylik
6 | // - Mail: tomasz.kurylik@mijick.com
7 | // - GitHub: https://github.com/FulcrumOne
8 | //
9 | // Copyright ©2023 Mijick. Licensed under MIT License.
10 |
11 |
12 | import XCTest
13 | @testable import GridView
14 |
15 | final class Matrix_Test: XCTestCase {
16 | var matrix: Matrix = .init(columns: 4, itemsSpacing: 8, policy: .fill)
17 | }
18 |
19 | // MARK: - Inserting
20 | extension Matrix_Test {
21 | func testInsertValue_WhenMatrixIsEmpty() {
22 | matrix.insert(.init(index: 0, value: 100))
23 |
24 | let result: [[CGFloat]] = matrix.items.map { $0.map(\.value) }
25 | let expectedResult: [[CGFloat]] = [[100, 0, 0, 0]]
26 |
27 | XCTAssertEqual(result, expectedResult)
28 | }
29 | func testInsertValue_WhenItemWithIndexIsPresent_ShouldNotInsertItem() {
30 | matrix.insert(.init(index: 0, value: 100))
31 | matrix.insert(.init(index: 0, value: 200))
32 |
33 | let result = matrix.items.flatMap { $0 }.filter { !$0.isEmpty }.count
34 | let expectedResult = 1
35 |
36 | XCTAssertEqual(result, expectedResult)
37 | }
38 | func testInsertValue_InsertManyItemsOfDifferentIndexes_ShouldInsertItems() {
39 | for index in 0..<5 { matrix.insert(.init(index: index, value: 100)) }
40 |
41 | let result = matrix.items.flatMap { $0 }.filter { !$0.isEmpty }.count
42 | let expectedResult = 5
43 |
44 | XCTAssertEqual(result, expectedResult)
45 | }
46 | func testInsertValue_InsertManyItemsOfDifferentIndexes_ShouldAddNewRow() {
47 | for index in 0..<5 { matrix.insert(.init(index: index, value: 100)) }
48 |
49 | let result = matrix.items.count
50 | let expectedResult = 2
51 |
52 | XCTAssertEqual(result, expectedResult)
53 | }
54 | func testInsertValue_WhenMatrixHasOneRowFilled_OrderedPolicy_ShouldMatchPattern() {
55 | matrix = .init(columns: 4, itemsSpacing: 8, policy: .ordered)
56 | for index in 0..<10 { matrix.insert(.init(index: index, value: entryValues[index])) }
57 |
58 | let result: [[CGFloat]] = matrix.items.map { $0.map { $0.value } }
59 | let expectedResult: [[CGFloat]] = [
60 | [100, 200, 50, 100],
61 | [150, 100, 50, 100],
62 | [100, 150, 0, 0]
63 | ]
64 |
65 | XCTAssertEqual(result, expectedResult)
66 | }
67 | func testInsertValue_WhenMatrixHasOneRowFilled_FillPolicy_ShouldMatchPattern() {
68 | for index in 0..<10 { matrix.insert(.init(index: index, value: entryValues[index])) }
69 |
70 | let result: [[CGFloat]] = matrix.items.map { $0.map { $0.value } }
71 | let expectedResult: [[CGFloat]] = [
72 | [100, 200, 50, 100],
73 | [100, 150, 150, 50],
74 | [100, 0, 0, 100]
75 | ]
76 |
77 | XCTAssertEqual(result, expectedResult)
78 | }
79 | }
80 |
81 | private extension Matrix_Test {
82 | var entryValues: [CGFloat] { [100, 200, 50, 100, 150, 100, 50, 100, 100, 150] }
83 | }
84 |
--------------------------------------------------------------------------------
/Tests/Helpers/Array++.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Array++.swift of MijickGridView
3 | //
4 | // Created by Tomasz Kurylik
5 | // - Twitter: https://twitter.com/tkurylik
6 | // - Mail: tomasz.kurylik@mijick.com
7 | // - GitHub: https://github.com/FulcrumOne
8 | //
9 | // Copyright ©2023 Mijick. Licensed under MIT License.
10 |
11 |
12 | import SwiftUI
13 |
14 | extension [[CGFloat]] {
15 | func toString() -> String {
16 | var text = ""
17 |
18 | for row in 0..