SwiftUI初学者がチーム向けに勉強会資料を作った Part3

## はじめに
　私が所属するチーム向けにSwiftUI勉強会を開催することとなったため、資料を記事として投稿します。SwiftUI初学者なので間違いなどあれば、教えていただきたいです！以前に作成した記事も以下からご覧ください。
- [Part1](https://qiita.com/uso_k_sk/items/e546eaad128eef8f5791)
- [Part2](https://qiita.com/uso_k_sk/items/a5fcd93925a17963e963)

## 目次
- [事前準備](#事前準備)
- [Part3のお品書き](#part3のお品書き)
    - [List](#list)
    - [配列を用いてListを表示させる](#配列を用いてlistを表示させる)
    - [Listに色々な機能をつける](listに色々な機能をつける)
- [まとめ](#まとめ)

## 事前準備
　事前準備については[Part1]()をご確認ください。今回は省略させていただきます。主に、Xcodeでのプロジェクト作成の手順を記載しています。

## Part3のお品書き
- [List](#list)
- [配列を用いてListを表示させる](#配列を用いてlistを表示させる)
    - [ForEach](#foreach)
- [Listに色々な機能をつける](#listに色々な機能をつける)
    - [NavigationStackを用いてタイトルをつける](#navigationstackを用いてタイトルをつける)
    - [NavigationLinkで画面遷移機能を作る](#navigationlinkで画面遷移機能を作る)
    - [要素をスワイプで削除できるようにする](#要素をスワイプで削除できるようにする)
- [課題](#課題)
    - [Level1](#level1)
    - [Level2](#level2)
    - [Level3](#level3)

### List
　```List```はデータを一覧表示することができます。```List```はiPhoneの設定画面をイメージしていただくのが一番わかりやすいと思います。

<img src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939098/fbd1250a-f126-e04d-7d31-a2a7170f1689.jpeg" width = 300>

　では早速ですが、```List```を実装してみましょう。以下のようにコードを記述してみてください。

```swift:ContentView.swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        List {
            Text("あいうえお")
            Text("かきくけこ")
            Text("さしすせそ")
        }
    }
}

#Preview {
    ContentView()
}
```

　プレビュー画面を確認すると、```List{}```内で追加されているテキストが```List```の要素として表示されていることが確認できます。

<img src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939098/7a70c85f-777c-ac2d-b975-d6fe5b5d334c.png" width = 300>

　```List```の要素には画像を入れることもできます。以下のようにコードを記述してみてください。

```swift:ContentView.swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        List {
            Image(systemName: "square.and.pencil")
            Image(systemName: "square.and.arrow.up")
            Image(systemName: "trash")
        }
    }
}

#Preview {
    ContentView()
}
```

　プレビュー画面を確認すると、```List{}```内で追加されている画像がListの要素として表示されていることが確認できます。

<img src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939098/65f72437-8ab6-c11c-2a46-febfeec99c0c.png" width = 300>

　また、1つの要素に画像とテキストを両立させることもできます。その場合、```HStack```を用いて画像とテキストを横並びにすると良いです。以下のようにコードを記述して、プレビューを確認してみてください。

```swift:ContentView.swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        List {
            HStack {
                Image(systemName: "mic")
                Text("マイク")
            }
            HStack {
                Image(systemName: "trash")
                Text("ゴミ箱")
            }
        }
    }
}

#Preview {
    ContentView()
}
```

<img src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939098/0792b7f9-5790-f26a-439e-930aa77c06a9.png" width = 300>

　画像とテキストを横並びにすることができました。画像のサイズを特に指定しない場合、元の画像サイズでそのまま要素として追加されます。サイズがバラバラな複数の画像を調整せずに適用した場合、画像サイズによって```List```1つ1つの要素の幅もバラバラになってしまいます。そのため、画像に対して```.resizable()```などのモディファイアを用いて調整してあげると見栄えも良くなると思います。

### 配列を用いてListを表示させる
　続いて、配列の要素を```List```で表示させてみましょう。これまでは```List```の要素にしたい```View```を```List{}```に定義することで実装していましたが、配列を用いることで配列の要素を一覧で表示させることができます。また、配列を用いて```List```を実装する場合は```ForEach```文を使用します。配列を用いた```List```の実装方法ともに、簡単に```ForEach```文について説明します。

#### ForEach
　```SwiftUI```で```ForEach```を用いる場面としては、繰り返し処理を行う過程で```View```を生成し、```List```・```VStack```などを用いて並べるといったところです。書き方のパターンは何通りかありますが、基本的なものだけさらっと紹介しつつ、本題の配列を用いた```List```の実装方法を説明します。

```swift:パターン1
struct ContentView: View {
    var body: some View {
        List {
            ForEach(1..<11) { num in
                Text("\(num)")
            }
        }
    }
}

```

　パターン1は```ForEach```文の```()```内に範囲を指定し、その範囲内で変数```num```に値を格納してクロージャーを実行する方法です。この場合は```ForEach```の```{}```内の処理が1回ずつ実行され、それと同時に1〜10までの範囲で現在の値が変数```num```へ格納されます。
　プレビュー画面を確認すると以下のようになります。

<img src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939098/c2d384bc-d647-3a0c-a200-483a0512a5db.png" width = 300>

```swift:パターン2
struct ContentView: View {
    let fruits = ["🍎", "🍌", "🍊"]
    var body: some View {
        List {
            ForEach(0..<fruits.count, id: \.self) { num in
                Text(fruits[num])
            }
        }
    }
}

```

　ここからが本題です。パターン2は配列の要素数を用いて範囲を指定する方法です。この場合、範囲は```0〜配列fruitsの要素数```となります。
　プレビュー画面を確認すると以下のようになります。

<img src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939098/2c059caa-fff8-b725-c392-c82863b01eee.png" width = 300>

　ここで注意点です。第2引数の```id```は指定せずともプレビューを確認することはできますが、以下のような警告文が表示されます。

**警告文**
> Non-constant range: argument must be an integer literal

　こちらが表示される原因は、範囲として指定している```fruits.count```の値が定数として処理されることで、配列```fruits```の要素数が増減するなどで範囲が変更されても対応（増減した要素を画面に反映）することができないためです。
 　試しに次のようにコードを変更して追加ボタンを押してみてください。

```swift:パターン2
struct ContentView: View {
    @State var fruits = ["🍎", "🍌", "🍊"]
    var body: some View {
        VStack {
            Button("追加") {
                self.fruits.append("新しい要素")
            }
            List {
                ForEach(0..<fruits.count) { num in
                    Text(fruits[num])
                }
            }
        }
    }
}

```
  
　追加ボタンを押すと配列```fruits```に要素は追加されますが、Listの要素としては画面に表示されず、デバックエリアに第2引数の```id```がねぇよ！みたいなエラー分が表示されます。
　ちなみにさらっと書いていますが、配列```fruits```の後ろに記載している```.append()```は```()```内の要素を末尾に追加してくれます。

 

```swift:パターン3
struct ContentView: View {
    let fruits = ["🍎", "🍌", "🍊"]
    var body: some View {
        List {
            ForEach(fruits.indices, id: \.self) { num in
                Text(fruits[num])
            }
        }
    }
}

```

　パターン4は第1引数の範囲の指定方法を```配列.indices```とする方法です。```配列.indices```で配列のインデックスの範囲を取得することができます。
　つまり、```配列.indices```と```0..<配列.count```は同義です。

```swift:パターン4
struct ContentView: View {
    let fruits = ["🍎", "🍌", "🍊"]
    var body: some View {
        List {
            ForEach(fruits, id: \.self) { item in
                Text(item)
            }
        }
    }
}

```

　パターン4は第1引数の範囲の指定方法に配列そのものを指定する方法です。変数```item```には配列の要素そのものが格納されます。

　配列を用いた```List```の実装方法の説明は以上です。

### Listに色々な機能をつける
#### NavigationStackを用いてタイトルをつける
　```List```にタイトルをつけてみましょう。上記のパターン3のコードを少し変更してみます。

```swift:ContentView.swift
import SwiftUI

struct ContentView: View {
    let fruits = ["🍎", "🍌", "🍊"]
    var body: some View {
        NavigationStack {
            List {
                ForEach(fruits.indices, id: \.self) { num in
                    Text(fruits[num])
                }
            }
            .navigationTitle("果物一覧")
        }
    }
}

#Preview() {
    ContentView()
}

```

　```List```に対して```.navigationTitle("果物一覧")```を設定することでタイトルを設定します。プレビュー画面を確認してください。

#### NavigationLinkで画面遷移機能を作る
　iPhoneの設定画面の```List```から要素をタップすると次の画面へ遷移することができますよね。画面遷移の機能は比較的簡単に実装できるのでやってみましょう。画面遷移機能の実装には```NavigationLink```と```NavigationView```を使用します。
　また、遷移先の画面用の```SwiftUI View```(NextView.swift)を追加しておいてください。

```swift:ContentView.swift
import SwiftUI

struct ContentView: View {
    let fruits = ["🍎", "🍌", "🍊"]
    var body: some View {
        NavigationView(content: {
            List {
                ForEach(fruits.indices, id: \.self) { num in
                    NavigationLink(destination: NextView()) {
                        Text(fruits[num])
                    }
                }
            }
        })
    }
}

#Preview() {
    ContentView()
}

```

```swift:NextView.swift
import SwiftUI

struct NextView: View {
    var body: some View {
        Text("Hello, World!")
    }
}

#Preview {
    NextView()
}

```
　```List```を```NavigationView```の中に配置し、```List```の要素として表示させていたテキストを```NavigationLink```の中に配置しています。```NavigationLink```の引数```destination```には遷移先の画面である```NextView```を指定しています。ではプレビューorシミュレータで確認してみましょう。

<img src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939098/937c26f1-3388-7dce-079d-02aa0a0c0b7b.gif" width = 300>

　各要素をタップすると、Hello, Worldが表示されている```NextView```に遷移できることが確認できると思います。

#### 要素をスワイプで削除できるようにする
　```List```は本来、選択や編集、削除といった機能があります。今回は削除機能を実装してみましょう。こちらも簡単に実装することができます。今回もパターン3のコードを変更してみます。
 
```swift:ContentView.swift
import SwiftUI

struct ContentView: View {
    @State var fruits = ["🍎", "🍌", "🍊"]
    var body: some View {
        List {
            ForEach(fruits.indices, id: \.self) { num in
                Text(fruits[num])
            }
            .onDelete(perform: { indexSet in
                removeItem(atIndexSet: indexSet)
            })
        }

    }
    private func removeItem(atIndexSet: IndexSet) {
        fruits.remove(atOffsets: atIndexSet)
    }
}

#Preview() {
    ContentView()
}
```

　今回、追加したモディファイアは```.onDelete()```の部分です。これを使うことでスワイプして削除するアクションが実装できます。
　ただ、これだけでは不十分です。配列の中から要素を削除しておげなければなりません。変数```indexSet```には、スワイプされた要素の行に該当する値が格納され、引数```perform```のクロージャーが実行されます。そのクロージャー内で変数indexSetを用いて配列の要素を削除する関数```removeItem()```を実行してあげます。
　配列の特定の要素を削除する場合は```配列.remove(atOffsets:)```を使用し、引数に```indexSet```を指定してください。

　プレビュー画面を確認して、要素を削除してみましょう。一見実装できているように見えますが、上記のコードでは削除時の挙動が少し変です。りんご、バナナ、みかん全てが```List```の要素として表示されている状態で、一番上のりんごをスワイプして削除してみてください。以下のようになりませんでしたか？

<img src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939098/f19e75de-031b-6f6a-5b7d-eb56cf37d086.gif" width = 300>

　りんごを削除すると一番下のみかんが一瞬隠れ、その後、もう一度バナナの下からみかんが登場するような挙動になってしまっています。まさに、「思ってたのと違う・・・」ですね。

　このようになってしまう原因は特定できていませんが、```ForEach```の書き方をパターン3ではなくパターン4で書くと、以下のように正常な挙動になりました。原因が特定できたら改めて記事を更新させていただきます。

<img src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939098/e875d3f5-bc2d-1b87-22cb-7f8f4a84cdd1.gif" width = 300>

　```List```の実装方法についての説明は以上となります。次章から課題を掲載しているので取り組んでみてください。

## 課題
　課題のサンプルコードを記載していますが、サンプルコードと完全一致していなくても他の方法でレイアウトを作成できていればOKです。

### Level1
　次のような画面を作成してください。また、以下のように各要素を設定してください。

- **各種設定**
    - ```List```の各要素をタップすると画面遷移できるようにする
    - 遷移先に```List```の要素を画面中央に表示させる（テキストに対してモディファイアは不要）
        - 例：```野球⚾️```をタップすると遷移先に```野球⚾️```が中央に表示される

<p></p>

<p>
<img src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939098/2e774828-1da9-85f8-2478-32fbdcbcc136.png" width = 300>

<img src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939098/a49741ef-589a-3154-ab12-5cb5837aaa6b.png" width = 300>
</p>

<details><summary>サンプルコード</summary>

```swift:ContentView.swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        let sportList = ["サッカー⚽️", "野球⚾️", "バスケットボール🏀"]
        NavigationView(content: {
            List {
                ForEach(sportList.indices, id: \.self) { index in
                    NavigationLink(destination: ContentView_Detail(sport: sportList[index])) {
                        Text(sportList[index])
                    }
                }
            }
        })
    }
}

#Preview {
    ContentView()
}
```

```swift:ContentViewDetail.swift
import SwiftUI

struct ContentViewDetail: View {
    let sport: String
    var body: some View {
        Text(sport)
    }
}

#Preview {
    ContentView7_Detail(sport: "")
}
```

</details>

### Level2
　次のような画面を作成してください。また、以下のように各要素を設定してください。

- **各種設定**
    - ```TextField```と```List```を縦に並べる（アプリ起動時```List```の要素はなし）

<p></p>

- ```TextField```
    - 角丸の枠線を付け、プレースホルダは```入力してください```
    - 文字を入力して```Enter/改行```を押すと、入力した文字が```List```の要素として追加・表示される
    - ```padding()```をつける

<p></p>

- ```List```
    - 要素を左にスワイプして削除できるようにする

<p>
<img src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939098/93009042-3263-5b95-9efc-dfd2305be395.png" width = 300>

<img src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939098/b6238072-29c0-1180-42cb-cefcc89fcb15.png" width = 300>

<img src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939098/7c053aaf-add4-9bf7-2edd-720ea5efcda6.png" width = 300>
</p>

<details><summary>サンプルコード</summary>

```swift:ContentView.swift
import SwiftUI

struct ContentView: View {
    @State var list: [String] = []
    @State var newListItem: String = ""
    
    var body: some View {
        NavigationStack {
            VStack {
                TextField("入力してください", text: $newListItem)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .padding()
                    .onSubmit {
                        if !newListItem.isEmpty {
                            self.list.append(newListItem)
                            newListItem = ""
                        }
                    }
                List {
                    ForEach(list, id: \.self) { listItem in
                        Text(listItem)
                    }
                    .onDelete(perform: { indexSet in
                        self.list.remove(atOffsets: indexSet)
                    })
                }
            }
        }
    }
}

#Preview {
    ContentView()
}
```

</details>


### Level3
　次のようなプロ野球ペナントレース順位表を```List```を用いて実装してください。また、以下のように各要素を設定してください。画像とサンプルコードは2024年度ペナントレース順位表をもとに実装していますが、[2023年度ペナントレース順位表](https://npb.jp/bis/2023/stats/)を作成してください（特に意味はありません）。

- **各種設定**
    - ナビゲーションのタイトルは```2023年度順位表```
    - セントラル・リーグとパシフィック・リーグで順位表を分ける
    - ```List```の要素には**順位**と**チーム名**を横並び(```HStack```)で表示させる
    - 各順位表はセクション毎に分けて実装する
    - ヘッダーとして```セントラル・リーグ```と```パシフィック・リーグ```追加すること

<p></p>

<img src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939098/35cab240-2443-dea2-b208-f597a58ad075.png" width = 300>

<details><summary>サンプルコード</summary>

```swift:ContentView.swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        let central = ["読売ジャイアンツ", "阪神タイガース", "横浜DeNAベイスターズ", "広島東洋カープ", "東京ヤクルトスワローズ", "中日ドラゴンズ"]
        let pacific = ["福岡ソフトバンクホークス", "北海道日本ハムファイターズ", "千葉ロッテマリーンズ", "東北楽天ゴールデンイーグルス", "オリックス・バファローズ", "埼玉西武ライオンズ"]
        NavigationStack {
            List {
                Section {
                    ForEach(central.indices, id: \.self) { index in
                        HStack {
                            Text("\(index + 1)位")
                            Text(central[index])
                        }
                    }
                } header: {
                    Text("セントラル・リーグ")
                }
                Section {
                    ForEach(pacific.indices, id: \.self) { index in
                        HStack {
                            Text("\(index + 1)位")
                            Text(pacific[index])
                        }
                    }
                } header: {
                    Text("パシフィック・リーグ")
                }
            }
            .navigationTitle("2024年度 順位表")
        }
    }
}

#Preview {
    ContentView()
}
```

</details>


## まとめ
　Part2の勉強会資料は以上となります。[Part1](https://qiita.com/uso_k_sk/items/e546eaad128eef8f5791)の記事を作成した時期と比較して、SwiftUIの理解はより深まってきたと思います。この勉強会資料作成を通して、SwiftUI初学者を脱却したいなと思っています。次回作（Part3）をお楽しみに！