將集合裡的資料變成一個個 view 的SwiftUI ForEach

SwiftUI 提供 ForEach 型別幫我們將集合裡的東西生成一個個 view,然後再合併成一個 view,大大簡化我們需要自己輸入的程式,也解決了 ViewBuilder 參數的元件數量問題,接下來就讓我們以劉增瞳的你從不知道歌詞為例說明吧。

顯示多行重覆的內容

我們可以在 VStack 裡耐心地產生每一個 Text("你從不知道 我因為你而煎熬")

struct ContentView: View {   var body: some View {      VStack {         Text("你從不知道 我因為你而煎熬")         Text("你從不知道 我因為你而煎熬")         Text("你從不知道 我因為你而煎熬")         Text("你從不知道 我因為你而煎熬")         Text("你從不知道 我因為你而煎熬")      }   }}

搭配 ForEach & Range

不過每寫一行都讓我們更加煎熬,就讓我們用 ForEach 試試吧。透過 ForEach 我們只要寫一行 Text("你從不知道 我因為你而煎熬"),比較不煎熬。

我們可以透過 Code Actions 選單快速輸入 ForEach。如下圖所示,按住 cmd 鍵點選 Text 後,選擇 Repeat

此時將自動將 Text 包在 ForEach 的 { } 裡。

以上的 ForEach 搭配的 init 如下

init(_ data: Range<Int>, @ViewBuilder content: @escaping (Int) -> Content)

參數說明

  • data

型別 Range 的 data 控制元件的數量,因此剛剛的 0..<5 將產生 5 個參數 content 回傳的元件。

  • content

回傳產生的元件。剛剛程式傳入的 closure 如下

{ item in  Text("你從不知道 我因為你而煎熬")}

它回傳了 Text("你從不知道 我因為你而煎熬"),因此將產生文字。closure 的參數 item 將對應 range 的數字,由於 range 是 0..<5,所以第一個 Text 的 item 是 0,第二個 Text 的 item 是 1,其它以此類推。

值得注意的,data 的型別是 Range,因此只能用 ..<,不能使用 ClosedRange 的 ... 語法。

搭配 ForEach & Range 呈現 array 內容

很多時候我們將東西存在 array 裡,ForEach 也可以結合 array,將 array 的內容變成一個個 view,然後組合起來,例如以下呈現你從不知道副歌歌詞的例子。

struct ContentView: View {   let lyrics = ["你從不知道", "我因為你而煎熬", "心碎過的每一分每一秒"]   var body: some View {      VStack {         ForEach(0..<lyrics.count) { item in            Text(lyrics[item])         }      }   }}

我們先將歌詞存在 array lyrics,然後在 ForEach 的參數傳入 0..<lyrics.count,接著利用 lyrics[item] 讀取 array 的每個成員。

雖然結果是對的,不過 Xcode 卻會顯示以下的警告訊息。

Non-constant range: argument must be an integer literal

這是因為 ForEach 搭配參數 Range 時,它要求 Range 最好要是固定的,否則可能會產生資料錯亂的問題。等下我們將介紹搭配 id 的寫法消除此警告。

ps: 當 array 資料會變動時,使用 ForEach 搭配參數 Range 會出問題。相關說明可參考以下連結。

搭配 array 的 indices

剛剛的程式還可以更簡化, 我們不須自己手動輸入0..<lyrics.count,讀取 array 的 indices 即可取得它的數字範圍。

ForEach(lyrics.indices) { index in   Text(lyrics[index])}

ps: 此寫法一樣會有黃色警告,等下我們將介紹搭配 id 的寫法消除此警告。

搭配 ForEach,array & id 呈現 array 內容

當我們想呈現的內容來自 array 時,也可以將 array 當參數傳入 ForEach,例如以下例子。

struct ContentView: View {   let lyrics = ["你從不知道", "我因為你而煎熬", "心碎過的每一分每一秒"]   var body: some View {      VStack {         ForEach(lyrics, id: \.self) { message in            Text(message)         }      }   }}

以上的 ForEach 搭配的 init 如下

init(_ data: Data, id: KeyPath<Data.Element, ID>, content: @escaping (Data.Element) -> Content)

當參數 data 傳入 array 時,我們需要額外設定型別 KeyPath 的參數 id。此 id 將設定 array 成員的 id,到時候 ForEach 將利用 id 區分 array 裡的成員。在此我們傳入 \.self,表示 array 成員自己就是 id,因此你從不知道的 id 是你從不知道我因為你而煎熬的 id 是我因為你而煎熬

