Centered "CollectionView" in SwiftUI

Viewed 853

Apple introduced the way to make CollectionViews in SwiftUI by using the new LazyVGrid and LazyHGrid embedded inside an ScrollView.

But if the last row have less elements than number of columns, the items appear aligned to the leading. It is possible to align the last row items to the .center?

Swift 5.3 - SwiftUI 2.0 - Xcode 12.0b - macOS 11 Big Sur

1 Answers

I don't know if that's possible within a LazyGrid, but here is a possible workaround:

You could simply put the last item inside a VStack and align it centered, whenever the number of items in your data array is uneven.

I have implemented a demo for you:

Simple:

import SwiftUI

//MARK: - Content

struct ContentView: View {
    //Your data
    let data = Array(0...4)
    
    let columns = [
        GridItem(.fixed(160)),
        GridItem(.fixed(160))
    ]
    
    //Same spacing both for items inside grid and between grid and stack
    let rowSpacing: CGFloat = 32
        
    //If number of items is odd, remove the last one from grid and add to stack
    var gridData: [Int] { data.count%2 == 1 ? data.dropLast() : data }
    var stackData: Int? { data.count%2 == 1 ? data.last : nil }
    
    var body: some View {
        ScrollView {
            VStack(spacing: rowSpacing) {
                LazyVGrid(columns: columns, spacing: rowSpacing) {
                    ForEach(gridData, id: \.self) { i in
                        ItemView(i: i)
                    }
                }
                if let data = stackData {
                    VStack {
                        ItemView(i: data)
                    }
                }
            }
        }
    }
}

//MARK: - Item

struct ItemView: View {
    let i: Int
    
    var body: some View {
        Rectangle()
            .frame(width: 160, height: 240)
            .foregroundColor(Color.green)
            .overlay(Text(String(i)).foregroundColor(.white))
    }
}

Alternative, reusable:

//MARK: - Data

struct SampleData: Identifiable {
    let id: Int
    var text: String
}


//MARK: - View

struct ContentView: View {
    //Your data
    let data = [SampleData(id: 0, text: "A"), SampleData(id: 1, text: "B"), SampleData(id: 2, text: "C")]
    
    let columns = [
        GridItem(.fixed(160)),
        GridItem(.fixed(160))
    ]
    
    var body: some View {
        ScrollView {
            CenteredLazyVGrid(data, columns: columns, spacing: 32) { i in
                ItemView(i: i.id)
            }
        }
    }
}

//MARK: - Item

struct ItemView: View {
    let i: Int
    
    var body: some View {
        Rectangle()
            .frame(width: 160, height: 240)
            .foregroundColor(Color.green)
            .overlay(Text(String(i)).foregroundColor(.white))
    }
}

//MARK: - Centered Grid View

struct CenteredLazyVGrid<Data, Content>: View where Data: RandomAccessCollection, Content: View, /*Data: Hashable, */Data.Element: Identifiable {
    private var data: Data
    //private var id: KeyPath<Data.Element, ID>
    private var columns: [GridItem]
    private var alignment: HorizontalAlignment = .center
    private var spacing: CGFloat? = nil
    private var pinnedViews: PinnedScrollableViews = []
    private var content: (Data.Element) -> Content
    
    init(_ data: Data, /*id: KeyPath<Data.Element, ID>, */columns: [GridItem], alignment: HorizontalAlignment = .center, spacing: CGFloat?
            = nil, pinnedViews: PinnedScrollableViews = .init(), content: @escaping (Data.Element) -> Content) {
        self.data = data
        //self.id = id
        self.columns = columns
        self.alignment = alignment
        self.spacing = spacing
        self.pinnedViews = pinnedViews
        self.content = content
    }
    
    private var gridData: [Data.Element] { data.count%2 == 1 ? data.dropLast() : data as! [Data.Element] }
    private var stackData: Data.Element? { data.count%2 == 1 ? data.last : nil }
    
    var body: some View {
        VStack(spacing: spacing) {
            LazyVGrid(columns: columns, alignment: alignment, spacing: spacing, pinnedViews: pinnedViews) {
                ForEach(gridData/*, id: id*/) { i in
                    content(i)
                }
            }
            if let data = stackData {
                VStack {
                    content(data)
                }
            }
        }
    }
}
Related