📚

List形式のUICollectionViewでセルの並び替えと削除を実装する

2022/10/04に公開

先日、List形式のUICollectionViewで並び替えと削除を実装したんですが、
意外と公式ドキュメント以外で詳細に書いている記事が出てこなかったので、書きます。

要件

たとえばミュージックアプリのプレイリストのような、編集可能なリストを実装するとします。
ここでいう編集可能とは、セルの追加・並び替え・削除が可能という意味です。

方針

SwiftUIならListを使うことになります。
UIKitだと、2つ選択肢があります。

  • UICollectionViewのList形式
  • UITableView

WWDC20あたりから、Apple推奨はUICollectionView使った方なので、今回はそっちで進めました。

https://qiita.com/noppefoxwolf/items/803370876a7e274a24f6#uitableview

List形式のUICollectionViewの実装

今回の記事でモダンCollectionViewでリストをどう実装するかまで書くと膨大になってしまうので、前提知識とします。
もしそちらを知りたい方は、下記のチュートリアルあたりを読んでください。

https://www.raywenderlich.com/16906182-ios-14-tutorial-uicollectionview-list

UICollectionViewListCellを継承したCellをつくる

シンプルなリストだったら、defaultContentConfiguration()が使えます。
設定画面ぐらいならこれで事足りるでしょう。
ただセルの中身が複雑になると、なかなかこれだけで完結するのは厳しく、結局僕はカスタムセルをつくることになりました。

class CustomListCell: UICollectionViewListCell {
    // …
}

accessoriesを指定すると色々できる

僕は実装している途中で、なんとなくDelegateメソッドに何かを書くのかなと思っていました。

func collectionView(
    _ collectionView: UICollectionView,
    reorder cell: UICollectionViewListCell,
    forItemAt indexPath: IndexPath
) {
     // …
}

みたいなイメージで。

しかし、モダンCollectionViewだと、Delegateメソッドではなく、accessoriesを指定します。

https://developer.apple.com/documentation/uikit/uicollectionviewlistcell

どこにどう指定するかが本記事のメインテーマです。
UICellAccessoryというのが、指定する構造体で、公式ドキュメントは下記です。

https://developer.apple.com/documentation/uikit/uicellaccessory

並び替え・削除以外にも、詳細とか複数選択とか色々できます。

個人的な印象ですが、モダンCollectionViewは、UITableViewより学習コストが高いですが、その分提供してる機能は多い印象です。

リストの編集のUI/UXを考える

具体的な実装に入る前に、リストの編集のUI/UXについて、一度整理しましょう。
普段なんとなくスワイプして削除するとか、編集ボタン押して編集モード入るとか、そういう動作をしています。

敢えて分類するなら、ユーザーが編集することだけを考えた画面と、リスト形式の中に編集アクションが存在する画面とにわけられます。
ここでは便宜上、編集モードと、非編集モードの画面と呼ぶことにします。
ミュージックアプリから当該の画面を探すと、下記のイメージです。

編集モード 非編集モード
編集モード 非編集モード

このUIはAppleの標準的なものと考えられます。

https://developer.apple.com/design/human-interface-guidelines/components/layout-and-organization/lists-and-tables/

Human Interface Guidelinesを見ても、そんなに詳しくは書いてませんが、純正アプリ見て、CollectionViewの挙動から察するに、

  • ユーザーがリストの複数セルに対して編集アクションをしたいときは編集画面を出す
  • (実装的にはedit modeをオンにする)
  • 編集画面への導線は、新規作成と既存リストの編集の2パターンある
    • 昔は編集ボタンが目立つところにあった気がするが、今はちょっとネスト深くなったような気がする
    • (個人的には編集ボタン押す→編集モードに入る挙動はあまり好きじゃない)
  • 単一のセルに対するクイックアクションとして、スワイプ削除やメニューからの削除があるとより便利
  • 編集モードのときの削除アイコンはセルの左、並び替えはセルの右
    • スワイプ削除だとはセルの右
    • 理由はよくわからない。右利きの人間が多いから、セルの左のスワイプアクションはやりづらいから、というだけ?

