Created
August 22, 2021 14:14
-
-
Save ANSCoder/8a4466235bdcae47876748bff52f686d to your computer and use it in GitHub Desktop.
Grid View in SwiftUI (iOS - 13)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// GridStack.swift | |
// Grocers | |
// | |
// Created by Anand Nimje on 27/08/20. | |
// Copyright © 2020 Anscoder. All rights reserved. | |
// | |
import SwiftUI | |
/// A container that presents rows of data arranged in multiple columns. | |
@available(iOS 13.0, OSX 10.15, *) | |
public struct QGrid<Data, Content>: View | |
where Data : RandomAccessCollection, Content : View, Data.Element : Identifiable { | |
private struct QGridIndex : Identifiable { var id: Int } | |
// MARK: - STORED PROPERTIES | |
private let columns: Int | |
private let columnsInLandscape: Int | |
private let vSpacing: CGFloat | |
private let hSpacing: CGFloat | |
private let vPadding: CGFloat | |
private let hPadding: CGFloat | |
private let isScrollable: Bool | |
private let showScrollIndicators: Bool | |
private let data: [Data.Element] | |
private let content: (Data.Element) -> Content | |
// MARK: - INITIALIZERS | |
/// Creates a QGrid that computes its cells from an underlying collection of identified data. | |
/// | |
/// - Parameters: | |
/// - data: A collection of identified data. | |
/// - columns: Target number of columns for this grid, in Portrait device orientation | |
/// - columnsInLandscape: Target number of columns for this grid, in Landscape device orientation; If not provided, `columns` value will be used. | |
/// - vSpacing: Vertical spacing: The distance between each row in grid. If not provided, the default value will be used. | |
/// - hSpacing: Horizontal spacing: The distance between each cell in grid's row. If not provided, the default value will be used. | |
/// - vPadding: Vertical padding: The distance between top/bottom edge of the grid and the parent view. If not provided, the default value will be used. | |
/// - hPadding: Horizontal padding: The distance between leading/trailing edge of the grid and the parent view. If not provided, the default value will be used. | |
/// - isScrollable: Boolean that determines whether or not the grid should scroll | |
/// - content: A closure returning the content of the individual cell | |
public init(_ data: Data, | |
columns: Int, | |
columnsInLandscape: Int? = nil, | |
vSpacing: CGFloat = 10, | |
hSpacing: CGFloat = 10, | |
vPadding: CGFloat = 10, | |
hPadding: CGFloat = 10, | |
isScrollable: Bool = true, | |
showScrollIndicators: Bool = false, | |
content: @escaping (Data.Element) -> Content) { | |
self.data = data.map { $0 } | |
self.content = content | |
self.columns = max(1, columns) | |
self.columnsInLandscape = columnsInLandscape ?? max(1, columns) | |
self.vSpacing = vSpacing | |
self.hSpacing = hSpacing | |
self.vPadding = vPadding | |
self.hPadding = hPadding | |
self.isScrollable = isScrollable | |
self.showScrollIndicators = showScrollIndicators | |
} | |
// MARK: - COMPUTED PROPERTIES | |
private var rows: Int { | |
data.count / self.cols | |
} | |
private var cols: Int { | |
#if os(tvOS) | |
return columnsInLandscape | |
#elseif os(macOS) | |
return columnsInLandscape | |
#else | |
return UIDevice.current.orientation.isLandscape ? columnsInLandscape : columns | |
#endif | |
} | |
/// Declares the content and behavior of this view. | |
public var body : some View { | |
GeometryReader { geometry in | |
Group { | |
if !self.data.isEmpty { | |
if self.isScrollable { | |
ScrollView(showsIndicators: self.showScrollIndicators) { | |
self.content(using: geometry) | |
} | |
} else { | |
self.content(using: geometry) | |
} | |
} | |
} | |
.padding(.horizontal, self.hPadding) | |
.padding(.vertical, self.vPadding) | |
} | |
} | |
// MARK: - `BODY BUILDER` 💪 FUNCTIONS | |
private func rowAtIndex(_ index: Int, | |
geometry: GeometryProxy, | |
isLastRow: Bool = false) -> some View { | |
HStack(spacing: self.hSpacing) { | |
ForEach((0..<(isLastRow ? data.count % cols : cols)) | |
.map { QGridIndex(id: $0) }) { column in | |
self.content(self.data[index + column.id]) | |
.frame(width: self.contentWidthFor(geometry)) | |
} | |
if isLastRow { Spacer() } | |
} | |
} | |
private func content(using geometry: GeometryProxy) -> some View { | |
VStack(spacing: self.vSpacing) { | |
ForEach((0..<self.rows).map { QGridIndex(id: $0) }) { row in | |
self.rowAtIndex(row.id * self.cols, | |
geometry: geometry) | |
} | |
// Handle last row | |
if (self.data.count % self.cols > 0) { | |
self.rowAtIndex(self.cols * self.rows, | |
geometry: geometry, | |
isLastRow: true) | |
} | |
} | |
} | |
// MARK: - HELPER FUNCTIONS | |
private func contentWidthFor(_ geometry: GeometryProxy) -> CGFloat { | |
let hSpacings = hSpacing * (CGFloat(self.cols) - 1) | |
let width = geometry.size.width - hSpacings - hPadding * 2 | |
return width / CGFloat(self.cols) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uses this code inside your code