```html SwiftUI: How to grid layout (iOS 17+, 2026)

How to implement grid layout in SwiftUI

iOS 17+ Xcode 16+ Beginner APIs: Grid / LazyVGrid Updated: May 11, 2026
TL;DR

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

  1. GridItem(.adaptive(minimum:maximum:)) — tells SwiftUI to pack as many columns as fit the available width, with each column between minimum and maximum points wide. This single-element array is all you need for a responsive gallery; SwiftUI recalculates column count automatically on rotation or split-view resize.
  2. 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. The spacing parameter controls vertical gap between rows; horizontal gaps between columns are set per-GridItem via its spacing argument.
  3. .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.
  4. .animation(.easeInOut, value: style) — animates the column layout change when the user taps a different segment. Because columns is a value type derived from the style enum, SwiftUI diffing catches the change and cross-fades the relayout smoothly.
  5. .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

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.

```