#18 多項目顯示(ForEach)
有了顯示單項內容的程式之後,接下來就可擴充為顯示多項內容。如何處理多項內容的資料呢?如果在第一單元,我們會用一個陣列,加上 for 迴圈來處理,就如同第1單元第4課範例1-4a的商王列表一樣。
不過在 View 結構裡面,無法直接用 for 迴圈指定值給主體 body,還好,SwiftUI 有 View 版本的替代方案-- ForEach,這個 ForEach 也是一個 View 物件類型,可用來將多個 View 物件組合起來,語法跟 for 迴圈有點類似。
上一節我們定義了「鳥類」的資料類型,假如我們再定義一個「特有種清單」的鳥類陣列,陣列每個元素都是鳥類物件的實例,例如黃山雀與台灣山鷓鴣,這個陣列的宣告如下:
let 特有種清單 = [
鳥類(
id: 1,
中文名: "黃山雀",
別名: "師公鳥",
科名: "山雀科",
英文名: "Yellow Tit",
圖片檔名: "640px-Taiwan_tit.jpg",
圖片來源: "https://zh.wikipedia.org/wiki/黃山雀",
攝影者: "Robert tdc"),
鳥類(
id: 2,
中文名: "台灣山鷓鴣",
別名: "深山竹雞",
科名: "雉科 ",
英文名: "Taiwan Partridge",
圖片檔名: "Taiwan_partridge_(Arborophila_crudigularis).jpg",
圖片來源: "https://zh.wikipedia.org/wiki/台湾山鹧鸪",
攝影者: "Francesco Veronesi")
]
如果是用第1單元 for 迴圈的做法,我們可以用 print() 列印出每個鳥種的資料,像這樣:
for 鳥種 in 特有種清單 {
print(鳥種.中文名, "\t", 鳥種.英文名, "\t", 鳥種.圖片檔名)
}
若要改成 View 結構,則可用 ForEach 語法如下:
ForEach(特有種清單) { 特有種 in
單項顯示(鳥: 特有種)
}
和 for 迴圈語法稍有差異,但意思差不多。這句程式碼的意思其實是:
for each 特有種 in 特有種清單 that returns 單項顯示(鳥: 特有種)
可翻譯為:對「特有種清單」中每個元素,指定給「特有種」,傳回「特有種」的「單項顯示」視圖。
在實際的語法上,ForEach 需要一個陣列當作參數,在此為「特有種清單」。ForEach對這個陣列參數有一個特別的要求,就是陣列的「元素」要符合 Identifiable 規範,也就是須有一個欄位,名稱為 id,類型可以是 Int, String 或其他,只要它的值不會重複即可。
因此「鳥類」的定義須增加一個 id欄位,並宣告符合 Identifiable 規範:
struct 鳥類: Identifiable {
var id: Int
var 中文名: String
var 別名: String
var 科名: String
var 英文名: String
var 圖片檔名: String
var 圖片來源: String
var 攝影者: String
}
然後ForEach() 接著一個大括號段落 { },比較特別的地方,是這個大括號段落會傳入一個臨時參數,就像 for 迴圈參數一樣,在這裡為「特有種」,這個參數會從陣列中依序取值,然後用在 { } 段落中。
類似這樣,會傳入參數的 { } 段落,是 Swift 非常重要的語法,術語稱為 Closure,中文稱為閉鎖、閉包或是封閉段落,本質上相當於一個未命名的函式,因此在本課程稱為「匿名函式」。
匿名函式可視為一種物件,能夠執行某些任務的物件,就像樂高積木的動力模組一樣,本身可作為變數值,或當作參數傳遞給其他函式或物件型態。在後面課程,若使用控制元件,如按鈕、刻度、選單,或是在第3單元,都會經常用到匿名函式。
上一節定義的「台灣特有種」物件,我們將它一般化,改稱為「單項顯示」,並增加一個屬性「鳥」,之後可當作初始化參數傳入,避免像上一節使用全域變數「黃山雀」。完整範例程式如下:
// 2-6b 鳥類清單
// Created by Heman, 2021/08/15
import PlaygroundSupport
import SwiftUI
struct 鳥類: Identifiable {
var id: Int
var 中文名: String
var 別名: String
var 科名: String
var 英文名: String
var 圖片檔名: String
var 圖片來源: String
var 攝影者: String
}
let 特有種清單 = [
鳥類(
id: 1,
中文名: "黃山雀",
別名: "師公鳥",
科名: "山雀科",
英文名: "Yellow Tit",
圖片檔名: "640px-Taiwan_tit.jpg",
圖片來源: "https://zh.wikipedia.org/wiki/黃山雀",
攝影者: "Robert tdc"),
鳥類(
id: 2,
中文名: "台灣山鷓鴣",
別名: "深山竹雞",
科名: "雉科",
英文名: "Taiwan Partridge",
圖片檔名: "Taiwan_partridge_(Arborophila_crudigularis).jpg",
圖片來源: "https://zh.wikipedia.org/wiki/台湾山鹧鸪",
攝影者: "Francesco Veronesi"),
鳥類(
id: 3,
中文名: "藍腹鷴",
別名: "臺灣藍鷳",
科名: "雉科",
英文名: "Swinhoe's Pheasant",
圖片檔名: "",
圖片來源: "",
攝影者: "")
]
for 鳥種 in 特有種清單 {
print(鳥種.中文名, "\t", 鳥種.英文名, "\t", 鳥種.圖片檔名)
}
struct 相框: View {
var 檔名: String
init(_ p: String) {
檔名 = p
}
var body: some View {
if 檔名 == "" {
Image(systemName: "camera.circle")
.resizable()
.scaledToFit()
.foregroundColor(.red)
.opacity(0.4)
} else {
Image(uiImage: UIImage(named: 檔名)!)
.resizable()
.scaledToFit()
}
}
}
struct 單項顯示: View {
var 鳥: 鳥類
var body: some View {
HStack {
VStack {
Text(鳥.中文名)
.font(.title)
.foregroundColor(.blue)
相框(鳥.圖片檔名)
.frame(width: 120)
}
VStack(alignment: .leading) {
Text("別名:" + 鳥.別名)
Text("科名:" + 鳥.科名)
Text("英文名稱:" + 鳥.英文名)
Text("圖片來源:" + 鳥.圖片來源)
Text("攝影者:" + 鳥.攝影者)
}
.font(.title2)
.lineLimit(1)
} .frame(height: 100)
}
}
struct 台灣特有種鳥類: View {
var body: some View {
ForEach(特有種清單) { 特有種 in
單項顯示(鳥: 特有種)
.padding()
}
}
}
PlaygroundPage.current.setLiveView(台灣特有種鳥類())
我們對於「相框」顯示,增加一個 if 條件句,如果圖片檔名是空字串的話,表示沒有照片可用,就顯示 SF Symbols 的系統圖示,如第三項藍腹鷴的圖片。

