Diffable Data Sourceでセルをリロードする方法

## Diffable Data Sourceとセルのリロード

iOS 13以降、UITableViewやUICollectionViewで使えるDiffable Data Sourceですが、WWDCのセッションやAppleのサンプルを参考に実装すると、あるセルの内容を更新したいときにうまくいかない場合があります。

ここではその問題を回避する方法をいくつか紹介します。

なお、Diffable Data Sourceそのものについては、以下を見てもらうのがわかりやすいかと思います。

- [WWDC 2019: Advances in UI Data Sources](https://developer.apple.com/videos/play/wwdc2019/220/)
- [時代の変化に応じて進化するCollectionView ~Compositional LayoutsとDiffable Data Sources~](https://qiita.com/shiz/items/a6032543a237bf2e1d19#data-source)

## うまくいかないパターン

[Snapshot](https://developer.apple.com/documentation/uikit/nsdiffabledatasourcesnapshot) の `ItemIdentifierType` は `Hashable` でさえあればなんでも構いません。WWDCのセッションやそこで参照されているサンプルでは、セルに表示するデータも含めたアイテムのstructを作ってそれを `ItemIdentifierType` としています。そうすることで、セルを構築する際に必要なものがそこに揃っているので便利です。

話を単純にするため、UITableViewにユーザー名を表示するという例で説明します。

<img width="370" alt="Screenshot" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33397/e4bf072b-5584-a1a8-3b61-3ea5bac110a5.png">

今回の例では、ユーザーは整数のIDとユーザー名で構成されるものとして、次のようにstructを定義します。

```swift
struct User: Hashable {
    let id: Int
    let name: String

    static func == (lhs: User, rhs: User) -> Bool {
        return lhs.id == rhs.id
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}
```

Snapshotはこんな感じで構築します。

```swift
var snapshot = NSDiffableDataSourceSnapshot<Section, User>()
snapshot.appendSections([.first])
snapshot.appendItems([
    User(id: 1, name: "Henry"),
    User(id: 2, name: "Thomas"),
    User(id: 3, name: "Percy"),
], toSection: .first)
dataSource.apply(snapshot, animatingDifferences: false)
```

このSnapshotを適用しているData Sourceは、次のような感じです。
セルを構築する際に、そこに表示するユーザー名として `User` の `name` がそのまま使えるので便利ですね。

```swift
dataSource = UITableViewDiffableDataSource(tableView: tableView,
                                           cellProvider: { (tableView, indexPath, user) in
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
    cell.textLabel?.text = user.name
    return cell
})
```

さて、ここでボタンが押されると、次のSnapshotを適用してみるようにします。
要は2番のidを持つユーザーの名前を変更しています。

```swift
var snapshot = NSDiffableDataSourceSnapshot<Section, User>()
snapshot.appendSections([.first])
snapshot.appendItems([
    User(id: 1, name: "Henry"),
    User(id: 2, name: "Tommy"),    // be modified
    User(id: 3, name: "Percy"),
], toSection: .first)
dataSource.apply(snapshot, animatingDifferences: true)
```

期待される動作は、Thomasと表示されていた2行目がTommyに変更されることですが、これはうまくいきません。Thomasのままになります :frowning2:

これを解決していきましょう。

## 解決策1： idだけで同値性を判定しない

さて、なぜ期待通りに動作しないのでしょうか。
それは、 `User` の同値性はidのみで比較されているからです。

```swift
static func == (lhs: User, rhs: User) -> Bool {
    return lhs.id == rhs.id
}
```

この同値性の評価においては、 `name` は存在しないと考えても同じです。
`User(id: 2, name: "Thomas") == User(id: 2, name: "Tommy")` を、 `User(id: 2) == User(id: 2)` と考えればいいでしょう。当然、 **同じもの** だと判定されます。同じなら更新されないのは当たり前です。

そこで、これを別物として扱ってやれば、更新はされるようになります。

```swift
struct User: Hashable {
    let id: Int
    let name: String

//    static func == (lhs: User, rhs: User) -> Bool {
//        return lhs.id == rhs.id
//    }
//
//    func hash(into hasher: inout Hasher) {
//        hasher.combine(id)
//    }
}
```

SwiftのstructはStoredプロパティがすべて `Hashable` なら、自動でそれらをすべて比較する `==` と `hash(into:)` が生成されるのでこれで構いません。

これで、 `User(id: 2, name: "Thomas")` と `User(id: 2, name: "Tommy")` は別物になったので、ちゃんと表示が更新されるようになります。

ただ、アニメーションさせるとわかるのですが、Thomasの行が削除されて、Tommyの行が追加されたという扱いになるので少し違和感が残ります。

## 解決策2: リロードさせる

実はSnapshotには、特定のアイテムをリロードさせるメソッドが用意されています。

```swift
mutating func reloadItems(_ identifiers: [ItemIdentifierType])
```

なんだ、最初からこれを使えばよかったのか！
そこで `User` は元のidでのみ比較するように戻して、ボタンが押された時の処理を次のようにしてみます。すると…ちっとも更新されません :cry: 

```swift
var snapshot = dataSource.snapshot()
snapshot.reloadItems([User(id: 2, name: "Tommy")])
dataSource.apply(snapshot, animatingDifferences: true)
```

なぜでしょうか。
実はリロードはされているのです。しかし、セルを構築する際に呼ばれる以下のクロージャーで `user.name` がThomasのままなのです。

```swift
cellProvider: { (tableView, indexPath, user) in
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
    cell.textLabel?.text = user.name // Thomas
    return cell
}
```

落ち着いて考えてみましょう。
Snapshotの `ItemIdentifierType` は名前が示すとおりアイテムの識別子となる型です。ですから、 `reloadItems()` に渡すのはリロードしたいセルの識別子（の配列）です。

```swift
snapshot.reloadItems([User(id: 2, name: "Tommy")])
```

これは単にidが2番のものをリロードしてくれと言っているだけです（同値性の判定はidのみに戻しています）。 **持っているデータを更新してくれと言っているわけではありません。**

Data SourceやSnapshotに渡すのはあくまでも識別子であってデータそのものではありません。ただ、データも識別子の一部として一緒に持っておくと便利だからそうしているだけです。識別子としては変更がないのにデータを変更したいという考えが間違っているように思います。

そこで、識別子とデータを別に管理するようにしてみます。まず、識別子とデータに分けます。
※以下の例はちょっとバカっぽいですが、現実のUserには名前以外にも他にいろいろデータがあると思います。

```swift
struct UserID: Hashable {
    let id: Int
}

struct User {
    let name: String
}
```

Snapshotに渡すのは識別子、つまり `UserID` です。

```swift
var snapshot = NSDiffableDataSourceSnapshot<Section, UserID>()
snapshot.appendSections([.first])
snapshot.appendItems([
    UserID(id: 1),
    UserID(id: 2),
    UserID(id: 3),
], toSection: .first)
dataSource.apply(snapshot, animatingDifferences: false)
```

識別子からデータを取れるようにしましょう。

```swift
private var users: [UserID: User] = [
    UserID(id: 1): User(name: "Henry"),
    UserID(id: 2): User(name: "Thomas"),
    UserID(id: 3): User(name: "Percy"),
]
```

セルを構築するときはここからデータを取得します。

```swift
dataSource = UITableViewDiffableDataSource(tableView: tableView, cellProvider: { [weak self] (tableView, indexPath, userID) in
    guard let self = self else { return nil }
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
    cell.textLabel?.text = self.users[userID]?.name
    return cell
})
```

このようにしておけば、識別子は更新しなくてもデータは更新できます。

```swift
let userID = UserID(id: 2)

// データを更新
users[userID] = User(name: "Tommy")

// リロード要求
var snapshot = dataSource.snapshot()
snapshot.reloadItems([userID])
dataSource.apply(snapshot, animatingDifferences: true)
```

これで期待する動きになります。
ただ、データを別に管理しなきゃいけなくなりました。

## 解決策3: 識別子からデータを「参照」させる

もともとデータをどこかで管理しているのなら解決策2は素直でいいんじゃないかなと思っているのですが、最初の（うまくいかないパターンのように）識別子の方にデータを持たせたい場合もあると思います。
そこで識別子にデータを持たせるのではなく、識別子からデータを参照させましょう。識別子自体は更新せずに、識別子に参照させたものを更新するようにします。

`User` のstructは次のようにします。

```swift
struct User: Hashable {
    let id: Int

    class Info {
        var name: String
        init(name: String) {
            self.name = name
        }
    }
    let info: Info

    init(id: Int, info: Info) {
        self.id = id
        self.info = info
    }
    
    static func == (lhs: User, rhs: User) -> Bool {
        return lhs.id == rhs.id
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}
```

違いは、id以外の情報（といってもここではnameしかないですが）を `Info` という **class** に入れて、それを参照するようにしました。 `Info` はstructではダメです。classにして参照型にする必要があります。

こうすることで、次のように `User.info` 自体は変更せずに `Info` の中身を更新することができます。（`user` は `let` になっていて変更してないことに注目）

```swift
var snapshot = dataSource.snapshot()

guard let user = snapshot.itemIdentifiers(inSection: .first)
    .first(where: { $0.id == 2 }) else { return }
user.info.name = "Tommy"

snapshot.reloadItems([user])
dataSource.apply(snapshot, animatingDifferences: true)
```

## まとめ

Diffable Data Sourceでセルをリロードさせたいなら次の方法が考えられます。
それぞれ長所・短所はあるので、どれが一番いいというのは一概には言えないと思います。

- 別の識別子であると認識させる（厳密にはリロードではない）
- 識別子とは別にデータを管理しておく
- 識別子からデータを「参照」させる

なお、今回の検証のために作ったプログラム全体はGitHubに置いてあります。
https://github.com/hironytic/DiffableDataSourceReload