SwiftUI - Grid View 的实现方法,逐步剖析助你实现

206 阅读4分钟

简介

在当前正式 SwiftUI 版本而言,很多控件都是缺少的。比如在 UIKit 框架里有 UICollectionView 组件,可以很方便地做 Gird 格子类型的视图。但是在 SwiftUI 这个框架里面,就没有对应 UICollectionView 的组件。我们当然可以用 UIViewRepresentable 来封装一个 UICollectionView ,但是本篇文章要探讨的是,如何使用 SwiftUI 来实现 Grid 格子视图,现在一起来实现吧。

实现思考

在思考前,我们先来定义生成随机颜色的函数,后面会用到的。

extension Double {
    static func randomData() -> Double {
        Double(arc4random()) / Double(UInt32.max)
    }
}

extension Color {
    static func random() -> Color {
        .init(red: Double.randomData(), green: Double.randomData(), blue: Double.randomData())
    }
}

想必 HStack 横向布局与 VStack 竖向布局你们已经掌握得很熟练了,比如竖向排列 6 个 Color 视图。

var data: [Color] {
    [
        Color.random(),
        Color.random(),
        Color.random(),
        Color.random(),
        Color.random(),
        Color.random()
    ]
}

var body: some View {
    VStack {
        ForEach(0..<data.count) { index in
            self.data[index]
        }
    }
}

有些童鞋会疑问,为什么颜色也能算是视图呢?这是因为在 SwiftUI 中,View 是一个协议,而 Color 也遵循了 View 协议,所以 Color 也是一个视图,可以直接在界面上展示它。

说回来现在的例子,效果长这样。

只需将上面代码里的 VStack 换成 HStack,就会变成这样,代码就不贴了,直接上效果图。

那么是不是可以通过组合 HStack 与 VStack 能够实现我们想要的 Grid 视图呢?答案是可以肯定的。

你们肯定发现了,视图的上方和下方出现了空白,这是因为 iPhoneX 及之后的版本存在安全边距,只需通过设置edgesIgnoringSafeArea方法,参数为vertical,代表的是忽略垂直方向的安全边距。

.edgesIgnoringSafeArea(.vertical)

Grid 实现

为了简单起见,我们先来打造一行三列的 Grid 视图。定义一个 View 取名为 GCRowView,视图的大小按照屏幕的宽度三分之一进行计算,这里的视图宽和高是一致的,代码如下所示,关键的代码我会标注数字,在后面进行讲解。

struct GCRowView: View {
    var itemPerRow = 3 // 1
    
    var views: [AnyView] = [ // 2
        AnyView(Image("1").resizable().aspectRatio(contentMode: .fill)),
        AnyView(Image("2").resizable().aspectRatio(contentMode: .fill)),
        AnyView(Image("3").resizable().aspectRatio(contentMode: .fill)),
    ]
    
    var itemWidth: CGFloat { // 3
        UIScreen.main.bounds.width / CGFloat(itemPerRow)
    }
    
    var body: some View {
        HStack(spacing: 0) { // 4
            ForEach(0..<views.count) { index in
                self.views[index]
                    .frame(width: self.itemWidth, height: self.itemWidth)
                    .clipped() // 5
            }
        }
    }
}

1 - 每一行有多少个视图。

2 - 展示的视图数组,存储的类型为 AnyView,后面可以直接取用视图。.resizable() 方法是为了让图片可以调整大小,.aspectRatio 设置为 .fill 是为了让图片保持原有的比例,并填满整个 frame。

3 - 计算每个视图的宽高。

4 - HStack 默认是有 spacing 的,这里的布局是一个视图贴着一个的,因此设为0。

5 - 图像超出部分进行裁剪。

现在的效果是这样的。

可以看到视图正确地显示出来了。

现在创建 GCGirdContentView ,在其内实现一些算法,分别是计算总共有多少行和每一行展示的具体视图。先来实现 rowCount(contentNums:itemPerRow:) 方法计算总行数,参数分别是视图总数每行的视图数量

func rowCount(contentNums: Int, itemPerRow: Int) -> Int {
    if contentNums % itemPerRow == 0 {
        return contentNums / itemPerRow
    }

    return contentNums / itemPerRow + 1
}

1 - 进行取余运算,余数为 0 则代表可以被整除

2 - 既然可以被整除,则可以直接计算商就可以了

3 - 若余数不为 0 ,则代表需要换行,因此除了计算商后还需要进行 +1

计算出每行排列的视图,返回视图数组,用于给 GCRowView 进行显示,方法的参数分别是当前行数每行的视图数量

func rowViews(currentRow: Int, itemPerRow: Int) -> [AnyView] {
    var views = [AnyView]()

    for i in 0..<itemPerRow { // 1
        let index = i + itemPerRow * currentRow // 2
        if index < contentViews.count { // 3
            views.append(contentViews[index]) // 4
        }
    }

    return views
}

1 - 循环遍历每行的视图数量

2 - 计算当前应该取出哪个视图

3 - 计算程序安全边界,若超出视图总数则忽略不计

4 - 取出视图并放入视图数组

接着把 GCRowView 封装得通用一点,把 itemPerRow 和 views 的默认值去除。

struct GCRowView: View {
    var itemPerRow: Int
    
    var views: [AnyView]
    
    //...

直到目前,我们已经完成了大部分的工作,现在来组装一下 GCGirdContentView 视图。

struct GCGirdContentView: View {
    var itemPerRow = 3
    
    var contentViews: [AnyView] = []
    
    init() { // 1
        for i in 1...12 {
            contentViews.append(AnyView(Image("\(i)").resizable().aspectRatio(contentMode: .fill)))
        }
    }
    
    var body: some View {
        VStack(alignment: .leading, spacing: 0) { // 2
            ForEach(0..<rowCount(contentNums: contentViews.count, itemPerRow: itemPerRow)) { i in // 3
                GCRowView(itemPerRow: self.itemPerRow, views: self.rowViews(currentRow: i, itemPerRow: self.itemPerRow)) // 4
            }
        }
    }
}

1 - 在 init 函数里初始化 contentViews ,加入需要展示的图像视图

2 - VStack 设置为左边对齐,行间距设为 0 ,让视图紧贴着彼此

3 - 遍历循环行数,用到了刚刚定义的 rowCount(contentNums:itemPerRow:) 方法

4 - 显示的 GCRowView 行视图,配合当前行 i 并利用 rowViews(currentRow:itemPerRow:) 方法计算出需要显示的具体视图组

现在运行,最终效果图如下所示。

总结

在 SwiftUI 里实现 Grid 其实不算是复杂,通过组合 HStack 与 VStack 就能够助我们实现 Grid 视图。

在最新的 SwiftUI Beta 版里,苹果推出了如 LazyVGrid、LazyHGrid、GridItem 来实现管理 Grid 视图,我们就拭目以待吧,后续有机会再来更新一波。

源码下载

我已经把源码 GCGridView 上传到 GitHub 上,往期所有的 Demo 源码皆放在了SwiftUI-Tutorials,欢迎自取。如果该项目帮到你的话,请给我个 Star 告知,谢谢!喜欢本篇文章的小伙伴,欢迎给个关注,后续继续更新更多文章,谢谢!

最后,欢迎关注我的公众号(凡人程序猿),获取往期文章阅读浏览,期待你们的关注!

关于作者

博文作者:GarveyCalvin

微博:weibo.com/feiyueharia

博客园:www.cnblogs.com/GarveyCalvi…

本文版权归作者,欢迎转载,但必须保留此段声明,并给出原文链接,谢谢合作!