2024-08-18|閱讀時間 ‧ 約 22 分鐘

使用SwiftUI Grid實現Bento Grid UI,提升資訊組織與視覺美感

自從我開始從事APP設計開發以來,每年我都會關注Apple的WWDC。這個系列活動會介紹針對不同操作系統的最新軟體與科技。自WWDC20起,Apple將一個主題的所有重點整合在一個畫面中呈現。內容以圓角矩形卡片的形式展現,每張卡片傳達一個特點,卡片大小會根據資訊內容或權重進行調整,並基於網格系統進行動態有序的排版。下圖便是今年WWDC24中關於iOS18最新資訊的介紹。

raw-image

這種Apple慣於用於宣傳新產品特點的排版手法,便是自2023年起流行的Bento Grid設計,中文稱為”便當盒設計“。


Table of contents

  • Bento Grid
  • SwiftUI Grid
  • SwiftUI Bento Grid
  • Conclusion


Bento Grid

Bento Grid的起源

如其名稱所示,Bento Grid 的靈感來自日本便當(弁当,bento)盒的分格方式。其特色在於以四方結構為間隔,提供多種菜餚在有限空間內各自擁有獨立的放置區域。這不僅提升了菜餚的承裝數量,也使食用者能一覽所有菜色;在進食過程中,也能方便且優雅地夾取食物。

日本便當展現了實用性與簡潔優雅的設計,體現出純淨的美感,是機能與美感兼具的經典設計範例。這種以使用者(食用者)的角度出發,考量產品整體使用過程(從承裝到食用的體驗)的設計理念,正是「形隨機能」(Form Follows Function)的體現。


Bento Grid 的特點

將便當盒的分隔手法轉化到介面設計上,可以將頁面分隔成大小不一的方塊,以呈現不同的內容。這種設計方式對設計師和使用者都具有以下優點:

模組化與彈性佈局(High Modularity and Flexibility)

  • Bento Grid 中的每個方塊(或稱模組)都可以自由調整大小、形狀和位置,為設計師提供了一個既有規則又不受限制的佈局框架。
  • 當設計師需要在一個畫面上呈現大量資訊時,格狀卡片佈局有助於清晰劃分資訊,並根據內容的重要性和視覺層級靈活調整方塊的大小與排列。

視覺層次與引導(Clear Visual Hierarchy and Guidance)

  • 井然有序的模組化佈局使使用者能快速掃描頁面,找到所需資訊。
  • 透過方塊的大小、位置和顏色的運用,能有效引導使用者的視線至重要內容,形成清晰的視覺層級。
  • 透過方塊排列方式的規劃,可以將相關內容歸類在一起,設計出清晰的視覺路徑,引導使用者的視線流動。


Bento Grid 的設計原則

讓我們以設計師實事求是的精神來檢視Apple WWDC24產品特點總覽的畫面設計,看看它是否遵循了Grid佈局原則。在這裡,我使用Figma的Layout grid進行比對。新增一個Frame元件,並設置畫面尺寸為1194 x 670。接著,在Layout grid中分別新增Columns和Rows:

  • Columns:將Count設置為20,Margin(邊界)和Gutter(分欄間距)各設置為10。
  • Rows:將Count設置為12,Margin(邊界)和Gutter(分欄間距)各設置為10。

同樣地,當你在設計Bento Grid時,可以善用Layout grid來作為網格佈局的參考。

Figma Grid Layout

可以確定的是,WWDC24產品頁面中的網格完全遵循了Layout grid的系統,並根據資訊內容的層級進行了大小不一的變化。


SwiftUI Grid

SwiftUI 的 Grid 是一種佈局容器,類似於文書工具中常見的表格,可以輕鬆地將內容組織成行(Rows)和列(Columns)。Grid 由三個主要構成元素組成,以下是其關鍵概念的說明:

  1. GridGrid 可以根據螢幕尺寸動態調整內容佈局,自動調整行與列的排列,以適應各種設備和狀況。其中,alignment 參數用於設定列(Columns)的對齊位置;horizontalSpacingverticalSpacing 參數則相當於分欄間距的概念。
  2. GridRowGridRow 用於管理單行中的所有列內容,並可通過新增 GridRow 來增加行(Rows)的數量。
  3. Column:在SwiftUI中無需特別定義Column,因為每個容器(如TextImageShape)本身就充當一個Column。
