0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

はじめに

 私が所属するチーム向けにSwiftUI勉強会を開催することとなったため、資料を記事として投稿します。SwiftUI初学者なので間違いなどあれば、教えていただきたいです!以前に作成した記事も以下からご覧ください。

目次

事前準備

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

Part3のお品書き

List

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

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

ContentView.swift
import SwiftUI

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

#Preview {
    ContentView()
}

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

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

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の要素として表示されていることが確認できます。

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

ContentView.swift
import SwiftUI

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

#Preview {
    ContentView()
}

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

配列を用いてListを表示させる

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

ForEach

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

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

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

パターン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の要素数となります。
 プレビュー画面を確認すると以下のようになります。

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

警告文

Non-constant range: argument must be an integer literal

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

パターン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()()内の要素を末尾に追加してくれます。

パターン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で配列のインデックスの範囲を取得することができます。
 つまり、配列.indices0..<配列.countは同義です。

パターン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のコードを少し変更してみます。

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から要素をタップすると次の画面へ遷移することができますよね。画面遷移の機能は比較的簡単に実装できるのでやってみましょう。画面遷移機能の実装にはNavigationLinkNavigationViewを使用します。
 また、遷移先の画面用のSwiftUI View(NextView.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()
}

NextView.swift
import SwiftUI

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

#Preview {
    NextView()
}

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

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

要素をスワイプで削除できるようにする

 Listは本来、選択や編集、削除といった機能があります。今回は削除機能を実装してみましょう。こちらも簡単に実装することができます。今回もパターン3のコードを変更してみます。

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の要素として表示されている状態で、一番上のりんごをスワイプして削除してみてください。以下のようになりませんでしたか?

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

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

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

課題

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

Level1

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

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

サンプルコード
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()
}
ContentViewDetail.swift
import SwiftUI

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

#Preview {
    ContentView7_Detail(sport: "")
}

Level2

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

  • 各種設定
    • TextFieldListを縦に並べる(アプリ起動時Listの要素はなし)

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

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

サンプルコード
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()
}

Level3

 次のようなプロ野球ペナントレース順位表をListを用いて実装してください。また、以下のように各要素を設定してください。画像では2024年度ペナントレース順位表をもとに実装していますが、2023年度ペナントレース順位表を作成してください(特に意味はありません)。

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

サンプルコード
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()
}

まとめ

 Part2の勉強会資料は以上となります。Part1の記事を作成した時期と比較して、SwiftUIの理解はより深まってきたと思います。この勉強会資料作成を通して、SwiftUI初学者を脱却したいなと思っています。次回作(Part3)をお楽しみに!

0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?