{"id":1312,"url":"https://github.com/exyte/Grid","last_synced_at":"2025-08-06T13:32:48.637Z","repository":{"id":37210255,"uuid":"255865003","full_name":"exyte/Grid","owner":"exyte","description":"The most powerful Grid container missed in SwiftUI","archived":false,"fork":false,"pushed_at":"2024-03-28T03:10:50.000Z","size":75496,"stargazers_count":1687,"open_issues_count":12,"forks_count":88,"subscribers_count":27,"default_branch":"master","last_synced_at":"2024-05-22T15:42:23.367Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/exyte.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-04-15T09:16:48.000Z","updated_at":"2024-05-21T18:28:39.000Z","dependencies_parsed_at":"2024-04-27T20:40:31.538Z","dependency_job_id":"30ff5a18-e299-4a1e-974e-b89fbe561b2f","html_url":"https://github.com/exyte/Grid","commit_stats":{"total_commits":245,"total_committers":5,"mean_commits":49.0,"dds":"0.17959183673469392","last_synced_commit":"65329599b1bbedbfd06eb5a432bab58403c27a8e"},"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/exyte%2FGrid","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/exyte%2FGrid/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/exyte%2FGrid/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/exyte%2FGrid/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/exyte","download_url":"https://codeload.github.com/exyte/Grid/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":215780271,"owners_count":15929791,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-01-05T20:15:43.578Z","updated_at":"2025-08-06T13:32:48.613Z","avatar_url":"https://github.com/exyte.png","language":"Swift","readme":"\u003ca href=\"https://exyte.com/\"\u003e\u003cpicture\u003e\u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"https://raw.githubusercontent.com/exyte/media/master/common/header-dark.png\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/exyte/media/master/common/header-light.png\"\u003e\u003c/picture\u003e\u003c/a\u003e\n\n\u003ca href=\"https://exyte.com/\"\u003e\u003cpicture\u003e\u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"https://raw.githubusercontent.com/exyte/media/master/common/our-site-dark.png\" width=\"80\" height=\"16\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/exyte/media/master/common/our-site-light.png\" width=\"80\" height=\"16\"\u003e\u003c/picture\u003e\u003c/a\u003e\u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;\u003ca href=\"https://twitter.com/exyteHQ\"\u003e\u003cpicture\u003e\u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"https://raw.githubusercontent.com/exyte/media/master/common/twitter-dark.png\" width=\"74\" height=\"16\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/exyte/media/master/common/twitter-light.png\" width=\"74\" height=\"16\"\u003e\n\u003c/picture\u003e\u003c/a\u003e \u003ca href=\"https://exyte.com/contacts\"\u003e\u003cpicture\u003e\u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"https://raw.githubusercontent.com/exyte/media/master/common/get-in-touch-dark.png\" width=\"128\" height=\"24\" align=\"right\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/exyte/media/master/common/get-in-touch-light.png\" width=\"128\" height=\"24\" align=\"right\"\u003e\u003c/picture\u003e\u003c/a\u003e\n\n\u003cimg width=\"480\" src=\"https://github.com/exyte/Grid/raw/media/Assets/calc-animation-mock-iPhone-XS-Max.gif\"/\u003e\n\n# Grid\n\nGrid view inspired by CSS Grid and written with SwiftUI\n\n\u003ca href=\"https://exyte.com/blog/implementing-grid-layout-in-swiftui\"\u003eRead Article »\u003c/a\u003e\n\n[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fexyte%2FGrid%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/exyte/Grid)\n[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fexyte%2FGrid%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/exyte/Grid)\n[![SPM Compatible](https://img.shields.io/badge/SwiftPM-Compatible-brightgreen.svg)](https://swiftpackageindex.com/exyte/Grid)\n[![Cocoapods Compatible](https://img.shields.io/badge/cocoapods-Compatible-brightgreen.svg)](https://cocoapods.org/pods/Grid)\n[![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-brightgreen.svg?style=flat)](https://github.com/Carthage/Carthage)\n[![License: MIT](https://img.shields.io/badge/License-MIT-black.svg)](https://opensource.org/licenses/MIT)\n\n## Overview\n\nGrid is a powerful and easy way to layout your views in SwiftUI:\n\n\u003cimg src=\"https://i.imgur.com/pl3k7iE.gif\"\u003e\n\nCheck out [full documentation](#documentation) below.\n\n## Installation\n\n#### CocoaPods\n\nGrid is available through [CocoaPods](https://cocoapods.org). To install\nit, simply add the following line to your Podfile:\n\n```ruby\npod 'ExyteGrid'\n```\n\n#### Swift Package Manager\n\nGrid is available through [Swift Package Manager](https://swift.org/package-manager).\n\nAdd it to an existing Xcode project as a package dependency:\n\n1. From the **File** menu, select **Swift Packages › Add Package Dependency…**\n2. Enter \"https://github.com/exyte/Grid\" into the package repository URL text field\n\n## Requirements\n\n* iOS 14.0+ (the latest iOS 13 support is in [v0.1.0](https://github.com/exyte/Grid/releases/tag/0.1.0))\n* MacOS 10.15+\n* Xcode 12+\n\n## Building from sources\n\n```shell\ngit clone git@github.com:exyte/Grid.git\ncd Grid/Example/\npod install\nopen Example.xcworkspace/\n```\n\n## Documentation\n- [**Initialization**](#1-initialization)\n- [**View containers**](#2-containers)\n  - [ForEach](#foreach)\n  - [GridGroup](#gridgroup)\n- [**Track sizes:**](#3-track-sizes)\n  - [Flexible `.fr(...)`](#flexible-sized-track-frn)\n  - [Fixed `.pt(...)`](#fixed-sized-track)\n  - [Content-based `.fit`](#content-based-size-fit)\n- [**Grid cell background and overlay**](#4-grid-cell-background-and-overlay)\n- [**Spanning grid views:**](#5-spans)\n  - by rows\n  - by columns\n- [**View position specifying:**](#6-starts)\n  - automatically (implicitly)\n  - start row\n  - start column\n  - both row and column\n- [**Flow direction:**](#7-flow)\n  - [by rows](#rows)\n  - [by columns](#columns)\n- [**Content mode:**](#8-content-mode)\n  - [fill a container](#fill)\n  - [scrollable content](#scroll)\n- [**Packing mode:**](#9-packing)\n  - [sparse](#sparse)\n  - [dense](#dense)\n- [**Vertical and horizontal spacing**](#10-spacing)\n- [**Alignment**](#11-alignment)\n- [**Content updates can be animated**](#12-animations)\n- [**Caching**](#13-caching)\n- [**Conditional statements / @GridBuilder**](#14-beta-conditional-statements--gridbuilder)\n- [**Release notes**](#release-notes)\n- [**Roadmap**](#roadmap)\n\n### 1. Initialization\n\n\u003cimg align=\"right\" width=\"30%\" height=\"30%\" src=\"https://github.com/exyte/Grid/raw/media/Assets/3-equal-fr-tracks.png\"/\u003e\n\nYou can instantiate Grid in different ways:\n1. Just specify tracks and your views inside ViewBuilder closure:\n```swift\nGrid(tracks: 3) {\n    ColorView(.blue)\n    ColorView(.purple)\n    ColorView(.red)\n    ColorView(.cyan)\n    ColorView(.green)\n    ColorView(.orange)\n}\n```\n\n2. Use Range:\n```swift\nGrid(0..\u003c6, tracks: 3) { _ in\n    ColorView(.random)\n}\n```\n\n3. Use Identifiable enitites:\n```swift\nGrid(colorModels, tracks: 3) {\n    ColorView($0)\n}\n```\n\n4. Use explicitly defined ID:\n```swift\nGrid(colorModels, id: \\.self, tracks: 3) {\n    ColorView($0)\n}\n```\n\n------------\n\n### 2. Containers\n#### ForEach\nInside ViewBuilder you also can use regular `ForEach` statement. \n*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](#gridgroup) created either with explicit ID value or `Identifiable` models to keep track of the grid views and their `View` representations in animations.*\n\n\u003cimg align=\"right\" width=\"30%\" height=\"30%\" src=\"https://github.com/exyte/Grid/raw/media/Assets/forEach-1.png\"\u003e\n\n```swift\nGrid(tracks: 4) {\n    ColorView(.red)\n    ColorView(.purple)\n    \n    ForEach(0..\u003c4) { _ in\n        ColorView(.black)\n    }\n\n    ColorView(.orange)\n    ColorView(.green)\n}\n```\n\n#### GridGroup\nNumber 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\u003cInt\u003e`, `Identifiable` models, by ID specified explicitly.\n\nYou 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.\n\n*There is no way to use View's `.id()` modifier as inner `ForEach` view clears that value*\n\nYou can use `GridGroup.empty` to define a content absence.\n\nExamples:\n\n```swift\nvar arithmeticButtons: GridGroup {\n    GridGroup {\n        CalcButton(.divide)\n        CalcButton(.multiply)\n        CalcButton(.substract)\n        CalcButton(.equal)\n    }\n}\n```\n\n```swift\nvar arithmeticButtons: GridGroup {\n    let operations: [MathOperation] =\n        [.divide, .multiply, .substract, .add, .equal]\n\t\n    return GridGroup(operations, id: \\.self) {\n        CalcButton($0)\n    }\n}\n```\n\n```swift\nvar arithmeticButtons: GridGroup {\n    let operations: [MathOperation] =\n        [.divide, .multiply, .substract, .add, .equal]\n\t\n    return GridGroup {\n        ForEach(operations, id: \\.self) {\n            CalcButton($0)\n        }\n    }\n}\n\n```\n\n```swift\nvar arithmeticButtons: GridGroup {\n    let operations: [MathOperation] =\n        [.divide, .multiply, .substract, .add, .equal]\n    return GridGroup(operations, id: \\.self) { \n         CalcButton($0)\n    }\n}\n```\n\n```swift\nvar arithmeticButtons: GridGroup {\n    let operations: [MathOperation] =\n        [.divide, .multiply, .substract, .add, .equal]\n    return GridGroup(operations, id: \\.self) { \n         CalcButton($0)\n    }\n}\n```\n\n```swift\nGrid {\n...\n    GridGroup(MathOperation.clear) {\n        CalcButton($0)\n    }\n}\n```\n\n------------\n\n### 3. Track sizes\n\nThere are 3 types of track sizes that you could mix with each other:\n\n\u003cimg align=\"right\" width=\"30%\" height=\"30%\" src=\"https://github.com/exyte/Grid/raw/media/Assets/3-const-tracks.png\"/\u003e\n\n#### Fixed-sized track: \n`.pt(N)` where N - points count. \n\n```swift\nGrid(tracks: [.pt(50), .pt(200), .pt(100)]) {\n    ColorView(.blue)\n    ColorView(.purple)\n    ColorView(.red)\n    ColorView(.cyan)\n    ColorView(.green)\n    ColorView(.orange)\n}\n```\n\n\u003cimg  align=\"right\" width=\"30%\" height=\"30%\" src=\"https://github.com/exyte/Grid/raw/media/Assets/3-fit-tracks.png\"/\u003e\n\n#### Content-based size: `.fit`\n\nDefines the track size as a maximum of the content sizes of every view in track\n\n```swift\nGrid(0..\u003c6, tracks: [.fit, .fit, .fit]) {\n    ColorView(.random)\n        .frame(maxWidth: 50 + 15 * CGFloat($0))\n}\n```\n\nPay 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.\n\n#### Flexible sized track: `.fr(N)`\n\n\u003cimg align=\"right\" width=\"30%\" height=\"30%\" src=\"https://github.com/exyte/Grid/raw/media/Assets/3-fr-tracks.png\"/\u003e\n\nFr 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](#fixed-sized-track) and [.fit](#content-based-size-fit)).\nSo the available space to distribute for them is the difference of the total size available and the sum of non-flexible track sizes.\n\n```swift\nGrid(tracks: [.pt(100), .fr(1), .fr(2.5)]) {\n    ColorView(.blue)\n    ColorView(.purple)\n    ColorView(.red)\n    ColorView(.cyan)\n    ColorView(.green)\n    ColorView(.orange)\n}\n```\n\nAlso, you could specify just an `Int` literal as a track size. It's equal to repeating `.fr(1)` track sizes:\n```swift\nGrid(tracks: 3) { ... }\n```\nis equal to:\n```swift\nGrid(tracks: [.fr(1), .fr(1), .fr(1)]) { ... }\n```\n\n------------\n\n### 4. Grid cell background and overlay\nWhen 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.\n\nSee [Content mode](#8-content-mode) and [Spacing](#10-spacing) examples.\n\n------------\n\n### 5. Spans\n\n\u003cimg align=\"right\" width=\"40%\" height=\"40%\" src=\"https://github.com/exyte/Grid/raw/media/Assets/span-1-example.png\"/\u003e\n\nEvery 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.\n\n*View with span \u003e= 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](#content-based-size-fit))*\n\n```swift\nGrid(tracks: [.fr(1), .pt(150), .fr(2)]) {\n    ColorView(.blue)\n        .gridSpan(column: 2)\n    ColorView(.purple)\n        .gridSpan(row: 2)\n    ColorView(.red)\n    ColorView(.cyan)\n    ColorView(.green)\n        .gridSpan(column: 2, row: 3)\n    ColorView(.orange)\n    ColorView(.magenta)\n        .gridSpan(row: 2)\n}\n```\n\n\u003cimg align=\"right\" width=\"35%\" height=\"35%\" src=\"https://github.com/exyte/Grid/raw/media/Assets/span-2-example.png\"/\u003e\n\nSpanning across tracks with different size types:\n\n```swift\nvar body: some View {\n    Grid(tracks: [.fr(1), .fit, .fit], spacing: 10) {\n        VCardView(text: placeholderText(),\n                  color: .red)\n        \n        VCardView(text: placeholderText(length: 30),\n                  color: .orange)\n            .frame(maxWidth: 70)\n        \n        VCardView(text: placeholderText(length: 120),\n                  color: .green)\n            .frame(maxWidth: 100)\n            .gridSpan(column: 1, row: 2)\n        \n        VCardView(text: placeholderText(length: 160),\n                  color: .magenta)\n            .gridSpan(column: 2, row: 1)\n        \n        VCardView(text: placeholderText(length: 190),\n                  color: .cyan)\n            .gridSpan(column: 3, row: 1)\n    }\n}\n```\n\n------------\n\n### 6. Starts\nFor every view you are able to set explicit start position by specifying a column, a row or both.\nView will be positioned automatically if there is no start position specified.\nFirstly, views with both column and row start positions are placed. \nSecondly, 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.\nAnd at the very end views with no explicit start position are placed.\n\nStart position is defined using `.gridStart(column: row:)` modifier.\n\n\u003cimg align=\"right\" width=\"40%\" height=\"40%\" src=\"https://github.com/exyte/Grid/raw/media/Assets/starts-spans-complex.png\"/\u003e\n\n```swift\nGrid(tracks: [.pt(50), .fr(1), .fr(1.5), .fit]) {\n    ForEach(0..\u003c6) { _ in\n        ColorView(.black)\n    }\n    \n    ColorView(.brown)\n        .gridSpan(column: 3)\n    \n    ColorView(.blue)\n        .gridSpan(column: 2)\n    \n    ColorView(.orange)\n        .gridSpan(row: 3)\n    \n    ColorView(.red)\n        .gridStart(row: 1)\n        .gridSpan(column: 2, row: 2)\n    \n    ColorView(.yellow)\n        .gridStart(row: 2)\n    \n    ColorView(.purple)\n        .frame(maxWidth: 50)\n        .gridStart(column: 3, row: 0)\n        .gridSpan(row: 9)\n    \n    ColorView(.green)\n        .gridSpan(column: 2, row: 3)\n    \n    ColorView(.cyan)\n    \n    ColorView(.gray)\n        .gridStart(column: 2)\n}\n```\n------------\n\n### 7. Flow\nGrid has 2 types of tracks. The first one is where you specify [track sizes](#3-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:\n\n#### **Rows**\n*Default.* The number of columns is fixed and [defined as track sizes](#3-track-sizes). Grid items are placed moving between columns and switching to the next row after the last column. Rows count is growing.\n\n#### **Columns**\nThe number of rows is fixed and [defined as track sizes](#3-track-sizes). Grid items are placed moving between rows and switching to the next column after the last row. Columns count is growing.\n\n*Grid flow could be specified in a grid constructor as well as using `.gridFlow(...)` grid modifier. The first option has more priority.*\n\n\u003cimg align=\"right\" width=\"31%\" height=\"31%\" src=\"https://github.com/exyte/Grid/raw/media/Assets/flow-animation.gif\"/\u003e\n\n```swift\nstruct ContentView: View {\n    @State var flow: GridFlow = .rows\n    \n    var body: some View {\n        VStack {\n            if self.flow == .rows {\n                Button(action: { self.flow = .columns }) {\n                    Text(\"Flow: ROWS\")\n                }\n            } else {\n                Button(action: { self.flow = .rows }) {\n                    Text(\"Flow: COLUMNS\")\n                }\n            }\n            \n            Grid(0..\u003c15, tracks: 5, flow: self.flow, spacing: 5) {\n                ColorView($0.isMultiple(of: 2) ? .black : .orange)\n                    .overlay(\n                        Text(String($0))\n                            .font(.system(size: 35))\n                            .foregroundColor(.white)\n                )\n            }\n            .animation(.default)\n        }\n    }\n}\n```\n------------\n\n### 8. Content mode\nThere are 2 kinds of content modes:\n\n#### Scroll\nIn this mode the inner grid content is able to scroll to the [growing direction](#7-flow). Grid tracks that orthogonal to the grid flow direction (growing) are implicitly assumed to have [.fit](#content-based-size-fit) size. This means that their sizes have to be defined in the respective dimension.\n\n*Grid content mode could be specified in a grid constructor as well as using  `.gridContentMode(...)` grid modifier. The first option has more priority.*\n \n###### Rows-flow scroll:\n\n\u003cimg align=\"right\" width=\"35%\" height=\"35%\" src=\"https://github.com/exyte/Grid/raw/media/Assets/scroll-vertical.gif\"/\u003e\n\n ```swift\nstruct VCardView: View {\n    let text: String\n    let color: UIColor\n    \n    var body: some View {\n        VStack {\n            Image(\"dog\")\n                .resizable()\n                .aspectRatio(contentMode: .fit)\n                .cornerRadius(5)\n                .frame(minWidth: 100, minHeight: 50)\n            \n            Text(self.text)\n                .layoutPriority(.greatestFiniteMagnitude)\n        }\n        .padding(5)\n        .gridCellBackground { _ in\n            ColorView(self.color)\n        }\n        .gridCellOverlay { _ in\n            RoundedRectangle(cornerRadius: 5)\n                .strokeBorder(Color(self.color.darker()),\n                              lineWidth: 3)\n        }\n    }\n}\n\nstruct ContentView: View {\n    var body: some View {\n        Grid(tracks: 3) {\n            ForEach(0..\u003c40) { _ in\n                VCardView(text: randomText(), color: .random)\n                    .gridSpan(column: self.randomSpan)\n            }\n        }\n        .gridContentMode(.scroll)\n        .gridPacking(.dense)\n        .gridFlow(.rows)\n    }\n    \n    var randomSpan: Int {\n        Int(arc4random_uniform(3)) + 1\n    }\n}\n ```\n\n###### Columns-flow scroll:\n\n\u003cimg align=\"right\" width=\"35%\" height=\"35%\" src=\"https://github.com/exyte/Grid/raw/media/Assets/scroll-horizontal.gif\"/\u003e\n\n ```swift\nstruct HCardView: View {\n    let text: String\n    let color: UIColor\n\n    var body: some View {\n        HStack {\n            Image(\"dog\")\n                .resizable()\n                .aspectRatio(contentMode: .fit)\n                .cornerRadius(5)\n            \n            Text(self.text)\n                .frame(maxWidth: 200)\n        }\n        .padding(5)\n        .gridCellBackground { _ in\n            ColorView(self.color)\n        }\n        .gridCellOverlay { _ in\n            RoundedRectangle(cornerRadius: 5)\n                .strokeBorder(Color(self.color.darker()),\n                              lineWidth: 3)\n        }\n    }\n}\n\nstruct ContentView: View {\n    var body: some View {\n        Grid(tracks: 3) {\n            ForEach(0..\u003c8) { _ in\n                HCardView(text: randomText(), color: .random)\n                    .gridSpan(row: self.randomSpan)\n            }\n        }\n        .gridContentMode(.scroll)\n        .gridFlow(.columns)\n        .gridPacking(.dense)\n    }\n    \n    var randomSpan: Int {\n        Int(arc4random_uniform(3)) + 1\n    }\n}\n ```\n \n  \n \u003cimg align=\"right\" width=\"31%\" height=\"31%\" src=\"https://github.com/exyte/Grid/raw/media/Assets/contentMode-animation.gif\"/\u003e\n\n#### Fill\n*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)](#flexible-sized-track-frn) size.\n\n```swift\n@State var contentMode: GridContentMode = .scroll\n\nvar body: some View {\n    VStack {\n        self.modesPicker\n        \n        Grid(models, id: \\.self, tracks: 3) {\n            VCardView(text: $0.text, color: $0.color)\n                .gridSpan($0.span)\n        }\n        .gridContentMode(self.contentMode)\n        .gridFlow(.rows)\n        .gridAnimation(.default)\n    }\n}\n```\n\n------------\n\n### 9. Packing\nAuto-placing algorithm could stick to one of two strategies:\n\n#### Sparse \n*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.\n\n#### Dense \nAttempts 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.\n\n*Grid packing could be specified in a grid constructor as well as using  `.gridPacking(...)` grid modifier. The first option has more priority.*\n\nExample:\n\n\u003cimg align=\"right\" width=\"35%\" height=\"35%\" src=\"https://github.com/exyte/Grid/raw/media/Assets/packing-animation.gif\"/\u003e\n\n```swift\n@State var gridPacking = GridPacking.sparse\n\nvar body: some View {\n    VStack {\n        self.packingPicker\n\n        Grid(tracks: 4) {\n            ColorView(.red)\n            \n            ColorView(.black)\n                .gridSpan(column: 4)\n            \n            ColorView(.purple)\n  \n            ColorView(.orange)\n            ColorView(.green)\n        }\n        .gridPacking(self.gridPacking)\n        .gridAnimation(.default)\n    }\n}\n```\n\n------------\n\n### 10. Spacing\nThere are several ways to define the horizontal and vertical spacings between tracks:\n\n- Using `Int` literal which means equal spacing in all directions:\n```swift\nGrid(tracks: 4, spacing: 5) { ... } \n```\n- Using explicit init\n```swift\nGrid(tracks: 4, spacing: GridSpacing(horizontal: 10, vertical: 5)) { ... } \n```\n- Using array literal:\n```swift\nGrid(tracks: 4, spacing: [10, 5]) { ... } \n```\n\nExample:\n\n\u003cimg align=\"right\" width=\"35%\" height=\"35%\" src=\"https://github.com/exyte/Grid/raw/media/Assets/spacing-animation.gif\"/\u003e\n\n```swift\n@State var vSpacing: CGFloat = 0\n@State var hSpacing: CGFloat = 0\n\nvar body: some View {\n    VStack {\n        self.sliders\n        \n        Grid(tracks: 3, spacing: [hSpacing, vSpacing]) {\n            ForEach(0..\u003c21) {\n                //Inner image used to measure size\n                self.image\n                    .aspectRatio(contentMode: .fit)\n                    .opacity(0)\n                    .gridSpan(column: max(1, $0 % 4))\n                    .gridCellOverlay {\n                        //This one is to display\n                        self.image\n                            .aspectRatio(contentMode: .fill)\n                            .frame(width: $0?.width, \n\t\t\t           height: $0?.height)\n                            .cornerRadius(5)\n                            .clipped()\n                            .shadow(color: self.shadowColor, \n\t\t\t            radius: 10, x: 0, y: 0)\n                }\n            }\n        }\n        .background(self.backgroundColor)\n        .gridContentMode(.scroll)\n        .gridPacking(.dense)\n    }\n}\n```\n\n------------\n\n### 11. Alignment\n\n#### `.gridItemAlignment`\nUse this to specify the alignment for a specific single grid item. It has higher priority than `gridCommonItemsAlignment`\n\n\n#### `.gridCommonItemsAlignment`\nApplies to every item as `gridItemAlignment`, but doesn't override its individual `gridItemAlignment` value. \n\n\n#### `.gridContentAlignment`\nApplies to the whole grid content. Takes effect when content size is less than the space available for the grid.\n\nExample:\n\n\u003cimg align=\"right\" width=\"35%\" height=\"35%\" src=\"https://github.com/exyte/Grid/raw/media/Assets/alignments.png\"/\u003e\n\n```swift\nstruct SingleAlignmentExample: View {\n  var body: some View {\n    Grid(tracks: 3) {\n      TextCardView(text: \"Hello\", color: .red)\n        .gridItemAlignment(.leading)\n\n      TextCardView(text: \"world\", color: .blue)\n    }\n    .gridCommonItemsAlignment(.center)\n    .gridContentAlignment(.trailing)\n  }\n}\n\nstruct TextCardView: View {\n  let text: String\n  let color: UIColor\n  var textColor: UIColor = .white\n\n  var body: some View {\n    Text(self.text)\n      .foregroundColor(Color(self.textColor))\n      .padding(5)\n      .gridCellBackground { _ in\n        ColorView(color)\n      }\n      .gridCellOverlay { _ in\n        RoundedRectangle(cornerRadius: 5)\n          .strokeBorder(Color(self.color.darker()),\n                        lineWidth: 3)\n      }\n  }\n}\n\n```\n------------\n\n### 12. Animations\nYou can define a specific animation that will be applied to the inner `ZStack` using `.gridAnimation()` grid modifier.  \nBy 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.\nYou can associate a specific ID to a grid view using [ForEach](#foreach) or [GridGroup](#gridgroup) initialized by `Identifiable` models or by explicit KeyPath as ID to force an animation to perform in the right way.\n\n*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](#foreach) with `Identifiable` models or [GridGroup](#gridgroup) created either with explicit ID value or `Identifiable` models to keep track of the grid views and their `View` representations in animations.* \n\n------------\n\n### 13. Caching\nIt's possible to cache grid layouts through the lifecycle of Grid.\n\n*Supported for iOS only*\n\n*Grid caching could be specified in a grid constructor as well as using  `.gridCaching(...)` grid modifier. The first option has more priority.*\n\n#### In memory cache \n*Default.* Cache is implemented with the leverage of NSCache. It will clear all the cached layouts on the memory warning notification.\n\n#### No cache\nNo cache is used. Layout calculations will be executed at every step of Grid lifecycle.\n\n------------\n\n### 14. Conditional statements / @GridBuilder\n\nStarting with Swift 5.3 we can use custom function builders without [any issues](https://github.com/apple/swift/pull/29626). \nThat gives us:\n\n- Full support of `if/if else`, `if let/if let else`, `switch` statements within the `Grid` and `GridGroup` bodies.\n\t\n- A better way to propagate view ID from nested `GridGroup` and `ForEach`\n\t\n- An ability to return heterogeneous views from functions and vars using `@GridBuilder` attribute and `some View` retrun type:\n\t\n```swift\n@GridBuilder\nfunc headerSegment(flag: Bool) -\u003e some View {\n    if flag {\n        return GridGroup { ... }\n    else {\n        return ColorView(.black)\n    }\n}\n```\n\n------------\n\n## Release notes:\n##### [v1.5.0](https://github.com/exyte/Grid/releases/tag/1.5.0):\n- add tvOS support, migrate Examples project to Xcode universal target, swift app lifecycle, SPM\n\n##### [v1.4.2](https://github.com/exyte/Grid/releases/tag/1.4.2):\n- fixes not working gridItemAlignment and gridCommonItemsAlignment for vertical axis\n\n\u003cdetails\u003e\n  \u003csummary\u003ePrevious releases\u003c/summary\u003e\n##### [v1.4.1](https://github.com/exyte/Grid/releases/tag/1.4.1):\n- fixes the issue when Grid doesn’t update its content\n\nIssue:\nIf any content item within GridBuilder uses any outer data then Grid doesn't update it.\nFor example:\n\n```\n@State var titleText: String = \"title\"\n\nGrid(tracks: 2) {\n  Text(titleText)\n  Text(\"hello\")\n}\n\n```\n\nGrid didn't update titleText even if it's changed.\n\n##### [v1.4.0](https://github.com/exyte/Grid/releases/tag/1.4.0):\n- adds `gridItemAlignment` modifier to align per item\n- adds `gridCommonItemsAlignment` modifier to align all items\n- adds `gridContentAlignment` modifier to align the whole grid content\n\t\n##### [v1.3.1.beta](https://github.com/exyte/Grid/releases/tag/1.3.1.beta):\n- adds `gridAlignment` modifier to align per item\n\n##### [v1.2.1.beta](https://github.com/exyte/Grid/releases/tag/1.2.1.beta):\n- adds `gridCommonItemsAlignment` modifier to align all items in Grid\n\n##### [v1.1.1.beta](https://github.com/exyte/Grid/releases/tag/1.1.1.beta):\n- adds WidgetKit support by conditionally rendering ScrollView\n\n##### [v1.1.0](https://github.com/exyte/Grid/releases/tag/1.1.0):\n- adds MacOS support\n\t\n##### [v1.0.1](https://github.com/exyte/Grid/releases/tag/1.0.1):\n- adds full support of conditional statements\n- adds `@GridBuilder` function builder\n\n##### [v0.1.0](https://github.com/exyte/Grid/releases/tag/0.1.0):\n- adds layout caching\n- adds `GridGroup` init using a single `Identifiable` or `Hashable` value\n\t\n##### [v0.0.3](https://github.com/exyte/Grid/releases/tag/0.0.3):\n- fixes any issues when Grid is conditionally presented\n- fixes wrong grid position with scrollable content after a device rotation\n- fixes \"Bound preference ** tried to update multiple times per frame\" warnings in iOS 14 and reduces them in iOS 13\n- simplifies the process of collecting grid preferences under the hood\n\t\n##### [v0.0.2](https://github.com/exyte/Grid/releases/tag/0.0.2)\n- added support for Swift Package Manager\n\t\n\u003c/details\u003e\n\n------------\n\n## Roadmap:\n\n- [ ] add WidgetKit example\n- [ ] add alignment per tracks\n- [ ] add regions or settings for GridGroup to specify position\n- [ ] dual dimension track sizes (grid-template-rows, grid-template-columns).\n- [ ] grid-auto-rows, grid-auto-columns\n- [ ] improve dense placement algorithm\n- [ ] ? grid min/ideal sizes\n- [ ] ? landscape/portrait layout\n- [ ] ? calculate layout in background thread\n- [x] add GridIdentified-like item to track the same Views in animations \n- [x] support if clauses using function builder\n- [x] add GridGroup\n- [x] grid item explicit row and/or column position\n- [x] different spacing for rows and columns\n- [x] intrinsic sized tracks (fit-content)\n- [x] forEach support\n- [x] dense/sparse placement algorithm\n- [x] add horizontal axis\n- [x] init via Identifiable models\n- [x] scrollable content\n\n\n## License\n\nGrid is available under the MIT license. See the LICENSE file for more info.\n\n## Our other open source SwiftUI libraries\n[PopupView](https://github.com/exyte/PopupView) - Toasts and popups library    \n[AnchoredPopup](https://github.com/exyte/AnchoredPopup) - Anchored Popup grows \"out\" of a trigger view (similar to Hero animation)    \n[ScalingHeaderScrollView](https://github.com/exyte/ScalingHeaderScrollView) - A scroll view with a sticky header which shrinks as you scroll    \n[AnimatedTabBar](https://github.com/exyte/AnimatedTabBar) - A tabbar with a number of preset animations   \n[MediaPicker](https://github.com/exyte/mediapicker) - Customizable media picker     \n[Chat](https://github.com/exyte/chat) - Chat UI framework with fully customizable message cells, input view, and a built-in media picker  \n[OpenAI](https://github.com/exyte/OpenAI) Wrapper lib for [OpenAI REST API](https://platform.openai.com/docs/api-reference/introduction)    \n[AnimatedGradient](https://github.com/exyte/AnimatedGradient) - Animated linear gradient     \n[ConcentricOnboarding](https://github.com/exyte/ConcentricOnboarding) - Animated onboarding flow    \n[FloatingButton](https://github.com/exyte/FloatingButton) - Floating button menu    \n[ActivityIndicatorView](https://github.com/exyte/ActivityIndicatorView) - A number of animated loading indicators    \n[ProgressIndicatorView](https://github.com/exyte/ProgressIndicatorView) - A number of animated progress indicators    \n[FlagAndCountryCode](https://github.com/exyte/FlagAndCountryCode) - Phone codes and flags for every country    \n[SVGView](https://github.com/exyte/SVGView) - SVG parser    \n[LiquidSwipe](https://github.com/exyte/LiquidSwipe) - Liquid navigation animation    \n","funding_links":[],"categories":["Layout","Libs","Swift","Grid","Samples","UI","Layout [🔝](#readme)","etc"],"sub_categories":["Other Hardware","Layout","Content","Grid"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fexyte%2FGrid","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fexyte%2FGrid","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fexyte%2FGrid/lists"}