Grid(alignment: .center, horizontalSpacing: gutter, verticalSpacing: gutter) {
    GridRow {
        Text("Row 1 / Column 1")
        Text("Row 1 / Column 2")
    }
    GridRow {
        Text("Row 2 / Column 1")
        Text("Row 2 / Column 2")
    }
}

SwiftUI Grid


Grid配置

當然,SwiftUI 提供了一些網格修飾符,可以讓我們設計出更具彈性和進階功能的表格。以下是兩個我實際使用過的配置功能:

gridCellColumn:使用 .gridCellColumns(_:) 修飾符可以讓某個網格橫跨多列,實現類似於 Word 中的跨欄或 Google Doc 中的合併儲存格效果。

GridRow {
    Text("Row 1 / Column 1")
        .padding()
        .background(Color.gray, in: RoundedRectangle(cornerRadius: 10))
        .gridCellColumns(2)
}

gridCellColumns(2) 內的參數2代表著"Column 1"橫跨2個欄位,加上 "Column 2"佔據1個Column,整個GridRow則創造了3個Column。這個概念對於稍後要實現的Bento Grid是關鍵的配置功能。

SwiftUI Grid gridCellColumn


gridCellUnsizedAxes:如果需要將網格的高度或寬度設置為無限大,以填滿整個欄位空間,可以使用 gridCellUnsizedAxes(_:) 修飾符來指定網格的適當大小。否則,網格可能會無限延伸,佔據整個屏幕。

GridRow {
    Text("Row 1 / Column 1")
        .padding()
        .frame(maxWidth: .infinity)
        .gridCellUnsizedAxes(.horizontal)
        .background(Color.gray, in: RoundedRectangle(cornerRadius: 10))
        .gridCellColumns(2)
}

對於 "Row 1 / Column 1",使用了 .frame(maxWidth: .infinity) 並搭配 gridCellUnsizedAxes(.horizontal),以指定其寬度必須根據內容適當調整。這樣,網格只會填滿2個欄位的寬度,而不會無限延伸。

SwiftUI Grid gridCellUnsizedAxes


SwiftUI Bento Grid

接下來,我將展示如何使用SwiftUI Grid來實現Bento Grid。我們將以Apple WWDC24的Bento Grid佈局作為參考,並給每個網格區塊分配一個英文編號以便識別。

WWDC Bento Grid


局部佈局

為了容易理解,讓我們先實作Grid B 到Grid F的局部佈局。

Grid(alignment: .leading, horizontalSpacing: gutter, verticalSpacing: gutter) {
    //GridRow that contains GridCard B&C and GridCard D
    GridRow {
        VStack(spacing: gutter) {
            GridCard("B")
                .frame(height: cellSize?.height)
            GridCard("C")
                .frame(height: cellSize?.height)
        }
        .gridCellColumns(6)

        GridCard("D")
            .gridCellColumns(4)
            .frame(height: gridCellRows(4))
    }

    //GridRow that contains GridCard E and GridCard F
    GridRow {
        GridCard("E")
            .gridCellColumns(2)
            .readSize { value in
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
                    //Use PreferenceKey to read the basic grid width
                    cellSize = CGSize(width: value.width, height: value.width)
                }
            }
  

        GridCard("F")
            .gridCellColumns(8)
    }
    .frame(height: gridCellRows(4))
}
  1. 建立Grid容器:設置 horizontalSpacingverticalSpacing,這些間距參數等於gutter的概念。
  2. 建立第一個GridRow:此列包含2個欄位,分別是GridCard B與C的堆疊和GridCard D。GridCard B與C的堆疊欄寬佔據整體Layout的6個網格,設置 .gridCellColumns(6)。GridCard D的欄寬佔據整體Layout的4個網格,設置 .gridCellColumns(4);列高佔據4個網格,設置 .frame(height: gridCellRows(4))
  3. 建立第二個GridRow:此列的列高也佔據4個網格,設置 .frame(height: gridCellRows(4));包含2個欄位,分別是GridCard E和GridCard F。GridCard E佔據2個網格,設置 .gridCellColumns(2)。GridCard F佔據8個網格,設置 .gridCellColumns(8)
善用 gridCellColumns 修飾符可以定義網格單元在Grid容器中所佔的欄位數量,讓我們自由地調整網格的欄寬。



整體佈局

有了基本的Grid概念後,就讓我們接著完成整個Grid佈局。

