iPhoneアプリでよく見かける「リスト表示」。設定アプリ、メモアプリ、連絡先など、多くのアプリで使われている基本的なUIですよね。
SwiftUIのList
を使えば、このようなスクロール可能なリスト表示を簡単に実装できます。本記事では、Swift初心者の方でも理解できるように、List
の基本から実践的な使い方まで詳しく解説します。
Listとは?
List
は、SwiftUIでスクロール可能なリスト形式のUIを作成するためのビューコンポーネントです。UIKitのUITableView
に相当し、より少ないコードで実装できます。
Listの主な特徴
- 自動でスクロール可能なビューを生成
- iOS標準のデザインを自動適用
- パフォーマンスが自動で最適化される
- 削除・移動などの編集機能が簡単に実装できる
Listでできること
- データの一覧表示
- セクション分けされたリスト
- 編集可能なリスト(削除・並び替え)
- 詳細画面への遷移
- カスタムデザインのセル
基本的な使い方
最もシンプルなList
まずは、最も基本的なリストの作り方から見ていきましょう。
import SwiftUI
struct ContentView: View {
var body: some View {
List {
Text("アイテム1")
Text("アイテム2")
Text("アイテム3")
}
}
}
このコードだけで、スクロール可能な3行のリストが完成します。
配列からListを生成する
実際のアプリでは、データの配列からリストを生成することが多いです。
struct ContentView: View {
let fruits = ["リンゴ", "バナナ", "オレンジ", "イチゴ", "ブドウ"]
var body: some View {
List(fruits, id: \.self) { fruit in
Text(fruit)
}
}
}
コードの解説
fruits
: 表示したいデータの配列id: \.self
: 各アイテムを一意に識別するためのキー{ fruit in ... }
: 各アイテムをどう表示するかを定義
ForEachを使った方法
ForEach
を使うと、リスト内で他の要素と組み合わせることができます。
struct ContentView: View {
let fruits = ["リンゴ", "バナナ", "オレンジ"]
var body: some View {
List {
Text("フルーツリスト").font(.headline)
ForEach(fruits, id: \.self) { fruit in
Text(fruit)
}
Text("合計: \(fruits.count)個").font(.caption)
}
}
}
ForEach
を使うことで、リストの前後に固定の要素を追加できます。
Identifiableプロトコルを使う
データモデルを作る際は、Identifiable
プロトコルに準拠させると便利です。
struct Task: Identifiable {
let id = UUID()
var title: String
var isCompleted: Bool
}
struct ContentView: View {
let tasks = [
Task(title: "買い物に行く", isCompleted: false),
Task(title: "レポートを書く", isCompleted: true),
Task(title: "ジムに行く", isCompleted: false)
]
var body: some View {
List(tasks) { task in
HStack {
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
.foregroundColor(task.isCompleted ? .green : .gray)
Text(task.title)
.strikethrough(task.isCompleted)
}
}
}
}
Identifiableのメリット
id
パラメータを省略できる- コードがシンプルになる
- SwiftUIが各アイテムを正しく追跡できる
Listのスタイルをカスタマイズする
listStyle
モディファイアを使って、リストの見た目を変更できます。
List {
Text("アイテム1")
Text("アイテム2")
}
.listStyle(.plain) // スタイルを指定
利用可能なスタイル
// シンプルなスタイル
.listStyle(.plain)
// グループ化されたスタイル
.listStyle(.grouped)
// インセットスタイル
.listStyle(.inset)
// インセット+グループ化
.listStyle(.insetGrouped)
// サイドバースタイル(iPad/Mac向け)
.listStyle(.sidebar)
各スタイルの見た目を試して、アプリのデザインに合ったものを選びましょう。
セクション分けされたリスト
Section
を使うと、リストをカテゴリごとに分けられます。
struct ContentView: View {
var body: some View {
List {
Section(header: Text("フルーツ")) {
Text("リンゴ")
Text("バナナ")
Text("オレンジ")
}
Section(header: Text("野菜")) {
Text("にんじん")
Text("トマト")
Text("キャベツ")
}
}
.listStyle(.insetGrouped)
}
}
セクションにフッターを追加
Section(
header: Text("設定"),
footer: Text("これらの設定はいつでも変更できます")
) {
Text("通知")
Text("プライバシー")
}
削除機能を実装する
onDelete
モディファイアを使うと、スワイプで削除する機能を簡単に追加できます。
struct ContentView: View {
@State private var items = ["アイテム1", "アイテム2", "アイテム3", "アイテム4"]
var body: some View {
NavigationView {
List {
ForEach(items, id: \.self) { item in
Text(item)
}
.onDelete { indexSet in
items.remove(atOffsets: indexSet)
}
}
.navigationTitle("リスト")
.toolbar {
EditButton()
}
}
}
}
コードの解説
@State
: データの変更を検知して画面を更新onDelete
: 削除処理を定義EditButton()
: 編集モードの切り替えボタン
並び替え機能を実装する
onMove
モディファイアで、アイテムの並び替えができます。
struct ContentView: View {
@State private var items = ["1番目", "2番目", "3番目", "4番目"]
var body: some View {
NavigationView {
List {
ForEach(items, id: \.self) { item in
Text(item)
}
.onMove { from, to in
items.move(fromOffsets: from, toOffset: to)
}
.onDelete { indexSet in
items.remove(atOffsets: indexSet)
}
}
.navigationTitle("並び替え可能なリスト")
.toolbar {
EditButton()
}
}
}
}
編集ボタンをタップすると、並び替えと削除が可能になります。
NavigationLinkで詳細画面へ遷移
リストの各行をタップして詳細画面に遷移する実装です。
struct ContentView: View {
let fruits = ["リンゴ", "バナナ", "オレンジ"]
var body: some View {
NavigationView {
List(fruits, id: \.self) { fruit in
NavigationLink(destination: DetailView(fruitName: fruit)) {
Text(fruit)
}
}
.navigationTitle("フルーツ一覧")
}
}
}
struct DetailView: View {
let fruitName: String
var body: some View {
VStack {
Text(fruitName)
.font(.largeTitle)
Text("詳細情報をここに表示")
.padding()
}
.navigationTitle(fruitName)
}
}
カスタムセルを作成する
リストの各行のデザインをカスタマイズできます。
struct FruitRow: View {
let name: String
let emoji: String
let color: Color
var body: some View {
HStack(spacing: 15) {
Circle()
.fill(color)
.frame(width: 50, height: 50)
.overlay(
Text(emoji)
.font(.title)
)
VStack(alignment: .leading) {
Text(name)
.font(.headline)
Text("新鮮な\(name)です")
.font(.caption)
.foregroundColor(.gray)
}
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.gray)
}
.padding(.vertical, 8)
}
}
struct ContentView: View {
var body: some View {
List {
FruitRow(name: "リンゴ", emoji: "🍎", color: .red.opacity(0.3))
FruitRow(name: "バナナ", emoji: "🍌", color: .yellow.opacity(0.3))
FruitRow(name: "オレンジ", emoji: "🍊", color: .orange.opacity(0.3))
}
}
}
実践的なToDoアプリの実装例
ここまで学んだことを組み合わせて、実用的なToDoアプリを作ってみましょう。
import SwiftUI
struct Task: Identifiable {
let id = UUID()
var title: String
var isCompleted: Bool
var priority: Priority
enum Priority: String {
case high = "高"
case medium = "中"
case low = "低"
var color: Color {
switch self {
case .high: return .red
case .medium: return .orange
case .low: return .blue
}
}
}
}
struct ContentView: View {
@State private var tasks = [
Task(title: "SwiftUIを学ぶ", isCompleted: false, priority: .high),
Task(title: "買い物に行く", isCompleted: true, priority: .medium),
Task(title: "ジムに行く", isCompleted: false, priority: .low),
Task(title: "メールを返信する", isCompleted: false, priority: .high)
]
@State private var showingAddTask = false
var body: some View {
NavigationView {
List {
Section(header: Text("未完了のタスク")) {
ForEach($tasks.filter { !$0.wrappedValue.isCompleted }) { $task in
TaskRow(task: $task)
}
.onDelete { indexSet in
deleteTask(at: indexSet, from: tasks.filter { !$0.isCompleted })
}
}
if tasks.contains(where: { $0.isCompleted }) {
Section(header: Text("完了済み")) {
ForEach($tasks.filter { $0.wrappedValue.isCompleted }) { $task in
TaskRow(task: $task)
}
.onDelete { indexSet in
deleteTask(at: indexSet, from: tasks.filter { $0.isCompleted })
}
}
}
}
.navigationTitle("ToDoリスト")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
showingAddTask = true
} label: {
Image(systemName: "plus")
}
}
ToolbarItem(placement: .navigationBarLeading) {
EditButton()
}
}
}
}
private func deleteTask(at offsets: IndexSet, from filteredTasks: [Task]) {
for index in offsets {
if let taskIndex = tasks.firstIndex(where: { $0.id == filteredTasks[index].id }) {
tasks.remove(at: taskIndex)
}
}
}
}
struct TaskRow: View {
@Binding var task: Task
var body: some View {
HStack(spacing: 12) {
Button {
task.isCompleted.toggle()
} label: {
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
.foregroundColor(task.isCompleted ? .green : .gray)
.font(.title3)
}
.buttonStyle(.plain)
VStack(alignment: .leading, spacing: 4) {
Text(task.title)
.strikethrough(task.isCompleted)
.foregroundColor(task.isCompleted ? .gray : .primary)
Text("優先度: \(task.priority.rawValue)")
.font(.caption)
.foregroundColor(task.priority.color)
}
}
.padding(.vertical, 4)
}
}
この実装では以下の機能を実現しています。
- タスクの完了/未完了の切り替え
- 優先度による色分け
- 完了済みタスクと未完了タスクのセクション分け
- スワイプで削除
- 編集モードでの一括削除
ListとLazyVStackの使い分け
SwiftUIにはList
以外にも、リスト表示を実現する方法があります。
Listを使うべき場合
- iOS標準のリストUIが欲しい
- 削除・並び替え機能が必要
- セクション分けをしたい
- NavigationLinkで画面遷移をしたい
LazyVStackを使うべき場合
- 完全にカスタムなデザインが必要
- リストっぽくないレイアウトにしたい
- より細かいレイアウト制御が必要
// LazyVStackの例
ScrollView {
LazyVStack(spacing: 20) {
ForEach(items, id: \.self) { item in
CustomCard(item: item)
}
}
.padding()
}
よくある質問(FAQ)
Q1. リストの背景色を変更したい
List {
Text("アイテム")
}
.scrollContentBackground(.hidden)
.background(Color.blue.opacity(0.1))
iOS 16以降では.scrollContentBackground(.hidden)
で背景を非表示にしてから、カスタム背景を設定できます。
Q2. リストの区切り線を消したい
List {
Text("アイテム")
.listRowSeparator(.hidden)
}
Q3. 特定の行だけ区切り線の色を変えたい
Text("アイテム")
.listRowSeparator(.visible)
.listRowSeparatorTint(.red)
Q4. リストの行の背景色を変更したい
Text("アイテム")
.listRowBackground(Color.yellow.opacity(0.3))
Q5. 空のリストに「データがありません」と表示したい
List {
if items.isEmpty {
Text("データがありません")
.foregroundColor(.gray)
.frame(maxWidth: .infinity, alignment: .center)
} else {
ForEach(items) { item in
Text(item.name)
}
}
}
パフォーマンスのベストプラクティス
大量のデータを扱う場合
Listは自動的にパフォーマンスを最適化しますが、以下の点に注意しましょう。
// ✅ 良い例:Identifiableを使う
struct Item: Identifiable {
let id = UUID()
var name: String
}
// ❌ 悪い例:インデックスをIDにする
List(items.indices, id: \.self) { index in
Text(items[index].name)
}
複雑な計算は避ける
// ❌ 悪い例:毎回計算する
List(items) { item in
Text(expensiveCalculation(item))
}
// ✅ 良い例:事前に計算しておく
List(items) { item in
Text(item.preCalculatedValue)
}
まとめ
List
を使えば、以下のような機能を簡単に実装できます。
- スクロール可能なリスト表示
- セクション分けされた整理されたUI
- 削除・並び替えなどの編集機能
- 詳細画面への遷移
- カスタムデザインのセル
初心者の方は、まずシンプルなリストから始めて、徐々に機能を追加していくことをおすすめします。List
はSwiftUIの中でも特に頻繁に使うコンポーネントなので、しっかりマスターしておきましょう!