\.self 的寫法跟 Swift KeyPath 有關,有興趣的朋友可進一步查詢相關說明。

不過並非任何東西都可以當 id,依據 ForEach 的定義,id 的型別必須遵從 protocol Hashable,比方我們熟悉的 Int & String 資料都可以當 id,因為它們有遵從 protocol Hashable。

struct ForEach<Data, ID, Content> where Data : RandomAccessCollection, ID : Hashable

也許有人有疑問,剛剛 array 裡很明顯每個歌詞的字串就能區分彼此,為何還需要特別告訴 ForEach 用 \.self 當區分的 id 呢 ? 這其實跟 array 成員的型別有關,當型別比較複雜時,我們得特別指定 id 它才知道如何區分,例如以下的例子。

ForEach 搭配成員是自訂型別的 array

當 array 的成員是比較複雜的型別時,我們可以用 KeyPath 指定以成員的某個 property 當 id,例如以下例子的 \.name 表示以 Song 的 name 當 id。

struct Song {   let name: String   let singer: String}struct ContentView: View {   let songs = [
Song(name: "小酒窩", singer: "林俊傑"),
Song(name: "你從不知道", singer: "劉增瞳"),
Song(name: "對的時間點", singer: "林俊傑")
]
var body: some View { VStack { ForEach(songs, id: \.name) { song in Text("\(song.name) by \(song.singer)") } } }}

若我們改用 \.singer,以歌手名當 id,將會產生特別的問題。

VStack {   ForEach(songs, id: \.singer) { song in      Text("\(song.name) by \(song.singer)")   }}

array songs 裡有兩首歌的歌手都是林俊傑,當 id 為林俊傑時,它將無法區分小酒窩對的時間點,因此最後畫面上變成顯示兩首小酒窩。

搭配 ForEach & Identifiable 呈現 array 內容

剛剛產生 ForEach 時,型別 KeyPath 的參數 id 是為了區分 array 成員。不過當成員的型別遵從 protocol Identifiable 時,它本身就會宣告用來區分的 property id,因此我們生成 List 時可以省略參數 id,不用再指定 id。

以剛剛的型別 Song 為例,我們讓它遵從 protocol Identifiable,在其中宣告 property id。id 的型別必須遵從 Hashable protocol,因此最簡單的莫過於用整數當 id,給予每首歌一個數字,不過這樣我們還要有方法確保每首歌的 id 數字不會重覆。

因此更常見的方法是利用 UUID 當 id,因為它可以幫我們產生獨一無二,不會跟別人同名的 ID。

import Foundationstruct Song: Identifiable {
let id = UUID()
let name: String
let singer: String
}

此時我們傳入 songs 生成 ForEach 時將可省略參數 id,因為 Song 的 id 就可以區分了。

VStack {   ForEach(songs) { song  in      Text("\(song.name) by \(song.singer)")   }}

此 ForEach 搭配的 init 宣告如下:

extension ForEach where ID == Data.Element.ID, Content : View, Data.Element : Identifiable {
public init(_ data: Data, @ViewBuilder content: @escaping (Data.Element) -> Content)
}

SwiftUI ForEach 搭配 array 時,如何取得成員的位置(index)

SwiftUI ForEach 搭配 array 時,若想同時取得 array 的成員跟位置,可參考以下連結。

讓 String & Int 遵從 protocol Identifiable

當 array 的內容是簡單的字串或數字時,我們也可以搭配 Identifiable,比方以下例子:

利用 extension 擴充 String,讓它遵從 protocol Identifiable,定義它的 id 是自己本身的字串內容。

extension String: Identifiable {
public var id: String { self }
}

因為 String 遵從 protocol Identifiable,所以現在搭配 ForEach 時不用再傳入參數 id 了。

struct ContentView: View {
let lyrics = ["你從不知道", "我因為你而煎熬", "心碎過的每一分每一秒"]
var body: some View {
VStack {
ForEach(lyrics) { message in
Text(message)
}

}
}
}

補充: 如何將 array 的成員變 Binding

補充: 為什麼 ForEach & List 需要區分 array 裡的成員 ?

前面介紹了 ForEach 如何區分 array 裡的成員,但到底為什麼它需要區分呢 ? 有興趣的朋友可再參考以下連結的說明。

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
彼得潘的 iOS App Neverland

彼得潘的 iOS App Neverland

5.3K Followers

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com