BLACK FRIDAY SALE: Save big on all my Swift books and bundles! >>

How to position views in a fixed grid

Paul Hudson    @twostraws   

Updated for Xcode 14.1

New in iOS 16

SwiftUI’s Grid view lets us create a static grid of views, with precise control over what goes into each row and column. You mark out individual rows using GridRow, then optionally also configure how wide each cell should be.

As a basic example, this creates a 2x2 grid with text reflecting where each cell with be positioned:

Grid {
    GridRow {
        Text("Top Leading")
            .background(.red)

        Text("Top Trailing")
            .background(.orange)
    }

    GridRow {
        Text("Bottom Leading")
            .background(.green)

        Text("Bottom Trailing")
            .background(.blue)
    }
}
.font(.title)

Download this as an Xcode project

If you don’t want to have the same number of cells in each row, you have three choices.

First, if you do nothing, SwiftUI will automatically insert empty cells to make sure the rows are equal. So, in this code we can add to the red and blue scores freely, and SwiftUI will keep the whole thing balanced:

struct ContentView: View {
    @State private var redScore = 0
    @State private var blueScore = 0

    var body: some View {
        Grid {
            GridRow {
                Text("Red")

                ForEach(0..<redScore, id: \.self) { _ in
                    Rectangle()
                        .fill(.red)
                        .frame(width: 20, height: 20)
                }
            }

            GridRow {
                Text("Blue")

                ForEach(0..<blueScore, id: \.self) { _ in
                    Rectangle()
                        .fill(.blue)
                        .frame(width: 20, height: 20)
                }
            }
        }
        .font(.title)

        Button("Add to Red") { redScore += 1 }
        Button("Add to Blue") { blueScore += 1 }
    }
}

Download this as an Xcode project

The second option is to place views into the grid without wrapping them in a GridRow, which will cause them to occupy a whole row by themselves. This is great for the Divider view. The third option is to use the gridCellColumns() modifier, to make one cell span multiple columns.

We can see the second and third option in a single code sample:

Grid {
    GridRow {
        Text("Food")
        Text("$200")
    }

    GridRow {
        Text("Rent")
        Text("$800")
    }

    GridRow {
        Text("Candles")
        Text("$3600")
    }

    Divider()

    GridRow {
        Text("$4600")
            .gridCellColumns(2)
            .multilineTextAlignment(.trailing)
    }

}
.font(.title)

Download this as an Xcode project

As you can see, using gridCellColumns() with the same number of columns you have yields the same result as placing a view outside of a GridRow.

Important: Unlike LazyHGrid and LazyVGrid, a plain Grid loads all its views immediately, so be careful how much work you do.

Grids are fantastic choices when you need exact layouts – we can use them to make a tic-tac-toe board:

Grid(horizontalSpacing: 20, verticalSpacing: 20) {
    GridRow {
        Image(systemName: "xmark")
        Image(systemName: "xmark")
        Image(systemName: "xmark")
    }

    GridRow {
        Image(systemName: "circle")
        Image(systemName: "xmark")
        Image(systemName: "circle")
    }

    GridRow {
        Image(systemName: "xmark")
        Image(systemName: "circle")
        Image(systemName: "circle")
    }
}
.font(.largeTitle)

Download this as an Xcode project

Or even a chessboard:

struct ContentView: View {
    var body: some View {
        Grid(horizontalSpacing: 0, verticalSpacing: 0) {
            ForEach(0..<8) { row in
                GridRow {
                    ForEach(0..<8) { col in
                        if (row + col).isMultiple(of: 2) {
                            Rectangle()
                                .fill(.black)
                        } else {
                            Rectangle()
                                .fill(.white)
                        }
                    }
                }
            }
        }
        .aspectRatio(1, contentMode: .fit)
        .border(.black, width: 1)
        .padding()
    }
}

Download this as an Xcode project

Hacking with Swift is sponsored by RevenueCat

SPONSORED In-app subscriptions are a pain to implement, hard to test, and full of edge cases. RevenueCat makes it straightforward and reliable so you can get back to building your app. Oh, and it's free if your app makes less than $10k/mo.

Learn more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Similar solutions…

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 5.0/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.