[SwiftUI]リストで左右のスワイプした時のボタンの実装

目次

概要

Listを実装したが、左スワイプして削除、右スワイプしてピン留めなどのように
ユーザーに優しい実装をするのも大事になってくる。わざわざボタン幾つもボタンを設置するのも見栄えが悪くなるし…
そんなリストの左右のスワイプで特定の処理を行う時は、swipeActionsを使用しよう!

ソースコード

アプリの最初の実行部分

TestApp.swift
import SwiftUI

@main
struct TestApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    var body: some Scene {
        WindowGroup {
            ListMenuView()
        }
    }
}
Swift

View表示部分

ListMenuView.swift
import SwiftUI

struct ListMenuView: View {
    
    @ObservedObject private var viewModel: ListMenuViewModel = ListMenuViewModel()
    
    var body: some View {
        NavigationView {
            VStack {
                listView()
            }
            .navigationTitle("リストの備忘録動作確認")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    listOrderMenu()
                }
            }
        }
    }
    
    private func listView() -> some View {
        return List {
            ForEach(Array(viewModel.dataList.enumerated()), id: \.offset) { index, data in
                Button {
                    print("\(data)をタップしました。")
                } label: {
                    Text(data)
                }
                .swipeActions(edge: .trailing) {
                    Button(role: .destructive) {
                        viewModel.deleteData(index: index)
                    } label: {
                        Image(systemName: "trash.fill")
                    }
                }
                .swipeActions(edge: .leading) {
                    Button(role: .cancel) {
                        viewModel.bringTop(index: index)
                    } label: {
                        Image(systemName: "pin.fill")
                    }
                    .tint(.green)
                }
                .contextMenu(menuItems: {
                    cellMenu(index: index)
                })
            }
        }
    }
    
    private func cellMenu(index: Int) -> some View {
        return VStack {
            Button {
                print("編集する?")
            } label: {
                Text("編集")
            }
            Button {
                viewModel.deleteData(index: index)
            } label: {
                Text("削除")
                    .foregroundColor(.red)
            }
        }
    }
    
    private func listOrderMenu() -> some View {
        return Menu {
            ForEach(DataOrder.allCases, id:\.self) { dataOrder in
                Button {
                    viewModel.sortDataOrder(selectedOrder: dataOrder)
                } label: {
                    HStack {
                        if dataOrder == viewModel.selectedOrder {
                            Image(systemName: "checkmark")
                        }
                        Text(dataOrder.rawValue)
                    }
                }
            }
        } label: {
            Image(systemName: "arrow.up.and.down.text.horizontal")
        }
    }
}
Swift

処理部分

ListMenuViewModel.swift
import Foundation

enum DataOrder: String, CaseIterable, Identifiable {
    case titleAscendingOrder = "項目名の昇順"
    case titleDescendingOrder = "項目名の降順"
    
    var id: String { rawValue }
}

class ListMenuViewModel: ObservableObject {
    @Published private(set) var dataList: [String]
    @Published private(set) var selectedOrder: DataOrder = .titleAscendingOrder
    
    init() {
        dataList = []
        for index in 0 ..< 20 {
            dataList.append("データ\(index)")
        }
    }
    
    func sortDataOrder(selectedOrder: DataOrder) {
        self.selectedOrder = selectedOrder
        switch selectedOrder {
        case .titleAscendingOrder:
            dataList = dataList.sorted(by: {$0 < $1})
        case .titleDescendingOrder:
            dataList = dataList.sorted(by: {$1 < $0})
        }
    }
    
    func deleteData(index: Int) {
        dataList.remove(at: index)
    }
    
    func bringTop(index: Int) {
        let moveData = dataList.remove(at: index)
        dataList.insert(moveData, at: 0)
    }
}
Swift

スクリーンショット

詳細

スワイプの方向と、その方向に応じて表示させるボタンの実装をする際、swipeActionsで以下のように書く。

.swipeActions(edge: /* スワイプの方向 */) {
    Button(role: /* ボタンの役割 */) {
        // ボタンの処理
    } label: {
        // 表示させたいアイコン
    }
}

スワイプの方向

設定値内容
.leadingセルを右にスワイプさせた時に表示するボタン。
ピン留めやお気に入りなどで実装されることが多い。
セルの左側に並ぶボタン群。
.trailingセルを左にスワイプさせた時に表示するボタン。
削除ボタンなどで実装されることが多い。
セルの左側に並ぶボタン。

ボタンの役割

設定値内容
cancel一定以上の量をスワイプするとボタンが非表示になる。
ボタンの背景色はデフォルトは明るいグレー。
destructive一定以上の量をスワイプするとデータの削除処理が行われる。
ボタンの背景色のデフォルトは赤。

ボタンの背景色の変更

このボタンの背景色はZStackを使うわけでも、foregroundColorを設定するわけでもない。
Buttonの最後に以下を追加することで色の設定ができる。

.tint(/* 設定したい背景色 */)

参考ページ:
stackoverflow「Set image color in SwipeAction」
.NET ゆる〜りワーク【SwiftUI】List にスワイプアクション(.swipeActions)を実装する【iOS15&Xcode13】