• SwiftUI


    简介

    在当前正式 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( 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

    微博:https://weibo.com/feiyueharia

    博客园:https://www.cnblogs.com/GarveyCalvin

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

    公众号

    欢迎关注我的公众号(对着月亮敲代码),获取往期文章阅读浏览,期待你们的关注!

    QQ群 / 微信群

    如需加群讨论(吃瓜),请加我 QQ ,本人将统一拉群,在此期待你们的加入!

  • 相关阅读:
    OptaPlanner实用技术 批量规划和实时规划(2)
    【Golang】创建有配置参数的结构体时,可选参数应该怎么传?
    k8s:Pod生命周期
    k8s:容器生命周期回调
    在工厂<> 中 <> 已经是个追加物料
    The headers or library files could not be found for jpeg
    从进入内核态看内存管理
    迅雷加速原来分析
    idea编译build一直卡在那不动解决方法
    python入门爬虫豆瓣电影Top250
  • 原文地址:https://www.cnblogs.com/GarveyCalvin/p/swiftui-grid-view.html
Copyright © 2020-2023  润新知