How to implement a grid of three items in collectionView

Viewed 816

I am trying to implement grid layout in collectionView. This is my current view enter image description here

instead of 1 item per collection I would like to show 3 items

this is my productView

struct ProductSearchView: View {
@Environment(\.presentationMode) var presentationMode

 @ObservedObject var model: SearchResultViewModel
var body: some View{
    NavigationView {
         List {
                ForEach(0 ..< Global.productArry.count) { value in
                  Text(Global.productArry[value].name)
                    CollectionView(model: self.model, data: Global.productArry[value])
                }
                
         }.navigationBarTitle("Store")
    }


}

}

and this is my collection view

struct CollectionView: View {
@ObservedObject var model: SearchResultViewModel

let data: Product
    var body: some View {
        VStack {
            HStack {
                Spacer()
                    AsyncImage(url: URL(string: self.data.productImageUrl)!, placeholder: Text("Loading ...")
                    ).aspectRatio(contentMode: .fit)
                Spacer()
               
            }
            VStack {
                Spacer()
                Text(self.data.name)
                Spacer()
                
            }
            HStack {
                Text("Aisle: \(self.data.location_zone)\(String(self.data.aisleNo))").bold()
                Text("$\(String(self.data.productPrice))")
            }
        }.onAppear(perform:thisVal)
     }

func thisVal (){
    
    print(self.data.productImageUrl)
    
}

}

how can I implement a grid of three items ?

3 Answers

Use a LazyVGrid. The following example is very simple and easy to read and you can change it to anything you need.

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) { _ in
                    Circle().scaledToFill()
                }
            }
            .padding(.horizontal)
        }
    }
}

Demo

The code snippet below is what you need if you are using SwiftUI version 1 which came with Xcode 11. It is a custom implementation which simply use a combination of ScrollView, VStack and HStack as well as a little maths to calculate the frame size depending on the number of items.

import SwiftUI

struct GridView<Content, T>: View where Content: View {
    // MARK: - Properties
    var totalNumberOfColumns: Int
    var numberRows: Int {
        return (items.count - 1) / totalNumberOfColumns
    }
    var items: [T]
    /// A parameter to store the content passed in the ViewBuilder.
    let content: (_ calculatedWidth: CGFloat,_ type: T) -> Content
    
    // MARK: - Init
    init(columns: Int, items: [T], @ViewBuilder content: @escaping(_ calculatedWidth: CGFloat,_ type: T) -> Content) {
        self.totalNumberOfColumns = columns
        self.items = items
        self.content = content
    }
    
    // MARK: - Helpers
    /// A function which help checking if the item exist in the specified index to avoid index out of range error.
    func elementFor(row: Int, column: Int) -> Int? {
        let index:Int = row * self.totalNumberOfColumns + column
        return index < items.count ? index : nil
    }
    
    // MARK: - Body
    var body: some View {
        GeometryReader { geometry in
            ScrollView{
                VStack {
                    ForEach(0...self.numberRows,id: \.self) { (row) in
                        HStack {
                            ForEach(0..<self.totalNumberOfColumns) { (column) in
                                Group {
                                    if (self.elementFor(row: row, column: column) != nil)  {
                                        self.content(geometry.size.width / CGFloat(self.totalNumberOfColumns), self.items[self.elementFor(row: row, column: column)!])
                                            .frame(width: geometry.size.width / CGFloat(self.totalNumberOfColumns), height: geometry.size.width / (CGFloat(self.totalNumberOfColumns)), alignment: .center)
                                    }else {
                                        Spacer()
                                    }
                                }
                            }
                        }
                        
                    }
                }
            }
        }
    }
}


Usage: It is generic ready to use with any view of your choice. For instance the Preview code below uses the system images.

struct GridView_Previews: PreviewProvider {
    static var previews: some View {
        GridView(columns: 3, items: ["doc.text.fill","paperclip.circle.fill", "globe","clear.fill","sun.min.fill", "cloud.rain.fill", "moon","power"], content: { gridWidth,item  in
            Image(systemName: item)
                .frame(width: gridWidth, height: gridWidth, alignment: .center)
                .border(Color.orange)
        })
        .padding(10)
    }
}

Output OutputImage

You can change the code to fit your needs for example paddings etc.

NOTE: Use this method only if you don't have lots of views as it is not very performant. If you have a lot of views to scroll I suggest you use the newer and simple build-in GridItem from Apple which was introduced this year at WWDC. But you will have to use Xcode 12 which currently is available as Beta version.

put for loop logic in HStack like givern below.

HStack {
ForEach(0..<3) { items in
Spacer()
   AsyncImage(url: URL(string: self.data.productImageUrl)!, placeholder: Text("Loading ...")
                ).aspectRatio(contentMode: .fit)
Spacer()
}.padding(.bottom, 16)
}
Related