【SwiftUI】Listの並び替えを実装する

2022.04.15

Listで行の並び替えをしたかったので調べました。今回はeditModeを使用して並び替えする方法を記載しております。

環境

  • Xcode 13.3

Listで並び替える

EditButtonを使用する

EditButton()を使用することで環境変数editModeが切り替わり、それに伴いListの編集モードも切り替わります。

コード

import SwiftUI

struct ContentView: View {

    @State private var numbers: [Int] = [1, 2, 3, 4, 5]

    var body: some View {

        VStack {

            List {
                ForEach(numbers, id: \.self) { number in
                    Text(String(number))
                }
                .onMove(perform: moveRow)
                .onDelete(perform: removeRow)
            }

            EditButton()
                .padding()
        }
    }

    private func moveRow(from source: IndexSet, to destination: Int) {
        numbers.move(fromOffsets: source, toOffset: destination)
    }

    private func removeRow(from source: IndexSet) {
        numbers.remove(atOffsets: source)
    }
}

Edit Button()を使用して、編集モードに切り替えるだけではListの並び替えは行えません。.onMoveonDeleteを使用することで削除ボタンや並び替えマークが表示されるようになります。

デモ

rearrage-editbutton

onMove

func onMove(perform action: ((IndexSet, Int) -> Void)?) -> some DynamicViewContent

動的ビューの要素が移動されたときにSwiftUIが呼び出すクロージャ。クロージャは、動的ビューの基になるデータのコレクションに対するオフセットを表す2つの引数を取ります。アイテムを移動する機能を無効にするには、nilを渡します。

第一引数は、移動元のIndexSetで、第二引数は、移動先のオフセットIntになります。その値を使用して、move(fromOffsets:, toOffset:)を実行します。

公式ドキュメントmove(fromOffsets:toOffset:)の内容がないですが、動き的には関数名を呼んで字の如く第二引数の箇所に移動しています。

onDelete

func onDelete(perform action: Optional<(IndexSet) -> Void>) -> some DynamicViewContent

ビュー内の要素が削除されたときにSwiftUIに実行させるアクション

削除させるコレクション要素に関連するIndexSetを持っていますので、それを使用して、remove(atOffsets: source)で要素を削除しています。

EditButtonを使わずに並び替える

上記では、EditButton()を使用して、編集モードを切り替えましたが、EditButton()を使用せずとも編集モードは切り替えることが出来ます。

環境変数editMode.activeにすることで編集モードに切り替えることが出来ます。

コード

struct ContentView: View {

    @State private var numbers: [Int] = [1, 2, 3, 4, 5]

    var body: some View {

        List {
            ForEach(numbers, id: \.self) { number in
                Text(String(number))
            }
            .onMove(perform: moveRow)
            .onDelete(perform: removeRow)
        }
        .environment(\.editMode, .constant(.active))
    }

    private func moveRow(from source: IndexSet, to destination: Int) {
        numbers.move(fromOffsets: source, toOffset: destination)
    }

    private func removeRow(from source: IndexSet) {
        numbers.remove(atOffsets: source)
    }
}

こちらの実装では.environmentを使用し、 editMode.constant(.active)にすることで常時編集モードにしています。

デモ

rearrage-always-edi

削除ボタンを非表示にする

.onDeleteを削除する。

List {
    ForEach(numbers, id: \.self) { number in
        Text(String(number))
    }
    .onMove(perform: moveRow)
}

または、.deleteDisabledを使用して、値をtrueにする。

List {
    ForEach(numbers, id: \.self) { number in
        Text(String(number))
    }
    .onMove(perform: moveRow)
    .onDelete(perform: removeRow)
    .deleteDisabled(true)
}

すると、削除ボタンが表示されなくなりました。

編集モードの時だけ削除ボタンを非表示にする

上記で紹介した方法だと、スワイプアクションの時も削除ボタンが非表示になってしまいます。編集モードの時は削除ボタンは非表示にするけど、スワイプアクション時は表示したい場合はeditModeの値を見て、処理を分岐させます。

import SwiftUI

struct ContentView: View {

    @Environment(\.editMode) var editMode
    @State private var numbers: [Int] = [1, 2, 3, 4, 5]

    var body: some View {

        VStack {

            List {
                ForEach(numbers, id: \.self) { number in
                    Text(String(number))
                }
                .onMove(perform: moveRow)
                .onDelete(perform: removeRow)
                .deleteDisabled(isDeleteDisabled)
            }

            EditButton()
                .padding()
        }
    }

    private var isDeleteDisabled: Bool {
        if editMode?.wrappedValue == .active {
            return true
        }
        return false
    }

    private func moveRow(from source: IndexSet, to destination: Int) {
        numbers.move(fromOffsets: source, toOffset: destination)
    }

    private func removeRow(from source: IndexSet) {
        numbers.remove(atOffsets: source)
    }
}

isDeleteDisabledで、環境変数editMode.activeの時はisDisabletrueにし、そうでなければfalseを返しています。

private var isDeleteDisabled: Bool {
    if editMode?.wrappedValue == .active {
        return true
    }
    return false
}

その値をdeleteDisabledに設定することで編集モード中は削除ボタンを非表示にすることが出来ます。

.deleteDisabled(isDeleteDisabled)

デモ

rearrange-deletebutton

おわりに

今回、UITableViewで行うような処理もListだと少ないコードで書くことが出来るということが分かりました。まだまだ現時点ではUITableViewよりは出来ることは少ないですが、これからの成長を楽しみにしていきたいですね。

参考