Inside ViewBuilder you also can use regular ForEach statement.
There is no way to get KeyPath id value from the initialized ForEach view. Its inner content will be distinguished by views order while doing animations. It’s better to use ForEach with Identifiable models or GridGroup created either with explicit ID value or Identifiable models to keep track of the grid views and their View representations in animations.
Number of views in ViewBuilder closure is limited to 10. It’s impossible to obtain content views from regular SwiftUI Group view. To exceed that limit you could use GridGroup. Every view in GridGroup is placed as a separate grid item. Unlike the Group view any outer method modifications of GridView are not applied to the descendant views. So it’s just an enumerable container. Also GridGroup could be created by Range<Int>, Identifiable models, by ID specified explicitly.
You can bind a view’s identity to the given single Hashable or Identifiable value also using GridGroup. This will produce transition animation to a new view with the same identity.
There is no way to use View’s .id() modifier as inner ForEach view clears that value
You can use GridGroup.empty to define a content absence.
Pay attention to limiting a size of views that fills the entire space provided by parent and Text() views which tend to draw as a single line.
Flexible sized track: .fr(N)
Fr is a fractional unit and .fr(1) is for 1 part of the unassigned space in the grid. Flexible-sized tracks are computed at the very end after all non-flexible sized tracks (.pt and .fit).
So the available space to distribute for them is the difference of the total size available and the sum of non-flexible track sizes.
Also, you could specify just an Int literal as a track size. It’s equal to repeating .fr(1) track sizes:
Grid(tracks: 3) { ... }
is equal to:
Grid(tracks: [.fr(1), .fr(1), .fr(1)]) { ... }
4. Grid cell background and overlay
When using non-flexible track sizes it’s possible that the extra space to be allocated will be greater than a grid item is able to take up. To fill that space you could use .gridCellBackground(...) and gridCellOverlay(...) modifiers.
Every grid view may span across the provided number of grid tracks. You can achieve it using .gridSpan(column: row:) modifier. The default span is 1.
View with span >= 2 that spans across the tracks with flexible size doesn’t take part in the sizes distribution for these tracks. This view will fit to the spanned tracks. So it’s possible to place a view with unlimited size that spans tracks with content-based sizes (.fit)
For every view you are able to set explicit start position by specifying a column, a row or both.
View will be positioned automatically if there is no start position specified.
Firstly, views with both column and row start positions are placed.
Secondly, the auto-placing algorithm tries to place views with either column or row start position. If there are any conflicts - such views are placed automatically and you see warning in the console.
And at the very end views with no explicit start position are placed.
Start position is defined using .gridStart(column: row:) modifier.
Grid has 2 types of tracks. The first one is where you specify track sizes - the fixed one. Fixed means that a count of tracks is known. The second one and orthogonal to the fixed is growing tracks type: where your content grows. Grid flow defines the direction where items grow:
Rows
Default. The number of columns is fixed and defined as track sizes. Grid items are placed moving between columns and switching to the next row after the last column. Rows count is growing.
Columns
The number of rows is fixed and defined as track sizes. Grid items are placed moving between rows and switching to the next column after the last row. Columns count is growing.
Grid flow could be specified in a grid constructor as well as using .gridFlow(...) grid modifier. The first option has more priority.
In this mode the inner grid content is able to scroll to the growing direction. Grid tracks that orthogonal to the grid flow direction (growing) are implicitly assumed to have .fit size. This means that their sizes have to be defined in the respective dimension.
Grid content mode could be specified in a grid constructor as well as using .gridContentMode(...) grid modifier. The first option has more priority.
Rows-flow scroll:
struct VCardView: View {
let text: String
let color: UIColor
var body: some View {
VStack {
Image("dog")
.resizable()
.aspectRatio(contentMode: .fit)
.cornerRadius(5)
.frame(minWidth: 100, minHeight: 50)
Text(self.text)
.layoutPriority(.greatestFiniteMagnitude)
}
.padding(5)
.gridCellBackground { _ in
ColorView(self.color)
}
.gridCellOverlay { _ in
RoundedRectangle(cornerRadius: 5)
.strokeBorder(Color(self.color.darker()),
lineWidth: 3)
}
}
}
struct ContentView: View {
var body: some View {
Grid(tracks: 3) {
ForEach(0..<40) { _ in
VCardView(text: randomText(), color: .random)
.gridSpan(column: self.randomSpan)
}
}
.gridContentMode(.scroll)
.gridPacking(.dense)
.gridFlow(.rows)
}
var randomSpan: Int {
Int(arc4random_uniform(3)) + 1
}
}
Columns-flow scroll:
struct HCardView: View {
let text: String
let color: UIColor
var body: some View {
HStack {
Image("dog")
.resizable()
.aspectRatio(contentMode: .fit)
.cornerRadius(5)
Text(self.text)
.frame(maxWidth: 200)
}
.padding(5)
.gridCellBackground { _ in
ColorView(self.color)
}
.gridCellOverlay { _ in
RoundedRectangle(cornerRadius: 5)
.strokeBorder(Color(self.color.darker()),
lineWidth: 3)
}
}
}
struct ContentView: View {
var body: some View {
Grid(tracks: 3) {
ForEach(0..<8) { _ in
HCardView(text: randomText(), color: .random)
.gridSpan(row: self.randomSpan)
}
}
.gridContentMode(.scroll)
.gridFlow(.columns)
.gridPacking(.dense)
}
var randomSpan: Int {
Int(arc4random_uniform(3)) + 1
}
}
Fill
Default. In this mode, grid view tries to fill the entire space provided by the parent view with its content. Grid tracks that orthogonal to the grid flow direction (growing) are implicitly assumed to have .fr(1) size.
Auto-placing algorithm could stick to one of two strategies:
Sparse
Default. The placement algorithm only ever moves “forward” in the grid when placing items, never backtracking to fill holes. This ensures that all of the auto-placed items appear “in order”, even if this leaves holes that could have been filled by later items.
Dense
Attempts to fill in holes earlier in the grid if smaller items come up later. This may cause items to appear out-of-order, when doing so would fill in holes left by larger items.
Grid packing could be specified in a grid constructor as well as using .gridPacking(...) grid modifier. The first option has more priority.
Example:
@State var gridPacking = GridPacking.sparse
var body: some View {
VStack {
self.packingPicker
Grid(tracks: 4) {
ColorView(.red)
ColorView(.black)
.gridSpan(column: 4)
ColorView(.purple)
ColorView(.orange)
ColorView(.green)
}
.gridPacking(self.gridPacking)
.gridAnimation(.default)
}
}
10. Spacing
There are several ways to define the horizontal and vertical spacings between tracks:
Using Int literal which means equal spacing in all directions:
@State var vSpacing: CGFloat = 0
@State var hSpacing: CGFloat = 0
var body: some View {
VStack {
self.sliders
Grid(tracks: 3, spacing: [hSpacing, vSpacing]) {
ForEach(0..<21) {
//Inner image used to measure size
self.image
.aspectRatio(contentMode: .fit)
.opacity(0)
.gridSpan(column: max(1, $0 % 4))
.gridCellOverlay {
//This one is to display
self.image
.aspectRatio(contentMode: .fill)
.frame(width: $0?.width,
height: $0?.height)
.cornerRadius(5)
.clipped()
.shadow(color: self.shadowColor,
radius: 10, x: 0, y: 0)
}
}
}
.background(self.backgroundColor)
.gridContentMode(.scroll)
.gridPacking(.dense)
}
}
11. Alignment
.gridItemAlignment
Use this to specify the alignment for a specific single grid item. It has higher priority than gridCommonItemsAlignment
.gridCommonItemsAlignment
Applies to every item as gridItemAlignment, but doesn’t override its individual gridItemAlignment value.
.gridContentAlignment
Applies to the whole grid content. Takes effect when content size is less than the space available for the grid.
Example:
struct SingleAlignmentExample: View {
var body: some View {
Grid(tracks: 3) {
TextCardView(text: "Hello", color: .red)
.gridItemAlignment(.leading)
TextCardView(text: "world", color: .blue)
}
.gridCommonItemsAlignment(.center)
.gridContentAlignment(.trailing)
}
}
struct TextCardView: View {
let text: String
let color: UIColor
var textColor: UIColor = .white
var body: some View {
Text(self.text)
.foregroundColor(Color(self.textColor))
.padding(5)
.gridCellBackground { _ in
ColorView(color)
}
.gridCellOverlay { _ in
RoundedRectangle(cornerRadius: 5)
.strokeBorder(Color(self.color.darker()),
lineWidth: 3)
}
}
}
12. Animations
You can define a specific animation that will be applied to the inner ZStack using .gridAnimation() grid modifier. By default, every view in the grid is associated with subsequent index as it’s ID. Hence SwiftUI relies on the grid view position in the initial and final state to perform animation transition.
You can associate a specific ID to a grid view using ForEach or GridGroup initialized by Identifiable models or by explicit KeyPath as ID to force an animation to perform in the right way.
There is no way to get KeyPath id value from the initialized ForEach view. Its inner content will be distinguished by views order while doing animations. It’s better to use ForEach with Identifiable models or GridGroup created either with explicit ID value or Identifiable models to keep track of the grid views and their View representations in animations.
13. Caching
It’s possible to cache grid layouts through the lifecycle of Grid.
Supported for iOS only
Grid caching could be specified in a grid constructor as well as using .gridCaching(...) grid modifier. The first option has more priority.
In memory cache
Default. Cache is implemented with the leverage of NSCache. It will clear all the cached layouts on the memory warning notification.
No cache
No cache is used. Layout calculations will be executed at every step of Grid lifecycle.
14. Conditional statements / @GridBuilder
Starting with Swift 5.3 we can use custom function builders without any issues.
That gives us:
Full support of if/if else, if let/if let else, switch statements within the Grid and GridGroup bodies.
A better way to propagate view ID from nested GridGroup and ForEach
An ability to return heterogeneous views from functions and vars using @GridBuilder attribute and some View retrun type:
@GridBuilder
func headerSegment(flag: Bool) -> some View {
if flag {
return GridGroup { ... }
else {
return ColorView(.black)
}
}
Grid
Grid view inspired by CSS Grid and written with SwiftUI
We are a development agency building phenomenal apps.
Overview
Grid is a powerful and easy way to layout your views in SwiftUI:
Check out full documentation below.
Installation
CocoaPods
Grid is available through CocoaPods. To install it, simply add the following line to your Podfile:
Swift Package Manager
Grid is available through Swift Package Manager.
Add it to an existing Xcode project as a package dependency:
Requirements
Building from sources
Documentation
.fr(...)
.pt(...)
.fit
1. Initialization
You can instantiate Grid in different ways:
Just specify tracks and your views inside ViewBuilder closure:
Use Range:
Use Identifiable enitites:
Use explicitly defined ID:
2. Containers
ForEach
Inside ViewBuilder you also can use regular
ForEach
statement. There is no way to get KeyPath id value from the initialized ForEach view. Its inner content will be distinguished by views order while doing animations. It’s better to useForEach
withIdentifiable
models or GridGroup created either with explicit ID value orIdentifiable
models to keep track of the grid views and theirView
representations in animations.GridGroup
Number of views in
ViewBuilder
closure is limited to 10. It’s impossible to obtain content views from regular SwiftUIGroup
view. To exceed that limit you could useGridGroup
. Every view inGridGroup
is placed as a separate grid item. Unlike theGroup
view any outer method modifications ofGridView
are not applied to the descendant views. So it’s just an enumerable container. AlsoGridGroup
could be created byRange<Int>
,Identifiable
models, by ID specified explicitly.You can bind a view’s identity to the given single
Hashable
orIdentifiable
value also usingGridGroup
. This will produce transition animation to a new view with the same identity.There is no way to use View’s
.id()
modifier as innerForEach
view clears that valueYou can use
GridGroup.empty
to define a content absence.Examples:
3. Track sizes
There are 3 types of track sizes that you could mix with each other:
Fixed-sized track:
.pt(N)
where N - points count.Content-based size:
.fit
Defines the track size as a maximum of the content sizes of every view in track
Pay attention to limiting a size of views that fills the entire space provided by parent and
Text()
views which tend to draw as a single line.Flexible sized track:
.fr(N)
Fr is a fractional unit and
.fr(1)
is for 1 part of the unassigned space in the grid. Flexible-sized tracks are computed at the very end after all non-flexible sized tracks (.pt and .fit). So the available space to distribute for them is the difference of the total size available and the sum of non-flexible track sizes.Also, you could specify just an
Int
literal as a track size. It’s equal to repeating.fr(1)
track sizes:is equal to:
4. Grid cell background and overlay
When using non-flexible track sizes it’s possible that the extra space to be allocated will be greater than a grid item is able to take up. To fill that space you could use
.gridCellBackground(...)
andgridCellOverlay(...)
modifiers.See Content mode and Spacing examples.
5. Spans
Every grid view may span across the provided number of grid tracks. You can achieve it using
.gridSpan(column: row:)
modifier. The default span is 1.View with span >= 2 that spans across the tracks with flexible size doesn’t take part in the sizes distribution for these tracks. This view will fit to the spanned tracks. So it’s possible to place a view with unlimited size that spans tracks with content-based sizes (.fit)
Spanning across tracks with different size types:
6. Starts
For every view you are able to set explicit start position by specifying a column, a row or both. View will be positioned automatically if there is no start position specified. Firstly, views with both column and row start positions are placed. Secondly, the auto-placing algorithm tries to place views with either column or row start position. If there are any conflicts - such views are placed automatically and you see warning in the console. And at the very end views with no explicit start position are placed.
Start position is defined using
.gridStart(column: row:)
modifier.7. Flow
Grid has 2 types of tracks. The first one is where you specify track sizes - the fixed one. Fixed means that a count of tracks is known. The second one and orthogonal to the fixed is growing tracks type: where your content grows. Grid flow defines the direction where items grow:
Rows
Default. The number of columns is fixed and defined as track sizes. Grid items are placed moving between columns and switching to the next row after the last column. Rows count is growing.
Columns
The number of rows is fixed and defined as track sizes. Grid items are placed moving between rows and switching to the next column after the last row. Columns count is growing.
Grid flow could be specified in a grid constructor as well as using
.gridFlow(...)
grid modifier. The first option has more priority.8. Content mode
There are 2 kinds of content modes:
Scroll
In this mode the inner grid content is able to scroll to the growing direction. Grid tracks that orthogonal to the grid flow direction (growing) are implicitly assumed to have .fit size. This means that their sizes have to be defined in the respective dimension.
Grid content mode could be specified in a grid constructor as well as using
.gridContentMode(...)
grid modifier. The first option has more priority.Rows-flow scroll:
Columns-flow scroll:
Fill
Default. In this mode, grid view tries to fill the entire space provided by the parent view with its content. Grid tracks that orthogonal to the grid flow direction (growing) are implicitly assumed to have .fr(1) size.
9. Packing
Auto-placing algorithm could stick to one of two strategies:
Sparse
Default. The placement algorithm only ever moves “forward” in the grid when placing items, never backtracking to fill holes. This ensures that all of the auto-placed items appear “in order”, even if this leaves holes that could have been filled by later items.
Dense
Attempts to fill in holes earlier in the grid if smaller items come up later. This may cause items to appear out-of-order, when doing so would fill in holes left by larger items.
Grid packing could be specified in a grid constructor as well as using
.gridPacking(...)
grid modifier. The first option has more priority.Example:
10. Spacing
There are several ways to define the horizontal and vertical spacings between tracks:
Int
literal which means equal spacing in all directions:Example:
11. Alignment
.gridItemAlignment
Use this to specify the alignment for a specific single grid item. It has higher priority than
gridCommonItemsAlignment
.gridCommonItemsAlignment
Applies to every item as
gridItemAlignment
, but doesn’t override its individualgridItemAlignment
value..gridContentAlignment
Applies to the whole grid content. Takes effect when content size is less than the space available for the grid.
Example:
12. Animations
You can define a specific animation that will be applied to the inner
ZStack
using.gridAnimation()
grid modifier.By default, every view in the grid is associated with subsequent index as it’s ID. Hence SwiftUI relies on the grid view position in the initial and final state to perform animation transition. You can associate a specific ID to a grid view using ForEach or GridGroup initialized by
Identifiable
models or by explicit KeyPath as ID to force an animation to perform in the right way.There is no way to get KeyPath id value from the initialized ForEach view. Its inner content will be distinguished by views order while doing animations. It’s better to use ForEach with
Identifiable
models or GridGroup created either with explicit ID value orIdentifiable
models to keep track of the grid views and theirView
representations in animations.13. Caching
It’s possible to cache grid layouts through the lifecycle of Grid.
Supported for iOS only
Grid caching could be specified in a grid constructor as well as using
.gridCaching(...)
grid modifier. The first option has more priority.In memory cache
Default. Cache is implemented with the leverage of NSCache. It will clear all the cached layouts on the memory warning notification.
No cache
No cache is used. Layout calculations will be executed at every step of Grid lifecycle.
14. Conditional statements / @GridBuilder
Starting with Swift 5.3 we can use custom function builders without any issues. That gives us:
if/if else
,if let/if let else
,switch
statements within theGrid
andGridGroup
bodies.GridGroup
andForEach
@GridBuilder
attribute andsome View
retrun type:Release notes:
v1.5.0:
v1.4.2:
Previous releases
##### [v1.4.1](https://github.com/exyte/Grid/releases/tag/1.4.1): - fixes the issue when Grid doesn’t update its contentIssue: If any content item within GridBuilder uses any outer data then Grid doesn’t update it. For example:
Grid didn’t update titleText even if it’s changed.
v1.4.0:
gridItemAlignment
modifier to align per itemgridCommonItemsAlignment
modifier to align all itemsgridContentAlignment
modifier to align the whole grid contentv1.3.1.beta:
gridAlignment
modifier to align per itemv1.2.1.beta:
gridCommonItemsAlignment
modifier to align all items in Gridv1.1.1.beta:
v1.1.0:
v1.0.1:
@GridBuilder
function builderv0.1.0:
GridGroup
init using a singleIdentifiable
orHashable
valuev0.0.3:
v0.0.2
Roadmap:
License
Grid is available under the MIT license. See the LICENSE file for more info.
Our other open source SwiftUI libraries
PopupView - Toasts and popups library
ScalingHeaderScrollView - A scroll view with a sticky header which shrinks as you scroll
AnimatedTabBar - A tabbar with number of preset animations
MediaPicker - Customizable media picker
Chat - Chat UI framework with fully customizable message cells, input view, and a built-in media picker
ConcentricOnboarding - Animated onboarding flow
FloatingButton - Floating button menu
ActivityIndicatorView - A number of animated loading indicators
ProgressIndicatorView - A number of animated progress indicators
SVGView - SVG parser
LiquidSwipe - Liquid navigation animation