という暗黙のルールがあるように見えます。
今回の記事は編集モードの画面をつくることがメインですが、スワイプ削除の実装も最後にすこしだけ書きます。

UICellAccessoryの指定をする

前置きが長くなりましたが、いよいよ実装について書きます。
といっても、実装は簡単で、UICollectionViewDiffableDataSourceを指定するところに、ちょろっと指定するだけです。
コード例はこんな感じ。

lazy var dataSource: UICollectionViewDiffableDataSource<Section, Item> = {
    let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { [weak self] collectionView, indexPath, item -> UICollectionViewCell? in
        let cell = CustomListCell.dequeue(from: collectionView, for: indexPath)
        cell.item = item
        cell.accessories = [
            .reorder(),
            .delete() {
                // delete action
            }
        ]
        return cell
    }
    dataSource.reorderingHandlers.canReorderItem = { _ in true }
    dataSource.reorderingHandlers.didReorder = { [weak self] _ in
        // data management after reordering
    }
    return dataSource
}()

UICollectionViewDiffableDataSourceの指定する際のセル設定で、チョロっとaccessoriesを指定します。
させたいアクションによって微妙に指定が違うので、詳しくは公式ドキュメントを見ながら実装してください。
たとえば削除についてはaccessoriesに対してクロージャーで指定しますが、並び替えはreorderingHandlersに対しての指定となります。

UICellAccessoryがセルの左右どちらに出るかは、デフォルトで決まっていて、削除は左、並び替えは右です。
ここは将来のiOSでどちらが標準になるかよくわからないところなので、デフォルトに従うが吉かなと思います。

編集モードじゃなくてもUICellAccessoryを表示しときたい

このdataSourceに対して、snapshotをapply()すると、編集可能なリストとなります。
ただこの指定だと、編集モードにしないとaccessoriesが表示されません。
collectionViewのisEditingをtrueにすることで編集モードにできますが、今回僕は編集専用画面としてつくったので、
編集モードに切り替えるのではなく、常時accessoriesが表示されていて欲しいと思いました。
またデフォルトだと左右のボタンには余白がほぼない状態で、もう少し余白が欲しいので、それも指定しました。

cell.accessories = [
    .reorder(displayed: .always, options: .init(reservedLayoutWidth: .custom(52))),
    .delete(displayed: .always, options: .init(reservedLayoutWidth: .custom(52))) {
        // delete action
    }
]

こんな指定になっています。
この指定だけで、CollectionViewの中で上手いことデータの並び替え・削除をやってくれるので、dataSource.snapshot().itemIdentifiersから編集後の結果が取得できます。

スワイプアクションで削除を実装する

編集モードにおける操作は以上なんですが、非編集モードのスワイプ操作のコード例も載せます。
UICollectionLayoutListConfigurationを指定するところでスワイプアクションを指定します。

var configuration = UICollectionLayoutListConfiguration(appearance: .plain)
configuration.trailingSwipeActionsConfigurationProvider = { indexPath -> UISwipeActionsConfiguration in
    let action = UIContextualAction(style: .destructive, title: nil) { [weak self] _, _, completion in
        // delete action
    }
    action.image = Asset.Images.trash.image
    return UISwipeActionsConfiguration(actions: [action])
}
let section = NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: environment)

普通のUICollectionViewCompositionalLayoutだと指定できなさそうで、ListConfiguration専用っぽいです。

(追記)UICellAccessory部分の背景色

普通のセルのbackgroundcolorを変えただけだと、アクセサリー部分はシステムデフォルトが適用されるみたいです。
UIBackgroundConfiguration というのを指定してあげると、アクセサリー部分も変えられました。

https://developer.apple.com/documentation/uikit/uicollectionviewcell/3600947-backgroundconfiguration

参考

https://dev.classmethod.jp/articles/wwdc-20-lists-in-uicollectionview/

Discussion