Grid(alignment: .center, horizontalSpacing: gutter, verticalSpacing: gutter) {
    //GridRow that contains Grid AL and Grid AL
    GridRow {
        //Grid AL that contains GridRow AF and GridRow MR
        Grid(alignment: .leading, horizontalSpacing: gutter, verticalSpacing: gutter) {
            //GridRow AF that contains GridCard A and GridBF B2F
            GridRow {
                GridCard("A")
                    .gridCellColumns(4)
                    .frame(height: gridCellRows(8))

                GridBF()
                    .gridCellColumns(10)
            }

            //GridRow GL that contains GridCard G2L
            GridRow {
                GridCardGL()
            }
            .frame(height: gridCellRows(4))
        }

        .gridCellColumns(14)

        //Grid MR that contains GridCard M2R
        Grid(alignment: .leading, horizontalSpacing: gutter, verticalSpacing: gutter) {
            GridCardMR()
        }

        .frame(height: gridCellRows(12))
        .gridCellColumns(6)
    }
}
.frame(height: gridCellRows(12))
  1. 建立Grid容器:設置 horizontalSpacingverticalSpacing,這些間距參數等於gutter的概念。
  2. 建立第一個GridRow:此列包含兩個Grid,分別是Grid AL和Grid MR。
  3. Grid AL:內部包含兩個GridRow。第一個GridRow 包含GridCard A 和先前建立的GridBF中的GridRow AF。第二個GridRow 包含GridRow GL。
  4. Grid MR:由四個GridRow組成。基於先前展示的Step 1 - Grid B2F的概念,實現起來會相對容易,這裡不再詳述。

因此,通過靈活運用Grid和GridRow的變化組合,我們可以成功還原Apple WWDC24 Bento Grid的佈局!

SwiftUI Bento Grid

GridRow 中可以包含多個 Grid,而 Grid 又可以包含多個 GridRow,就像俄羅斯娃娃一樣。掌握了這個概念,我們就能輕鬆實現複雜的Grid佈局。


最後,我將前面仿照WWDC24的Bento Grid套用到oThings的產品特色與功能說明中。結果證明,Bento Grid的佈局方式確實方便設計師規劃內容豐富的界面。透過網格的尺寸變化與配置,能夠提供規律、彈性以及具視覺層次的資訊呈現。這是一種由理性編排支撐的感性視覺美感。

oThings Bento Grid

附帶一提,中間oThings logo卡片的漸層背景使用了iOS18推出的MeshGradient漸層功能,這是一個非常強大的漸層效果。通過以下程式碼,你可以定義網格的格數、座標與顏色參數,輕鬆設計出優美的漸層視覺效果。

MeshGradient(width: 2, height: 2, points: [
    [0, 0], [1, 0], [0, 1], [1, 1]
], colors: [
    .cyan, .yellow, .green, .purple
])



oThings是Kyle獨立設計與開發的APP, 是一款專為您的生活記憶而設的記憶保存應用程式,讓您專注於記錄珍愛的事物。以SwiftUI介面框架開發,結合設計的創意與程式的創新所創造的混合體驗設計,呈現高質感的動畫轉場與絲滑的互動效果,打造具品牌辨識度的使用體驗。歡迎有興趣的您下載體驗。


Conclusion

透過這次使用SwiftUI Grid實踐Bento Grid的實驗,我深刻體會到SwiftUI Grid的功能雖然簡單,但卻非常強大。以下是幾點關於Bento Grid的心得:

  1. 適合大尺寸屏幕:大尺寸畫面,例如平板或電腦較大的螢幕,能夠容納更多的網格,從而突顯出網格間的變化差異。
  2. 善用Grid與GridRow的組合:雖然GridRow需要被Grid包覆,但內層也可以再包覆Grid,這樣才能創造出複雜的Grid佈局。當處理全新的複雜佈局時,可以先通過簡單的繪圖拆解GridGridRow的結構關係,這樣會更容易進行畫面的設計。
  3. Grid的欄位數量由Grid內定義:與Word或Google Doc中的表格功能不同,SwiftUI Grid的網格欄位數量是由Grid自身定義的。例如,在一個GridRow內放置兩個視圖,對於Grid而言,整體欄位數量為2。當我們對某個欄位定義了 .gridCellColumns(3),那麼整體的欄位數量就會增加到4個。



分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.