MENU

【SwiftUI】Listの使い方完全ガイド|基本から実践まで初心者向けに解説

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の中でも特に頻繁に使うコンポーネントなので、しっかりマスターしておきましょう!

参考リンク

プログラミングの独学におすすめ
プログラミング言語の人気オンラインコース
独学でプログラミングを学習している方で、エラーなどが発生して効率よく勉強ができないと悩む方は多いはず。Udemyは、プロの講師が動画で実際のプログラムを動かしながら教えてくれるオンライン講座です。講座の価格は、セール期間中には専門書籍を1冊買うよりも安く済むことが多いです。新しく学びたいプログラミング言語がある方は、ぜひUdemyでオンライン講座を探してみてください。
目次