How to implement grid layout in SwiftUI
Use LazyVGrid with an array of GridItem columns inside a ScrollView for scrollable photo-gallery–style grids, or use the structured Grid view for fixed alignment tables and forms. Both work on iOS 17+ with no additional setup.
import SwiftUI
struct QuickGridView: View {
let columns = [GridItem(.adaptive(minimum: 100))]
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 12) {
ForEach(1...20, id: \.self) { i in
RoundedRectangle(cornerRadius: 10)
.fill(.blue.opacity(0.2))
.frame(height: 100)
.overlay(Text("\(i)").bold())
}
}
.padding()
}
}
}
Full implementation
The example below shows a photo-style grid app section with three column modes the user can toggle: adaptive (fluid columns that fill available width), fixed (always three equal columns), and flexible (proportionally sized). The grid is lazy-loading, so SwiftUI only renders cells currently on screen — critical for performance with large datasets. A ScrollView wraps everything so the grid scrolls naturally.
import SwiftUI
// MARK: - Model
struct PhotoItem: Identifiable {
let id: Int
let color: Color
let label: String
}
// MARK: - Column Style
enum GridStyle: String, CaseIterable {
case adaptive = "Adaptive"
case fixed = "Fixed (3)"
case flexible = "Flexible"
var columns: [GridItem] {
switch self {
case .adaptive:
return [GridItem(.adaptive(minimum: 90, maximum: 160))]
case .fixed:
return Array(repeating: GridItem(.fixed(110)), count: 3)
case .flexible:
return Array(repeating: GridItem(.flexible()), count: 3)
}
}
}
// MARK: - Grid View
struct PhotoGridView: View {
@State private var style: GridStyle = .adaptive
private let items: [PhotoItem] = (1...30).map { i in
PhotoItem(
id: i,
color: [Color.blue, .purple, .pink, .orange, .teal, .green][i % 6],
label: "Photo \(i)"
)
}
var body: some View {
NavigationStack {
ScrollView {
LazyVGrid(
columns: style.columns,
spacing: 10
) {
ForEach(items) { item in
GridCell(item: item)
}
}
.padding(.horizontal, 12)
.padding(.top, 8)
.animation(.easeInOut, value: style)
}
.navigationTitle("Photo Grid")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Picker("Layout", selection: $style) {
ForEach(GridStyle.allCases, id: \.self) { s in
Text(s.rawValue).tag(s)
}
}
.pickerStyle(.segmented)
.frame(width: 220)
}
}
}
}
}
// MARK: - Cell
struct GridCell: View {
let item: PhotoItem
var body: some View {
RoundedRectangle(cornerRadius: 12)
.fill(item.color.gradient.opacity(0.35))
.aspectRatio(1, contentMode: .fit)
.overlay(
VStack(spacing: 4) {
Image(systemName: "photo")
.font(.title2)
.foregroundStyle(item.color)
Text(item.label)
.font(.caption2.weight(.semibold))
.foregroundStyle(.secondary)
}
)
.accessibilityLabel(item.label)
}
}
// MARK: - Preview
#Preview {
PhotoGridView()
}
How it works
-
GridItem(.adaptive(minimum:maximum:))— tells SwiftUI to pack as many columns as fit the available width, with each column betweenminimumandmaximumpoints wide. This single-element array is all you need for a responsive gallery; SwiftUI recalculates column count automatically on rotation or split-view resize. -
LazyVGrid(columns:spacing:)— the workhorse view. "Lazy" means rows are rendered on demand as they scroll into view, keeping memory and CPU usage low even with hundreds of items. Thespacingparameter controls vertical gap between rows; horizontal gaps between columns are set per-GridItemvia itsspacingargument. -
.aspectRatio(1, contentMode: .fit)on each cell — makes every cell a perfect square regardless of which column mode is active. The cell expands to fill its column width and the height matches, giving a uniform photo-grid appearance. -
.animation(.easeInOut, value: style)— animates the column layout change when the user taps a different segment. Becausecolumnsis a value type derived from thestyleenum, SwiftUI diffing catches the change and cross-fades the relayout smoothly. -
.accessibilityLabel(item.label)on each cell — VoiceOver users hear "Photo 1", "Photo 2", etc. instead of a generic description. Always add this when cells have no visible text that fully identifies the item.
Variants
Structured alignment with Grid (fixed table)
When you need column alignment across rows — like a settings table or comparison chart — use the structured Grid view instead of LazyVGrid. It aligns values across rows automatically.
struct PricingGrid: View {
let plans = [
("Free", "5 GB", "$0"),
("Pro", "50 GB", "$9"),
("Team", "2 TB", "$29"),
]
var body: some View {
Grid(alignment: .leading, horizontalSpacing: 24, verticalSpacing: 12) {
// Header row
GridRow {
Text("Plan").bold()
Text("Storage").bold()
Text("Price").bold()
}
Divider().gridCellUnsizedAxes(.horizontal)
// Data rows
ForEach(plans, id: \.0) { plan, storage, price in
GridRow {
Text(plan)
Text(storage).foregroundStyle(.secondary)
Text(price).monospacedDigit()
}
}
}
.padding()
}
}
#Preview { PricingGrid() }
Pinterest-style uneven rows with LazyVGrid + pinned header
Pass a pinnedViews: [.sectionHeaders] argument to LazyVGrid and wrap your ForEach in a Section(header:). The section header sticks to the top of the scroll view as the user scrolls, exactly like the iOS Photos app's month headers. Each cell can have a variable height — just avoid .aspectRatio and let the cell size itself from its content.
Common pitfalls
-
Forgetting
ScrollView:LazyVGriddoes not scroll on its own — it sizes itself to fit all content. Always wrap it in aScrollViewor it will clip off-screen on large datasets. -
Using
Gridfor huge datasets: UnlikeLazyVGrid, the structuredGridview is eager — it renders every row immediately. Keep it for small, fixed tables (under ~50 rows). For large scrollable lists, always reach forLazyVGrid. -
Missing accessibility on image-only cells: A cell that is just a colored rectangle or remote image has no intrinsic accessibility description. Add
.accessibilityLabelto every cell so VoiceOver users get a meaningful name, not "Image" repeated 200 times.
Prompt this with Claude Code
When using Soarias or Claude Code directly to implement this:
Implement grid layout in SwiftUI for iOS 17+. Use Grid and LazyVGrid with adaptive, fixed, and flexible GridItem modes. Make it accessible (VoiceOver labels on every cell). Add a #Preview with realistic sample data (at least 20 items).
In Soarias's Build phase, paste this prompt directly into the code editor chat panel to scaffold the grid component, then use the visual preview to fine-tune column counts and spacing before pushing to TestFlight.
Related
FAQ
Does this work on iOS 16?
LazyVGrid is available from iOS 14+, so the scrollable grid examples work on iOS 16. However, the structured Grid view and the .gradient shorthand on Color require iOS 16+. The #Preview macro requires Xcode 15+ and iOS 17+; replace it with PreviewProvider if you must target iOS 16 specifically.
When should I use Grid vs LazyVGrid?
Use Grid for small, static tables where cross-row column alignment matters (e.g., pricing tables, form layouts, comparison charts). Use LazyVGrid for any scrollable, dynamically sized collection — photo galleries, app icon grids, product cards — especially when the item count is large or unknown at compile time. The lazy rendering of LazyVGrid is essential for scroll performance.
What's the UIKit equivalent?
LazyVGrid maps most closely to UICollectionView with a UICollectionViewFlowLayout (or the newer UICollectionViewCompositionalLayout for adaptive grids). The structured Grid view is closer to UIStackView nesting or a manual UICollectionViewLayout that enforces column alignment. SwiftUI's API is dramatically less boilerplate for both cases.
Last reviewed: 2026-05-11 by the Soarias team.