How to create grid in SwiftUI

Viewed 6193

I know that we can create a List in vertical SwiftUI like this,

struct ContentView : View {
    var body: some View {
        NavigationView {
            List {
                Text("Hello")
            }
        }
    }
}

but is there any way that we could split the list in 2 or 3 or maybe more spans that covers the screen like a grid like we did in UICollectionView

5 Answers

Checkout ZStack based example here

Grid(0...100) { _ in
    Rectangle()
        .foregroundColor(.blue)
}

enter image description here

iOS 14

There is 2 new native Views that you can use:

  1. LazyHGrid HGrid

  2. LazyVGrid VGrid

With code or directly from the library:

Preview

The library contains a fully working sample code that you can test yourself.

You can create your customView like this to achieve UICollectionView behavior:-

struct ContentView : View {
    var body: some View {
        VStack(alignment: .leading, spacing: 10) {
            ScrollView(showsHorizontalIndicator: true) {
                HStack {
                    ForEach(0...10) {_ in
                        GridView()
                    }
                }
            }
            List {
                ForEach(0...5) {_ in
                    ListView()
                }
            }
            Spacer()
        }
    }
}

struct ListView : View {
    var body: some View {
        Text(/*@START_MENU_TOKEN@*/"Hello World!"/*@END_MENU_TOKEN@*/)
        .color(.red)
    }
}

struct GridView : View {
    var body: some View {
        VStack(alignment: .leading, spacing: 10) {
            Image("marker")
                .renderingMode(.original)
                .cornerRadius(5)
                .frame(height: 200)
                .border(Color.red)
            Text("test")
        }
    }
}

enter image description here

Available for iOS/iPadOS 14 on Xcode 12. You can use LazyVGrid to load just what the user see into screen and not the whole list, List is lazy by default.

import SwiftUI

//MARK: - Adaptive
struct ContentView: View {
    
    var body: some View {
        ScrollView {
            LazyVGrid(columns: [GridItem(.adaptive(minimum:100))]) {
                ForEach(yourObjects) { object in
                    YourObjectView(item: object)
                }
            }
        }
    }
}

//MARK: - Custom Columns
struct ContentView: View {
        
    var body: some View {
         ScrollView {   
             LazyVGrid(columns: Array(repeating: GridItem(), count: 4)) {
                 ForEach(yourObjects) { object in
                     YourObjectView(item: object)
                 }
             }
         }
    }
}

Don't forget replace the info objects with your info and YourObjectView with your customView.

SwiftUI’s LazyVGrid and LazyHGrid give us grid layouts with a fair amount of flexibility.

The simplest possible grid is made up of three things: your raw data, an array of GridItem describing the layout you want, and either a LazyVGrid or a LazyHGrid that brings together your data and your layout.

For example, this will create a vertical grid layout using cells that are 80 points in size:

struct ContentView: View {
    let data = (1...100).map { "Item \($0)" }

    let columns = [
        GridItem(.adaptive(minimum: 80))
    ]

    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns, spacing: 20) {
                ForEach(data, id: \.self) { item in
                    Text(item)
                }
            }
            .padding(.horizontal)
        }
        .frame(maxHeight: 300)
    }
}

enter image description here

Using GridItem(.adaptive(minimum: 80)) means we want the grid to fit in as many items per row as possible, using a minimum size of 80 points each.

If you wanted to control the number of columns you can use .flexible() instead, which also lets you specify how big each item should be but now lets you control how many columns there are. For example, this creates five columns:

struct ContentView: View {
    let data = (1...100).map { "Item \($0)" }

    let columns = [
        GridItem(.flexible()),
        GridItem(.flexible()),
        GridItem(.flexible()),
        GridItem(.flexible()),
        GridItem(.flexible())
    ]

    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns, spacing: 20) {
                ForEach(data, id: \.self) { item in
                    Text(item)
                }
            }
            .padding(.horizontal)
        }
        .frame(maxHeight: 300)
    }
}

enter image description here

A third option is to use fixed sizes. For example, this will make the first column be exactly 100 points wide, and allow the second column to fill up all the remaining space:

struct ContentView: View {
    let data = (1...100).map { "Item \($0)" }

    let columns = [
        GridItem(.fixed(100)),
        GridItem(.flexible()),
    ]

    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns, spacing: 20) {
                ForEach(data, id: \.self) { item in
                    Text(item)
                }
            }
            .padding(.horizontal)
        }
        .frame(maxHeight: 300)
    }
}

enter image description here

You can also use LazyHGrid to make a horizontally scrolling grid, which works in much the same way except it accepts rows in its initializer.

For example, we could create 10 side by side heading images that are horizontally scrolling like this:

struct ContentView: View {
    let items = 1...50

    let rows = [
        GridItem(.fixed(50)),
        GridItem(.fixed(50))
    ]

    var body: some View {
        ScrollView(.horizontal) {
            LazyHGrid(rows: rows, alignment: .center) {
                ForEach(items, id: \.self) { item in
                    Image(systemName: "\(item).circle.fill")
                        .font(.largeTitle)
                }
            }
            .frame(height: 150)
        }
    }
}

enter image description here

As you can see, the code required to create horizontal and vertical grids is almost the same, changing just rows for columns.

If you’re required to support iOS 13 you won’t have access to LazyHGrid or LazyVGrid, so read below for an alternative…

SwiftUI gives us VStack for vertical layouts and HStack for horizontal layouts, but nothing that does both – nothing that can lay out views in a grid structure.

Fortunately we can write one ourselves by leveraging SwiftUI’s view builder system. This means writing a type that must be created using a row and column count, plus a closure it can run to retrieve the views for a given cell in the grid. Inside the body it can then loop over all the rows and columns and create cells inside VStack and HStack to make a grid, each time calling the view closure to ask what should be in the cell.

In code it looks like this:

struct GridStack<Content: View>: View {
    let rows: Int
    let columns: Int
    let content: (Int, Int) -> Content

    var body: some View {
        VStack {
            ForEach(0 ..< rows, id: \.self) { row in
                HStack {
                    ForEach(0 ..< columns, id: \.self) { column in
                        content(row, column)
                    }
                }
            }
        }
    }

    init(rows: Int, columns: Int, @ViewBuilder content: @escaping (Int, Int) -> Content) {
        self.rows = rows
        self.columns = columns
        self.content = content
    }
}

// An example view putting GridStack into practice.
struct ContentView: View {
    var body: some View {
        GridStack(rows: 4, columns: 4) { row, col in
            Image(systemName: "\(row * 4 + col).circle")
            Text("R\(row) C\(col)")
        }
    }
}

enter image description here

That creates a 4x4 grid with an image and text in each cell.

Happy Coding ;)

Related