Anup Kumar Mishra
Anup Kumar Mishra

Reputation: 11

Design the dynamic Grid Layout

I want to design the Horizontal Grid based on condition I need when data comes from API when meet "HEADING" then should be only one cell in the whole row otherwise 4 cells.

This is my required view:

Required UI

And this is my approach: **Nte the Fetched Response is "evaluationKeysData": [ { "id": "basic", "description": "HEADING", "str_sort_order": "001" },

        {
            "id": "arm_left",
            "description": "Arm - Left",
            "str_sort_order": "010"
        },
        {
            "id": "wingspan",
            "description": "Wingspan",
            "str_sort_order": "011"
        },
        {
            "id": "measurement",
            "description": "HEADING",
            "str_sort_order": "012"
        },
        {
            "id": "neck",
            "description": "Neck",
            "str_sort_order": "013"
        },
        {
            "id": "chest",
            "description": "Chest",
            "str_sort_order": "014"
        },
        {
            "id": "hip",
            "description": "Hip",
            "str_sort_order": "015"
        },
        {
            "id": "waist",
            "description": "Waist",
            "str_sort_order": "016"
        },
        {
            "id": "thigh_right",
            "description": "Thigh - Right",
            "str_sort_order": "017"
        },
        {
            "id": "bicep_right",
            "description": "Bicep - Right",
            "str_sort_order": "018"
        },
        {
            "id": "hinge",
            "description": "Hinge",
            "str_sort_order": "019"
        },
        {
            "id": "bilateral",
            "description": "Bilateral",
            "str_sort_order": "020"
        },
         
        {
            "id": "r_abduction",
            "description": "R - Abduction",
            "str_sort_order": "027"
        },
        {
            "id": "l_abduction",
            "description": "L - Abduction",
            "str_sort_order": "028"
        },
         
        
        {
            "id": "vertical",
            "description": "Vertical",
            "str_sort_order": "039"
        },
        {
            "id": "reaction",
            "description": "Reaction",
            "str_sort_order": "040"
        },
        {
            "id": "10_yd",
            "description": "10 yd",
            "str_sort_order": "041"
        },
        {
            "id": "15_ft",
            "description": "15 ft",
            "str_sort_order": "042"
        },
        {
            "id": "20_yd",
            "description": "20 yd",
            "str_sort_order": "043"
        },
        {
            "id": "30_ft",
            "description": "30 ft",
            "str_sort_order": "044"
        },
        {
            "id": "mph",
            "description": "MPH",
            "str_sort_order": "045"
        },
        {
            "id": "40_yd",
            "description": "40 yd",
            "str_sort_order": "046"
        },
        {
            "id": "60_yd",
            "description": "60 yd",
            "str_sort_order": "047"
        },
        {
            "id": "75_ft",
            "description": "75 ft",
            "str_sort_order": "048"
        },
        {
            "id": "adjusted",
            "description": "Adjusted",
            "str_sort_order": "049"
        },
        {
            "id": "fly_5_/_10",
            "description": "Fly 5 / 10",
            "str_sort_order": "050"
        },
        {
            "id": "flying_10_/_10",
            "description": "Flying 10 / 10",
            "str_sort_order": "051"
        },
        {
            "id": "half",
            "description": "Half",
            "str_sort_order": "052"
        },
        {
            "id": "full",
            "description": "Full",
            "str_sort_order": "053"
        },
        {
            "id": "cmj_height",
            "description": "CMJ - Height",
            "str_sort_order": "054"
        },
         
        {
            "id": "broad_jump",
            "description": "Broad Jump",
            "str_sort_order": "060"
        },
        {
            "id": "l_drill_half",
            "description": "L - Drill - Half",
            "str_sort_order": "061"
        },
        {
            "id": "x-drill",
            "description": "X-Drill",
            "str_sort_order": "062"
        },
        {
            "id": "measurement_3",
            "description": "HEADING",
            "str_sort_order": "063"
        },
        {
            "id": "row_2000_m",
            "description": "Row - 2000 m",
            "str_sort_order": "064"
        },
        {
            "id": "row_500_m",
            "description": "Row - 500 m",
            "str_sort_order": "065"
        },
        {
            "id": "row_power",
            "description": "Row - Power",
            "str_sort_order": "066"
        },
        {
            "id": "hr_pre",
            "description": "HR - Pre",
            "str_sort_order": "067"
        } 
    ]**
ScrollView{
    LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], spacing: 2) {
        
        ForEach(bleGroupMeta.fetchedData) { index in
            
            if index.desc != "HEADING" {
                
                
                TextField("", text: Binding(
                    get: { userMeasurements[index.id].stringValue },
                    set: { newValue in
                        userMeasurements[index.id] = JSON(newValue)
                        let measurement = Measurement(measurement: index.id, value: newValue)
                        print(measurement)
                        if !modifiedMeasurements.contains(measurement) {
                            modifiedMeasurements.append(measurement)
                        }
                    }
                ) , prompt: Text(index.desc)
                    .font(Font.custom("Agency FB", size: 16))
                    .foregroundColor(Constants.White.opacity(0.8)))
                .padding(.leading)
                .keyboardType(.decimalPad)
                .font(Font.custom("Agency FB", size: 16))
                .foregroundColor(Constants.White.opacity(0.8))
                .background(Color.clear)
                .frame(height: 40)
                .frame(maxWidth: .infinity)
                .cornerRadius(4)
                .overlay(
                    RoundedRectangle(cornerRadius: 4)
                        .inset(by: 0.5)
                        .stroke(Color(red: 0.26, green: 0.27, blue: 0.3), lineWidth: 1)
                )
            } else {
                // Add the design for the heading
                VStack {
                    HStack(spacing: 20){
                        Rectangle().fill(Color.gray).frame(height: 1)
                        //                                        Spacer()
                        Text("Heading").font(Font.custom("Agency FB", size: 16)).foregroundColor(Constants.Orange)
                        //                                        Spacer()
                        Rectangle().fill(Color.gray).background(Color.gray).frame(height: 1)
                    }
                }.frame(maxWidth: .infinity)
            }
        }.padding()
    }
}