Image(systemName: "camera.circle")
.resizable()
.scaledToFit()
.foregroundColor(.red)
.opacity(0.4)
在這裡我們用了 .opacity() 修飾語,將圖片打淡,opacity 是透明度的意思,參數值從0~1.0,0代表完全透明,1.0代表完全不透明。
對於多項資料的顯示,我們除了 ForEach 之外,還可在外層配合用 ScrollView,這樣如果陣列的元素比較多時,螢幕可以往下滑動。本節範例只有三項內容,用或不用 ScrollView,顯示結果是一樣的。
從這個例子可以看出,ForEach 可以將資料陣列轉換為一組View,只是它沒有辦法控制螢幕的捲動,View超出螢幕範圍的部分,ForEach 便無法顯示。圖解如下:

所以一個常用的句型是 ScrollView 配合 ForEach,由 ScrollView 來負責捲動螢幕,這樣的句型後面還會出現,其中ScrollView 可以換成其他的容器類型,例如 List, NavigationView 等等,就像這樣:
ScrollView {
ForEach(特有種清單) { 特有種 in
單項顯示(鳥: 特有種)
.padding()
}
}
註解
- 台灣山鷓鴣圖片下載地址為 https://upload.wikimedia.org/wikipedia/commons/thumb/c/c9/Taiwan_partridge_%28Arborophila_crudigularis%29.jpg/640px-Taiwan_partridge_%28Arborophila_crudigularis%29.jpg
- 本節仍為(CC BY-SA 2.0) 方式分享版權。
- 經過 ForEach 產出的 View,並非總是垂直排列,試著將最後一段改成:
HStack {
ForEach(特有種清單) { 特有種 in
單項顯示(鳥: 特有種)
.padding()
}
}
就會知道,ForEach只是將View集合在一起,排列方式仍是由上一層View決定。
- ForEach 也可以接受陣列以外的參數,就像 for 迴圈一樣,不過在第2單元只用陣列就夠了。