(_ file: String) -> T {
23 | guard let url = Bundle.main.url(forResource: file, withExtension: nil),
24 | let data = try? Data(contentsOf: url),
25 | let typedData = try? JSONDecoder().decode(T.self, from: data) else {
26 | fatalError("Error while loading data from file: \(file)")
27 | }
28 | return typedData;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/QGridTestApp/Screenshots/People1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Q-Mobile/QGrid/8565654a123a0fa5e7969443cc2b13512c6cea33/QGridTestApp/Screenshots/People1.png
--------------------------------------------------------------------------------
/QGridTestApp/Screenshots/People2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Q-Mobile/QGrid/8565654a123a0fa5e7969443cc2b13512c6cea33/QGridTestApp/Screenshots/People2.png
--------------------------------------------------------------------------------
/QGridTestApp/Screenshots/People3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Q-Mobile/QGrid/8565654a123a0fa5e7969443cc2b13512c6cea33/QGridTestApp/Screenshots/People3.png
--------------------------------------------------------------------------------
/QGridTestApp/Screenshots/People4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Q-Mobile/QGrid/8565654a123a0fa5e7969443cc2b13512c6cea33/QGridTestApp/Screenshots/People4.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | **[NOTE]**
3 | If you'd like to see `QGrid` in action, check out this demo of `QDesigner` (see video below).
4 | Install `QDesigner`: [https://apps.apple.com/us/app/qdesigner/id1500810470](https://apps.apple.com/us/app/qdesigner/id1500810470)
5 | Install a companion `QDesigner Client` on iPhone, to see your UI design on a target device, updated in real-time:
6 | [https://apps.apple.com/us/app/qdesignerclient/id1500946484](https://apps.apple.com/us/app/qdesignerclient/id1500946484)
7 |
8 | Learn more at: [https://Q-Mobile.IT/Q-Designer](https://Q-Mobile.IT/Q-Designer)
9 |
10 | [](https://youtu.be/_nCM9O-v7mQ)
11 |
12 | 
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | `QGrid` is the missing SwiftUI collection view. It uses the same approach as SwiftUI's `List` view, by computing its cells from an underlying collection of identified data.
24 |
25 |
26 |
27 |
28 |
29 | ## 🔷 Requirements
30 |
31 | ✅ macOS 10.15+
32 | ✅ Xcode 11.0
33 | ✅ Swift 5+
34 | ✅ iOS 13+
35 | ✅ tvOS 13+
36 |
37 | ## 🔷 Installation
38 |
39 | `QGrid` is available via [Swift Package Manager](https://swift.org/package-manager).
40 |
41 | Using Xcode 11, go to `File -> Swift Packages -> Add Package Dependency` and enter [https://github.com/Q-Mobile/QGrid](https://github.com/Q-Mobile/QGrid)
42 |
43 | ## 🔷 Usage
44 |
45 | #### ✴️ Basic scenario:
46 |
47 | In its simplest form, `QGrid` can be used with just this 1 line of code within the `body` of your View, assuming you already have a custom cell view:
48 |
49 | ```Swift
50 | struct PeopleView: View {
51 | var body: some View {
52 | QGrid(Storage.people, columns: 3) { GridCell(person: $0) }
53 | }
54 | }
55 |
56 | struct GridCell: View {
57 | var person: Person
58 |
59 | var body: some View {
60 | VStack() {
61 | Image(person.imageName)
62 | .resizable()
63 | .scaledToFit()
64 | .clipShape(Circle())
65 | .shadow(color: .primary, radius: 5)
66 | .padding([.horizontal, .top], 7)
67 | Text(person.firstName).lineLimit(1)
68 | Text(person.lastName).lineLimit(1)
69 | }
70 | }
71 | }
72 | ```
73 |
74 | #### ✴️ Customize the default layout configuration:
75 |
76 | You can customize how `QGrid` will layout your cells by providing some additional initializer parameters, which have default values:
77 |
78 | ```swift
79 | struct PeopleView: View {
80 | var body: some View {
81 | QGrid(Storage.people,
82 | columns: 3,
83 | columnsInLandscape: 4,
84 | vSpacing: 50,
85 | hSpacing: 20,
86 | vPadding: 100,
87 | hPadding: 20) { person in
88 | GridCell(person: person)
89 | }
90 | }
91 | }
92 | ```
93 |
94 | ## 🔷 Example App
95 |
96 | 📱`QGridTestApp` directory in this repository contains a very simple application that uses `QGrid`. Open `QGridTestApp/QGridTestApp.xcodeproj` and either use the new Xcode Previews feature or just run the app.
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | |
105 |
106 |
107 | |
108 |
109 |
110 |
111 |
112 |
113 |
114 | |
115 |
116 |
117 |
118 |
119 | |
120 |
121 |
122 |
123 |
124 | ## 🔷 QGrid Designer
125 |
126 | 📱`QGridTestApp` contains also the QGrid Designer area view, with sliders for dynamic run-time configuration of the QGrid view (with config option to hide it). Please refer to the following demo executed on the device:
127 |
128 |
129 |
130 |
131 |
132 | ## 🔷 Roadmap / TODOs
133 |
134 | Version `0.1.1` of `QGrid ` contains a very limited set of features. It could be extended by implementing the following tasks:
135 |
136 | ☘️ Parameterize spacing&padding configuration depending on the device orientation
137 | ☘️ Add the option to specify scroll direction
138 | ☘️ Add content-only initializer to QGrid struct, without a collection of identified data as argument (As in SwiftUI’s `List`)
139 | ☘️ Add support for other platforms (watchOS)
140 | ☘️ Add `Stack` layout option (as in `UICollectionView`)
141 | ☘️ Add unit/UI tests
142 | ☘️ ... many other improvements
143 |
144 | ## 🔷 Contributing
145 |
146 | 👨🏻🔧 Feel free to contribute to `QGrid ` by creating a pull request, following these guidelines:
147 |
148 | 1. Fork `QGrid `
149 | 2. Create your feature branch
150 | 3. Commit your changes, along with unit tests
151 | 4. Push to the branch
152 | 5. Create pull request
153 |
154 |
155 | ## 🔷 Author
156 |
157 | 👨💻 [Karol Kulesza](https://github.com/karolkulesza) ([@karolkulesza](https://twitter.com/karolkulesza))
158 |
159 |
160 | ## 🔷 License
161 |
162 | 📄 QGrid is available under the MIT license. See the LICENSE file for more info.
163 |
--------------------------------------------------------------------------------
/Sources/QGrid/QGrid.swift:
--------------------------------------------------------------------------------
1 | //
2 | // QGrid.swift
3 | // QGrid
4 | //
5 | // Created by Karol Kulesza on 7/06/19.
6 | // Copyright © 2019 Q Mobile { http://Q-Mobile.IT }
7 | //
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in
17 | // all copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 | // THE SOFTWARE.
26 |
27 | import SwiftUI
28 |
29 |
30 | /// A container that presents rows of data arranged in multiple columns.
31 | @available(iOS 13.0, OSX 10.15, *)
32 | public struct QGrid: View
33 | where Data : RandomAccessCollection, Content : View, Data.Element : Identifiable {
34 | private struct QGridIndex : Identifiable { var id: Int }
35 |
36 | // MARK: - STORED PROPERTIES
37 |
38 | private let columns: Int
39 | private let columnsInLandscape: Int
40 | private let vSpacing: CGFloat
41 | private let hSpacing: CGFloat
42 | private let vPadding: CGFloat
43 | private let hPadding: CGFloat
44 | private let isScrollable: Bool
45 | private let showScrollIndicators: Bool
46 |
47 | private let data: [Data.Element]
48 | private let content: (Data.Element) -> Content
49 |
50 | // MARK: - INITIALIZERS
51 |
52 | /// Creates a QGrid that computes its cells from an underlying collection of identified data.
53 | ///
54 | /// - Parameters:
55 | /// - data: A collection of identified data.
56 | /// - columns: Target number of columns for this grid, in Portrait device orientation
57 | /// - columnsInLandscape: Target number of columns for this grid, in Landscape device orientation; If not provided, `columns` value will be used.
58 | /// - vSpacing: Vertical spacing: The distance between each row in grid. If not provided, the default value will be used.
59 | /// - hSpacing: Horizontal spacing: The distance between each cell in grid's row. If not provided, the default value will be used.
60 | /// - 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.
61 | /// - 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.
62 | /// - isScrollable: Boolean that determines whether or not the grid should scroll
63 | /// - content: A closure returning the content of the individual cell
64 | public init(_ data: Data,
65 | columns: Int,
66 | columnsInLandscape: Int? = nil,
67 | vSpacing: CGFloat = 10,
68 | hSpacing: CGFloat = 10,
69 | vPadding: CGFloat = 10,
70 | hPadding: CGFloat = 10,
71 | isScrollable: Bool = true,
72 | showScrollIndicators: Bool = false,
73 | content: @escaping (Data.Element) -> Content) {
74 | self.data = data.map { $0 }
75 | self.content = content
76 | self.columns = max(1, columns)
77 | self.columnsInLandscape = columnsInLandscape ?? max(1, columns)
78 | self.vSpacing = vSpacing
79 | self.hSpacing = hSpacing
80 | self.vPadding = vPadding
81 | self.hPadding = hPadding
82 | self.isScrollable = isScrollable
83 | self.showScrollIndicators = showScrollIndicators
84 | }
85 |
86 | // MARK: - COMPUTED PROPERTIES
87 |
88 | private var rows: Int {
89 | data.count / self.cols
90 | }
91 |
92 | private var cols: Int {
93 | #if os(tvOS)
94 | return columnsInLandscape
95 | #elseif os(macOS)
96 | return columnsInLandscape
97 | #else
98 | return UIDevice.current.orientation.isLandscape ? columnsInLandscape : columns
99 | #endif
100 | }
101 |
102 | /// Declares the content and behavior of this view.
103 | public var body : some View {
104 | GeometryReader { geometry in
105 | Group {
106 | if !self.data.isEmpty {
107 | if self.isScrollable {
108 | ScrollView(showsIndicators: self.showScrollIndicators) {
109 | self.content(using: geometry)
110 | }
111 | } else {
112 | self.content(using: geometry)
113 | }
114 | }
115 | }
116 | .padding(.horizontal, self.hPadding)
117 | .padding(.vertical, self.vPadding)
118 | }
119 | }
120 |
121 | // MARK: - `BODY BUILDER` 💪 FUNCTIONS
122 |
123 | private func rowAtIndex(_ index: Int,
124 | geometry: GeometryProxy,
125 | isLastRow: Bool = false) -> some View {
126 | HStack(spacing: self.hSpacing) {
127 | ForEach((0..<(isLastRow ? data.count % cols : cols))
128 | .map { QGridIndex(id: $0) }) { column in
129 | self.content(self.data[index + column.id])
130 | .frame(width: self.contentWidthFor(geometry))
131 | }
132 | if isLastRow { Spacer() }
133 | }
134 | }
135 |
136 | private func content(using geometry: GeometryProxy) -> some View {
137 | VStack(spacing: self.vSpacing) {
138 | ForEach((0.. 0) {
144 | self.rowAtIndex(self.cols * self.rows,
145 | geometry: geometry,
146 | isLastRow: true)
147 | }
148 | }
149 | }
150 |
151 | // MARK: - HELPER FUNCTIONS
152 |
153 | private func contentWidthFor(_ geometry: GeometryProxy) -> CGFloat {
154 | let hSpacings = hSpacing * (CGFloat(self.cols) - 1)
155 | let width = geometry.size.width - hSpacings - hPadding * 2
156 | return width / CGFloat(self.cols)
157 | }
158 | }
159 |
160 |
--------------------------------------------------------------------------------