Upvotes: 0

Views: 307

Answers (2)

Jayant Badlani
Jayant Badlani

Reputation: 157

You can use the TokenLayout class to achieve a grid view. Change the width and spacing calculation according to your needs. I hope this solves your issue.

enter image description here

TokenLayout source code for GridView

struct TokenLayout: Layout {
    
    var alignment: Alignment = .center
    var horizontalSpacingBetweenItem: CGFloat = 10
    var verticalSpacingBetweenItem: CGFloat = 0
    
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        let maxWidth = proposal.width ?? 0
        var totalHeight: CGFloat = 0
        
        let rows = generateRows(maxWidth, proposal, subviews)
        
        for (index, _) in rows.enumerated() {
            totalHeight += rows[index].maxHeight(proposal)
            totalHeight += index == rows.count - 1 ? 0 : verticalSpacingBetweenItem
        }
        
        return CGSize(width: maxWidth, height: totalHeight)
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        var origin = CGPoint(x: bounds.origin.x, y: bounds.origin.y)
        
        let maxWidth = bounds.width
        let rows = generateRows(maxWidth, proposal, subviews)
        
        for (index, row) in rows.enumerated() {
            let totalWidth = row.reduce(0) { partialResult, view in
                let width = view.sizeThatFits(proposal).width
                return partialResult + width
            }
            let totalSpacing = CGFloat(row.count - 1) * horizontalSpacingBetweenItem
            let remainingSpace = maxWidth - totalWidth - totalSpacing
            
            switch alignment {
            case .leading:
                origin.x = bounds.minX
            case .trailing:
                origin.x = bounds.maxX - (totalWidth + totalSpacing)
            case .center:
                origin.x = bounds.minX + remainingSpace / 2
            default:
                break
            }
            
            for view in row {
                let viewSize = view.sizeThatFits(proposal)
                view.place(at: origin, proposal: proposal)
                origin.x += (viewSize.width + horizontalSpacingBetweenItem)
            }
            
            origin.y += (row.maxHeight(proposal) + (index == rows.count - 1 ? 0 : verticalSpacingBetweenItem))
        }
    }
    
    func generateRows(_ maxWidth: CGFloat, _ proposal: ProposedViewSize, _ subviews: Subviews) -> [[LayoutSubviews.Element]] {
        var row: [LayoutSubviews.Element] = []
        var rows: [[LayoutSubviews.Element]] = []
        var origin = CGRect.zero.origin
        
        for view in subviews {
            let viewSize = view.sizeThatFits(proposal)
            let totalWidth = origin.x + viewSize.width + (row.isEmpty ? 0 : horizontalSpacingBetweenItem)
            
            if totalWidth > maxWidth {
                rows.append(row)
                row.removeAll()
                origin.x = 0
            }
            
            row.append(view)
            origin.x += (viewSize.width + horizontalSpacingBetweenItem)
        }
        
        if !row.isEmpty {
            rows.append(row)
        }
        return rows
    }
}


extension Array where Element == LayoutSubviews.Element {
    func maxHeight(_ proposal: ProposedViewSize) -> CGFloat {
        return self.compactMap { view in
            return view.sizeThatFits(proposal).height
            
        }.max() ?? 0
    }
}

Example: How to use:

import SwiftUI


struct MyView: View {
    
    let itemsPerRow = 4
    let numberOfItem = 13
    let horizontalSpacingBetweenItem: CGFloat = 15
    
    var body: some View {
        
        TokenLayout(alignment: .leading, horizontalSpacingBetweenItem: horizontalSpacingBetweenItem, verticalSpacingBetweenItem: 20) {
            
            ForEach(0..<numberOfItem) { row in
                Rectangle()
                    .frame(width: ((UIScreen.main.bounds.width - 50) - CGFloat(itemsPerRow - 1) * horizontalSpacingBetweenItem) / CGFloat(itemsPerRow),
                           height: 40)
                    .foregroundColor(.clear)
                    .overlay(
                        RoundedRectangle(cornerRadius: 4)
                            .stroke(Color.gray, lineWidth: 1)
                    )
                    .overlay(
                        Text("Text")
                            .foregroundColor(.black)
                            .padding(8)
                            .multilineTextAlignment(.leading)
                            .frame(maxWidth: .infinity, alignment: .leading)
                    )
            }
        }
        .padding(.horizontal, 15)
    }
}

struct MyView_Previews: PreviewProvider {
    static var previews: some View {
        MyView()
    }
}

Upvotes: 0

Parth
Parth

Reputation: 636

You can call gridSwitch() function and set gridLayout with gridColumn.

@State private var gridLayout: [GridItem] = [GridItem(.flexible())]
@State private var gridColumn: Int = 1

func gridSwitch(isHeadingType: Bool) {
    gridColumn = isHeadingType ? 1 : 4
    gridLayout = Array(repeating: .init(.flexible()), count: gridColumn)
    gridColumn = gridLayout.count
}

ScrollView(.vertical, showsIndicators: false) {
    LazyVGrid(columns: gridLayout, alignment: .center, spacing: 10) {
        ForEach(bleGroupMeta.fetchedData) { item in
            NavigationLink(destination: DetailView(gridItem: item)) {
                DetailItemView(gridItem: item)
            }
        }//: Loop
    }//: Grid
}

Upvotes: 0

Related Questions