如何在 SwiftUI 中制作带有垂直和水平分隔线的分页网格

问题描述 投票:0回答:1

我希望在 SwiftUI 中制作一个分页网格视图,并在水平和垂直方向上连续分隔,但一直遇到困难。这就是我想要的: enter image description here

这是我想出的最好的:

struct ContentView: View {
    let rows = [
        GridItem(.fixed(90)),
        GridItem(.fixed(74))
    ]
    
    var body: some View {
        TabView {
            // Page 1
            pageView(startIndex: 0)
            
            // Page 2
            pageView(startIndex: 8)
        }
        .tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
    }
    

    func pageView(startIndex: Int) -> some View {
        ScrollView(.horizontal, showsIndicators: false) {
            
            LazyHGrid(rows: rows, spacing: 0) {
                ForEach(startIndex..<startIndex+8, id: \.self) { index in
                    VStack(spacing: 0) {
                        if !index.isMultiple(of: 2) {
                            Divider()
                                .frame(height: 2)
                                .overlay(Color.gray)
                        }
                        
                        HStack(spacing: 0) {
                            Spacer()
                            Divider()
                                .frame(width: 2)
                                .overlay(Color.gray)
                            Spacer()
                            RoundedRectangle(cornerRadius: 10)
                                .fill(Color.blue)
                                .frame(width: 80, height: 80)
                        }
                    }
                }
            }
        }
    }
}

这并不能真正按原样工作,而且使它看起来像这样的部分很糟糕。例如,行实际上应该具有相同的高度,但我发现将第二行变小会使垂直分隔线接触。在 SwiftUI 中实现这种效果的最佳方法是什么?

swift swiftui swiftui-table swiftui-grid swiftui-divider
1个回答
0
投票

获得网格分页滚动的一个简单方法是使用

.scrollTargetBehavior(.paging)
。其结构如下:

ScrollView(.horizontal, showsIndicators: false) {
    LazyHGrid(rows: rows) {
        //...
    }
    .scrollTargetLayout()
}
.scrollTargetBehavior(.paging)

所以我建议删除

TabView
并创建一个网格来显示所有项目。

对于网格线,显示线条的一种方法是在单元格后面的背景中显示它们,使用负填充延伸到单元格之间的间距。

这里是更新的示例,展示了它的工作方式。更多注释如下。

struct ContentView: View {
    static private let spacing: CGFloat = 18
    private let lineWidth: CGFloat = 2
    private let rows = [
        GridItem(.fixed(90), spacing: Self.spacing),
        GridItem(.fixed(74), spacing: Self.spacing)
    ]

    var body: some View {
        let indices = Array(0..<100)
        return ScrollView(.horizontal, showsIndicators: false) {
            LazyHGrid(rows: rows, spacing: Self.spacing) {
                ForEach(Array(indices.enumerated()), id: \.offset) { offset, index in
                    RoundedRectangle(cornerRadius: 10)
                        .fill(.blue)
                        .frame(width: 80.25)
                        .overlay {
                            Text("\(index)")
                                .font(.title)
                                .foregroundStyle(.white)
                        }
                        .background {
                            gridLines(cellIndex: offset, nItems: indices.count)
                        }
                }
            }
            .scrollTargetLayout()
            .padding(.horizontal, Self.spacing / 2)
        }
        .scrollTargetBehavior(.paging)
    }

    private func gridLines(cellIndex: Int, nItems: Int) -> some View {
        let nRows = rows.count
        let nCols = Int((Double(nItems) / Double(nRows)).rounded(.up))
        let isFirstRow = cellIndex % nRows == 0
        let isLastRow = cellIndex % nRows == nRows - 1
        let isFirstCol = cellIndex < nRows
        let isLastCol = cellIndex / nRows == nCols - 1
        let negativePadding = -((Self.spacing + lineWidth) / 2)
        return ZStack {
            if !isLastRow {
                Color.gray
                    .frame(height: lineWidth)
                    .frame(maxHeight: .infinity, alignment: .bottom)
            }
            if !isLastCol {
                Color.gray
                    .frame(width: lineWidth)
                    .frame(maxWidth: .infinity, alignment: .trailing)
            }
        }
        .padding(.leading, isFirstCol ? 0 : negativePadding)
        .padding(.trailing, isLastCol ? 0 : negativePadding)
        .padding(.top, isFirstRow ? 0 : negativePadding)
        .padding(.bottom, isLastRow ? 0 : negativePadding)
    }
}

Animation

备注:

  • 是否在背景中添加负填充并显示分隔线取决于它是第一行还是最后一行,还是第一列还是最后一列。这需要知道行数、项目总数以及项目在整个数组中的索引。

  • 如果要防止页面在滚动时“漂移”,则(单元格宽度+间距)的大小需要正好除以屏幕宽度。例如,在 iPhone 16 上,屏幕宽度为 393 点,因此单元格宽度为 80.25 且间距为 18 会给出精确的页面(如上所用)。

  • 提供给

    spacing
    的参数
    LazyHGrid
    仅用于水平间距。要控制垂直间距,请将
    spacing
    作为参数添加到
    GridItem

  • 示例中单元格的内容基本上只是一个

    Color
    。颜色是贪婪的,所以它们使用了所有可用的空间。这很重要,因为单元格背景的大小由单元格内容决定。

  • 如果单元格内容不是贪婪的,例如如果它只是

    Text
    ,则网格线将不会位于正确的位置。解决方法是使用
    Color.clear
    作为主要单元格内容,然后将文本显示为叠加层。

  • 为了让细胞系发挥作用,细胞内容物适合细胞内部也很重要。在最初的示例中,您将内容的高度设置为 80。这溢出了第二行中单元格的可用高度,因为您已为第二行的

    GridItem
    指定了固定高度 74。在上面更新的示例中,单元格的高度由网格决定。

  • 当您使用

    TabView
    时,您还有可见的页面指示器。这里的解决方案中缺少指标。一种实现方法是添加一个状态变量来跟踪
    .scrollPosition
    ,然后从当前选定的索引中导出页码。然后,您可以将自己的指标显示为叠加层,就像在这个答案中所做的那样。但是,要实现此功能,您需要知道屏幕的大小,以便可以计算当前页面的索引。

© www.soinside.com 2019 - 2024. All rights reserved.