iOS13で並び替えアニメーション


はじめに

iOS12以前のCollectionViewやTableViewのデータの扱い方や並び替えのアニメーションのイメージを一言で表すと、「煩雑で難しい」でした...

ところが、iOS13からCollectionViewやTableViewでのデータの扱い方が簡単になった&なんかアニメーションも自動でやってくれるようになりました!

ただ、なったはいいのですがiOS12以前のアニメーションの仕組みを考えるととてもその自動アニメーションの動作が本当に実用に耐えうるのか疑問が残るところ...

こいうことで実際に自動アニメーションを何パターンか試してみました!

どう変わったのか

iOS12以前

データの扱い

特にデータを扱うためのクラスなどはなく、自分でデータをよしなに扱う必要がありました。
また、このデータと表示されるセルの対応などをUICollectionViewDataSourceで自分できちんと扱う必要がありました。
多分こんな感じで扱う人が多かったんじゃないかと

struct Section {
    /// 何か必要なデータ
    let datas: [Data]
}

struct Data {
    /// 何か必要なデータ
}

extension ViewController: CollectionViewDataSource {
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        /// セクション数を返す
        /// 自動でやってほしい...
    }

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        /// セクション内のアイテムの数を返す
        /// 自動でやってほしい...
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        /// セルの値を設定する
    }
}

アニメーション

iOS12以前の並び替えアニメーションは非常に複雑、かつちょっとでもミスするとすぐクラッシュするので非常に扱いに困ります...
詳しく知りたい方は、こちらの記事が非常に参考になるので読んでみてください!
【Swift】CollectionViewを再理解する

    collectionView.performBatchUpdates {
        /// この中でcollectionViewと元データに対してDelete、Insert,Moveを行うが、
        /// DeleteとInsertではみているIndexの対象が異なっていたりと複雑で、
        /// その複雑さによるミスなどで削除対象にしたデータをMoveしようとしたり、
        /// 同じ場所にMove/Insertしようとしたり、データ数が一致しなくなったりでクラッシュする
        /// クラッシュログもっとわかりやすくして...
    }

iOS13以降

ここからが本題

データの扱い

iOS13以降はしっかりとデータを扱うためのクラスが用意されています!
https://developer.apple.com/documentation/uikit/uicollectionviewdiffabledatasourcereference

使い方はこんな感じです

enum Section {
    /// セクション情報
    /// Hashableに適合していればenumでなくてもよい
}

struct Item {
    /// Hashableに適合していればなんでもよい
}

/// これが本体
private var dataSource: UICollectionViewDiffableDataSource<Section, Item> = {
    let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) {
            (collectionView: UICollectionView, indexPath: IndexPath, item: Item) -> UICollectionViewCell? in
        /// セルの値を設定する
    }

    dataSource.supplementaryViewProvider = { [weak self]
        (collectionView: UICollectionView, kind: String, indexPath: IndexPath) -> UICollectionReusableView? in
        /// ヘッダ、フッタなどの値を設定する
    }
    return dataSource
}()

func initDataSource() {
    var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
    /// セクションを登録
    snapshot.appendSections("セクションのリスト")
    /// アイテムを登録
    snapshot.appendItems("アイテム", toSection: "登録したいセクション")
    /// データを反映
    dataSource.apply(snapshot)
}    

一見iOS12より複雑になったように見えますが、セクション数やアイテム数の処理を書かなくて良いですし、
実データと表示が対応するようになるので非常にわかりやすくなっています!

アニメーション

なんとおどろき!上記ソースのデータ反映を下に変えるだけ!!!

/// データを反映
dataSource.apply(snapshot, animatingDifferences: true)

DiffableDataSourceはHashで一意にセクションとアイテムを管理しているので、どのアイテムが削除/追加/移動されたかを特定することができます。そのおかげで、自動的にアニメーションさせることもできるようになっているのです!

実際にどういったアニメーションがされるのか

簡単にですが、2パターンの自動アニメーションを検証してみました。

レイアウトが変わるパターン

CompositionalLayoutと組み合わせてレイアウトを切り替えられるようにしました。

スムーズにアニメーションしたりしなかったりで微妙な感じですね...
あまり大きくレイアウトが変わるアニメーションや、ヘッダがある場合のアニメーションには向かないようです

セクション数が変わるパターン


これはセクション数が変わるからなのかは微妙ですが(上記と同様に移動量が多いことが原因かも)、
移動しないでフェードで表示されるアニメーションが散見されますね...
ここも注意が必要そうです

まとめ

iOS13でとても複雑だったアニメーションが自動化され楽になりました。
ですが、実際に動かしてみた感じうまくアニメーションさせるにはコツが必要そうなため、
実際のアプリに適用するにはなかなか苦労しそうです...

実際に試したソースを公開しているので、よかったら参考にしてください!
https://github.com/rihitenLab/DiffableDataSourceAnimationVerification