SwiftUI – Grid View 的實現方法,逐步剖析助你實現

簡介

在當前正式 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/GarveyCalvin

本文版權歸作者,歡迎轉載,但必須保留此段聲明,並給出原文鏈接,謝謝合作!

公眾號

歡迎關注我的公眾號(對著月亮敲程式碼),獲取往期文章閱讀瀏覽,期待你們的關注!

QQ群 / 微信群

如需加群討論(吃瓜),請加我 QQ ,本人將統一拉群,在此期待你們的加入!