iOS 17 App 程式設計實戰心法(SwiftUI)

第 6 章
List 與 ForEach

That's been one of my mantras - Focus and Simplicity. Simple can be harder than complex: You have to work hard to get your thinking clean to make it simple. But it's worth it in the end because once you get there, you can move mountains.

- Steve Jobs

現在你已經對範例 App 的原型有了基本概念,本章將進行一些更有趣的內容,並使用清單視圖( List View )建立一個簡單的 App,一旦你能掌握這個技術與清單視圖,我們便會開始建立 FoodPin App。

首先,iPhone App 的清單視圖是什麼呢?如果你之前使用過 UIKit,SwiftUI 中的清單視圖與 UIKit 的表格視圖(Table View )是同樣的東西。清單視圖是 iOS App 中最常見的 UI,大部分的 App(除了遊戲以外)或多或少會使用清單視圖來顯示內容,最常見的便是內建的電話 App,你的聯絡人是以清單視圖來顯示;另一個例子是郵件 App,它利用一個清單視圖來顯示郵件信箱與郵件。不僅是文字資料清單,清單視圖也可以顯示圖片資料,例如: TED、Google+ 以及 Airbnb 皆是不錯的 App 案例。圖 6.1 展示了一些清單式 App 範例,雖然外表看起來有些出入,但這些全部都是使用清單視圖完成的。

圖 6.1. 清單視圖 App 範例(從左到右:Techcrunch、App Store、Product Hunt 與TED)
圖 6.1. 清單視圖 App 範例(從左到右:Techcrunch、App Store、Product Hunt 與TED)

我們準備在本章建立一個非常簡單的清單視圖,並學習如何填入資料(圖片與文字)。如果你使用UIKit 實作過表格視圖,應該知道實作一個簡單的表格視圖需要花一點工夫。SwiftUI 簡化了整個過程,只需要幾行程式碼,就能以表格形式來顯示清單資料,即使你需要自訂列的佈局,也只需要極少的工夫便可辦到。

仍是覺得困惑嗎?待會你就會明白我的意思。

建立一個 SimpleTable 專案

不要只是閱讀本書。當你想認真學習 iOS 程式語言,則要停止只是閱讀,請開啟你的 Xcode,然後撰寫程式碼,這是學習程式的最佳捷徑。

我們來開始建立一個簡單的 App 吧 !這個 App 非常簡單,我們將在簡單的清單視圖中顯示一串餐廳名稱,下一章中我們將會繼續改造它。若你尚未開啟 Xcode,則開啟它,使用 iOS 下的「App」模板來新建一個專案,如圖 6.2 所示。

圖 6.2.  Xcode 專案模板
圖 6.2. Xcode 專案模板

點選「Next」按鈕,在Xcode 專案選項中填入下列資訊:

  • Product Name(專案名稱): SimpleTable – 這是你的App名稱
  • Team(團隊): 這裡先不做設定。
  • Organization Identifier(組織識別碼): com.appcoda – 這其實是反向域名,如果你擁 有網域,你可以使用自己的網域名稱;否則的話,你可以使用「com.< 你的名稱>」。
  • Bundle Identifier(套件識別碼): com.appcoda.SimpleTable - ;這個欄位是由 Xcode自動產生的。
  • Interface(介面): SwiftUI - Xcode 支援兩種建立 UI 的方式,這個專案請選擇 SwiftUI, 因為本書會採用SwiftUI 來開發UI。
  • Language(語言): Swift – Xcode 支援使用 Objective-C 與 Swift 開發 App,本書的主 題是 Swift 語言,因此我們選擇使用 Swift 來開發專案。
  • Include Tests(包含測試): [不用勾選] – 這個選項不要勾選,此專案不會進行任何測試。

點選「Next」按鈕,接著Xcode 會詢問你要將 SimpleTable 專案儲存在哪裡,在你的 Mac 電腦中挑選一個資料夾,並點選「Create」按鈕來繼續。

建立一個簡單的清單

建立專案後,Xcode 應該會顯示 ContentView.swift 的內容。從模擬器清單中選取「iPhone 15 Pro」,我建議使用此裝置來預覽 UI。

我們從一個簡單的清單來開始了解 List 視圖的用法。將 ContentView 結構內的程式碼替換為下列的程式碼:

struct ContentView: View {
    var body: some View {
        List {
            Text("Item 1")
            Text("Item 2")
            Text("Item 3")
            Text("Item 4")
        }
    }
}

以上是建立一個簡單的清單或表格所需要的程式碼。當你將文字視圖嵌入 List 時,清單視圖會以列的形式顯示資料,這裡每一列顯示不同敘述的文字視圖,如圖 6.3 所示。

圖 6.3. 建立一個簡單的清單
圖 6.3. 建立一個簡單的清單

相同的程式碼片段可以使用 ForEach 來編寫,如下所示:

