はじめに
私が所属するチーム向けにSwiftUI勉強会を開催することとなったため、資料を記事として投稿します。SwiftUI初学者なので間違いなどあれば、教えていただきたいです!以前に作成した記事も以下からご覧ください。
目次
事前準備
事前準備についてはPart1をご確認ください。今回は省略させていただきます。主に、Xcodeでのプロジェクト作成の手順を記載しています。
Part3のお品書き
List
List
はデータを一覧表示することができます。List
はiPhoneの設定画面をイメージしていただくのが一番わかりやすいと思います。
では早速ですが、List
を実装してみましょう。以下のようにコードを記述してみてください。
import SwiftUI
struct ContentView: View {
var body: some View {
List {
Text("あいうえお")
Text("かきくけこ")
Text("さしすせそ")
}
}
}
#Preview {
ContentView()
}
プレビュー画面を確認すると、List{}
内で追加されているテキストがList
の要素として表示されていることが確認できます。
List
の要素には画像を入れることもできます。以下のようにコードを記述してみてください。
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
を用いて画像とテキストを横並びにすると良いです。以下のようにコードを記述して、プレビューを確認してみてください。
import SwiftUI
struct ContentView: View {
var body: some View {
List {
HStack {
Image(systemName: "mic")
Text("マイク")
}
HStack {
Image(systemName: "trash")
Text("ゴミ箱")
}
}
}
}
#Preview {
ContentView()
}
画像とテキストを横並びにすることができました。画像のサイズを特に指定しない場合、元の画像サイズでそのまま要素として追加されます。サイズがバラバラな複数の画像を調整せずに適用した場合、画像サイズによってList
1つ1つの要素の幅もバラバラになってしまいます。そのため、画像に対して.resizable()
などのモディファイアを用いて調整してあげると見栄えも良くなると思います。
配列を用いてListを表示させる
続いて、配列の要素をList
で表示させてみましょう。これまではList
の要素にしたいView
をList{}
に定義することで実装していましたが、配列を用いることで配列の要素を一覧で表示させることができます。また、配列を用いてList
を実装する場合はForEach
文を使用します。配列を用いたList
の実装方法ともに、簡単にForEach
文について説明します。
ForEach
SwiftUI
でForEach
を用いる場面としては、繰り返し処理を行う過程でView
を生成し、List
・VStack
などを用いて並べるといったところです。書き方のパターンは何通りかありますが、基本的なものだけさらっと紹介しつつ、本題の配列を用いたList
の実装方法を説明します。
struct ContentView: View {
var body: some View {
List {
ForEach(1..<11) { num in
Text("\(num)")
}
}
}
}
パターン1はForEach
文の()
内に範囲を指定し、その範囲内で変数num
に値を格納してクロージャーを実行する方法です。この場合はForEach
の{}
内の処理が1回ずつ実行され、それと同時に1〜10までの範囲で現在の値が変数num
へ格納されます。
プレビュー画面を確認すると以下のようになります。
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
の要素数が増減するなどで範囲が変更されても対応(増減した要素を画面に反映)することができないためです。
試しに次のようにコードを変更して追加ボタンを押してみてください。
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()
は()
内の要素を末尾に追加してくれます。
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
は同義です。
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のコードを少し変更してみます。
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)を追加しておいてください。
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()
}
import SwiftUI
struct NextView: View {
var body: some View {
Text("Hello, World!")
}
}
#Preview {
NextView()
}
List
をNavigationView
の中に配置し、List
の要素として表示させていたテキストをNavigationLink
の中に配置しています。NavigationLink
の引数destination
には遷移先の画面であるNextView
を指定しています。ではプレビューorシミュレータで確認してみましょう。
各要素をタップすると、Hello, Worldが表示されているNextView
に遷移できることが確認できると思います。
要素をスワイプで削除できるようにする
List
は本来、選択や編集、削除といった機能があります。今回は削除機能を実装してみましょう。こちらも簡単に実装することができます。今回もパターン3のコードを変更してみます。
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
の要素を画面中央に表示させる(テキストに対してモディファイアは不要)- 例:
野球⚾️
をタップすると遷移先に野球⚾️
が中央に表示される
- 例:
-
サンプルコード
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()
}
import SwiftUI
struct ContentViewDetail: View {
let sport: String
var body: some View {
Text(sport)
}
}
#Preview {
ContentView7_Detail(sport: "")
}
Level2
次のような画面を作成してください。また、以下のように各要素を設定してください。
-
各種設定
-
TextField
とList
を縦に並べる(アプリ起動時List
の要素はなし)
-
-
TextField
- 角丸の枠線を付け、プレースホルダは
入力してください
- 文字を入力して
Enter/改行
を押すと、入力した文字がList
の要素として追加・表示される
- 角丸の枠線を付け、プレースホルダは
-
List
- 要素を左にスワイプして削除できるようにする
サンプルコード
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
)で表示させる - 各順位表はセクション毎に分けて実装する
- ヘッダーとして
セントラル・リーグ
とパシフィック・リーグ
追加すること
- ナビゲーションのタイトルは
サンプルコード
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)をお楽しみに!