struct ContentView: View {
    var body: some View {
        List {
            ForEach(1...4, id: \.self) { index in
                Text("Item \(index)")
            }
        }
    }
}

由於這些文字視圖非常相似,因此你可在 SwiftUI 中使用 ForEach 迴圈來建立視圖。

從已識別的底層集合中,依照需求計算視圖的一種結構。.

- Apple 官方文件 (https://developer.apple.com/documentation/swiftui/foreach)

你可以提供 ForEach 一組資料集合或一個範圍,不過你必須要注意的是,你需要告訴 ForEach 如何識別集合中的每個項目,參數 id 的目的即在此。而為什麼 ForEach 需要唯一識別項目呢?SwiftUI 功能強大,當集合中的部分或全部項目變更時,它可以自動更新 UI。為了實現這一點,它需要一個識別碼來在更新或刪除項目時唯一識別該項目。

在上列的程式碼中,我們向 ForEach 傳送一個要迴圈遍歷的值範圍。該識別碼設定為其值(即1、2、3、4),index 參數儲存迴圈的目前值,例如:它從「1」開始,index 參數的值則為「1」。

在閉包(ForEach 內的程式碼區塊)中,即是渲染視圖所需的程式碼。這裡我們建立文字視圖,其敘述將會依照迴圈中的 index 值而變化,這就是你如何在清單中建立四個不同標題的項目的方法。

我再教你一種技巧,相同的程式碼片段也可以進一步重寫如下:

struct ContentView: View {
    var body: some View {
        List {
            ForEach(1...4, id: \.self) {
                Text("Item \($0)")
            }
        }
    }
}

你可以省略 index 參數,並使用參數名稱縮寫「$0」,它引用閉包的第一個參數。

我們將進一步將程式碼重寫得更簡單些,你可以將資料集合直接傳送到 List 視圖,程式碼如下:

struct ContentView: View {
    var body: some View {
        List(1...4, id: \.self) {
            Text("Item \($0)")
        }
    }
}

如你所見,只需要幾行程式碼,即可建立一個簡單的清單或表格。

使用陣列項目顯示清單

現在你已經知道如何建立一個簡單的清單,接著我們來看如何使用更多樣化的佈局, 參見圖 6.4。在大多數的情況下,清單視圖的項目皆會包含文字與圖片,而你該如何實作呢?如果你知道 Image、Text、VStack 與 HStack 工作原理的話,你應該對如何建立一個複雜的清單有概念了。

圖 6.4. 顯示餐廳列的簡單清單視圖
圖 6.4. 顯示餐廳列的簡單清單視圖

現在開啟 ContentView.swift 來編寫 UI 的程式碼。我們宣告 restaurantNames 變數,並在結構中插入下列的程式碼:

var restaurantNames = ["Cafe Deadend", "Homei", "Teakha", "Cafe Loisl", "Petite Oyster", "For Kee Restaurant", "Po's Atelier", "Bourke Street Bakery", "Haigh's Chocolate", "Palomino Espresso", "Upstate", "Traif", "Graham Avenue Meats And Deli", "Waffle & Wolf", "Five Leaves", "Cafe Lore", "Confessional", "Barrafina", "Donostia", "Royal Oak", "CASK Pub and Kitchen"]

在這個範例中,我們使用陣列來儲存清單資料,如果你忘記陣列的語法,則請參考第 2 章的內容說明。陣列中不同的值是以逗號分隔,然後用一對方括號包裹起來。

當我說:「在結構中插入程式碼」時,表示你必須將變數宣告在結構的大括號內,如下所示:

struct ContentView: View {

    var restaurantNames = ["Cafe Deadend", "Homei", "Teakha", "Cafe Loisl", "Petite Oyster", "For Kee Restaurant", "Po's Atelier", "Bourke Street Bakery", "Haigh's Chocolate", "Palomino Espresso", "Upstate", "Traif", "Graham Avenue Meats And Deli", "Waffle & Wolf", "Five Leaves", "Cafe Lore", "Confessional", "Barrafina", "Donostia", "Royal Oak", "CASK Pub and Kitchen"]

    .
    .
    .

}

陣列是電腦程式設計中的基本資料結構,你可以將陣列想成是資料元素的集合。以上列程式碼的 restaurantNames 陣列來說,它表示了 String 元素的集合,你可將陣列視覺化為圖 6.5。

圖 6.5. restaurantNames 陣列
圖 6.5. restaurantNames 陣列

每個陣列元素都由索引值(index )來標示或存取,一個陣列中如果有 10 個元素,則有 0 至 9 的索引值。restaurantNames[0] 表示回傳陣列中的第一個項目。

我們繼續編寫程式碼,並更新 body 變數,如下所示:

var body: some View {
    List {
        ForEach(0...restaurantNames.count-1, id: \.self) { index in
            Text(restaurantNames[index])
        }
    }
}

我們使用 ForEach 迴圈遍歷陣列的每一個項目。如前所述,陣列的第一個索引值是「0」,因此我們將範圍設定為0 至 restaurantNames.count-1,count 屬性回傳陣列中項目的總數,restaurantNames.count-1 的值是陣列的最後一個索引值。

為了顯示餐廳名稱,我們於 ForEach 的程式碼區塊中建立一個 Text 視圖,並將相應的餐廳名稱傳送給文字視圖。

當你更新程式碼後,預覽應該會顯示餐廳名稱的清單,如圖 6.6 所示。要捲動清單,你可以使用滑鼠游標來上下拖曳它。

圖 6.6. App 顯示餐廳名稱清單
圖 6.6. App 顯示餐廳名稱清單

將縮圖加到清單視圖

我們還沒有將圖片加到每一列,首先下載取得範例圖片: http://www.appcoda.com/resources/swift53/simpletable-images1.zip,這個 zip 壓縮檔內有三個圖檔,解壓縮檔案, 並將圖片從 Finder 拖曳至素材目錄(Assets.xcassets ),如圖 6.7 所示。

圖 6.7. 加入圖片至素材目錄
圖 6.7. 加入圖片至素材目錄

現在編輯 ContentView,並將 Text 視圖替換為以下的 HStack 視圖:

HStack {
    Image("restaurant")
        .resizable()
        .frame(width: 40, height: 40)

    Text(restaurantNames[index])
}

我們使用 Image 視圖來載入餐廳圖片。為了調整圖片大小,我們使用 resizable 修飾器及 frame 修飾器來將圖片縮小至 40×40 點。

程式碼變更後,預覽應該會在每一列中顯示圖片,如圖 6.8 所示。

圖 6.8. App 顯示餐廳圖片
圖 6.8. App 顯示餐廳圖片

變更清單視圖的樣式

List 視圖在 iOS 15 內預設為使用「插入分組樣式」(Inset Grouped Style )。「插入分組清單樣式」顯示背景顏色,並在清單視圖的四周加入間距。如果要變更清單樣式,你可以將 listStyle 修飾器加到 List,如下所示:

List {
    .
    .
    .
}
.listStyle(.plain)

要使用簡單的樣式,則可以設定為 .plain 或 PlainListStyle(),圖 6.9 為最後顯示的結果。

圖 6.9.  清單樣式設定為簡單樣式
圖 6.9. 清單樣式設定為簡單樣式

顯示清單的另一種方式

在本章結束之前,我希望你了解實作清單(及其他功能)有好幾種方式,現在我們在 ForEach 中指定restaurantNames 的索引範圍,如下所示:

ForEach(0...restaurantNames.count-1, id: \.self) { index in
  .
  .
  .
}

實際上,你可以使用 .indices 屬性重寫程式碼,來取得可用項目的範圍:

ForEach(restaurantNames.indices, id: \.self) { index in
   .
   .
   .
}

如果你更新程式中程式碼,將會得到相同的結果。還有另一種方式可以迴圈遍歷 restaurantNames 陣列中的項目,我們不使用索引,而是將整個陣列傳送給 ForEach,如下所示:

ForEach(restaurantNames, id: \.self) { restaurantName in
    HStack {
        Image("restaurant")
            .resizable()
            .frame(width: 40, height: 40)

        Text(restaurantName)
    }
}

在閉包中,有一個名為「restaurantName」的參數,此 restaurantName 參數儲存了迴圈的目前名稱,因此我們可以簡單地在 Text 視圖中使用它。

你可以試著變更你的程式碼,預覽應該是相同的。

你的作業:各個儲存格顯示不同的圖片

現在範例 App 的所有儲存格都是顯示相同的圖片,試著調整 App 來讓各個儲存格顯示不同的圖片(提示:為圖片建立另一個陣列)。你可下載使用本章所準備的範例圖片 http://www.appcoda.com/resources/swift4/simpletable-images-2.zip ,圖 6.10 展示最後的結果畫面。

圖 6.10. 在 App 中顯示不同的餐廳圖片
圖 6.10. 在 App 中顯示不同的餐廳圖片

假使你不知道該如何完成這個作業,也不用擔心,我會在下一章中完整說明。

Credit: 範例中所使用的圖片是由unsplash.com提供。

本章小結

「清單視圖」是 SwiftUI 中最常用的元件之一,如果你已徹底了解這些內容並成功建立 App,那麼你應該對如何建立自己的清單視圖有堅實的理解。

我試著讓這個範例 App 保持一切簡單,但是在真實世界的 App 中,清單視圖的資料通常不會「寫死」( hard-coded ),它一般是從檔案、資料庫或某處載入,之後的章節內容將會談到這部分。此時,請確認你已經完全理解清單視圖的工作原理,若是仍然感到困惑的話,請回到本章開頭並重新閱讀本章的內容。

本章所準備的範例檔中,有最後完整的Xcode 專案供下載:http://www.appcoda.com/resources/swift59/swiftui-list-view.zip.