ローソンデジタルイノベーション テックブログ

ローソンデジタルイノベーション(LDI)の技術ブログです

DiffableDataSourceで表示が更新されない? Hashable実装の落とし穴

はじめに

こんにちは、ローソンデジタルイノベーション(LDI)でiOSエンジニアを担当している山形です。
先日、以下の記事でDiffableDataSourceへの移行についてご紹介しました。 techblog.ldi.co.jp

今回はDiffableDataSourceへの移行にあたって必要なSectionIdentifierTypeItemIdentifierTypeに該当するデータをHashableに準拠させる上で注意したいことをご紹介します。
そのため一通りDiffableDataSourceへの対応は済んでいる前提でのお話になります。

当記事でDiffableDataSourceと記載しているものは以下の2つを対象としております。
掲載しているサンプルコードはUICollectionViewDiffableDataSourceを利用する例となっていますが、どちらの場合でもお役に立てるかと思います。 developer.apple.com developer.apple.com

SectionIdentifierTypeItemIdentifierTypeHashableに準拠させる上での注意点

DiffableDataSourceにおいて Hashable プロトコルは結構重要で、Hashableに準拠することで、DiffableDataSourceSectionIdentifierTypeItemIdentifierType を使用して、セクションとアイテムを一意に識別します。

ではHashableに準拠させる上での注意点をご紹介していきます。

【注意点】Hashable実装の落とし穴:表示内容の変更が反映されないケース

例えばセルにタイトルと説明文を表示させるためのHashableなオブジェクトを用意したとしましょう。
以下のような構造体で良さそうです。

struct LDITechBlogItem: Hashable {
    private let id: Int // Hash衝突を避けるため一意のIDも持っておきます。冒頭で紹介した記事に理由を記載しています。
    let title: String
    let description: String
}

例えば、この構造体を取り扱うセルをユーザによる操作など何らかの契機に内容はそのままに見た目を切り替えたいとします。
当記事で説明するにあたってセルの生成とセットアップを以下のようにしてみました。

以下のようなセルのタイプを定義しました。

enum LDITechBlogCellType {
    case normal
    case Special
}

セルの生成とセットアップを以下のように行って表示内容に反映させてみます。

guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "LDITechBlogCellReuseIdentifier", for: indexPath) as? LDITechBlogCell else {
    return UICollectionViewCell()
}

cell.setup(item: ldiTechBlogItem, cellType: cellType) // この部分で`LDITechBlogItem`と`LDITechBlogCellType`を別々で渡しています。

DiffableDataSourceに対応するための対応を一通り済ませいざ、動かしてみるとLDITechBlogCellTypeを切り替えた時にセルが更新されません!

理由の解説と解決策
理由

なぜ更新されないのかについて解説します。
理由は単純でHashableオブジェクトであるLDITechBlogCellItemLDITechBlogCellTypeがプロパティとして定義されていないためです。

前述の通りDiffableDataSourceがセクションとアイテムを一意に識別しているのは Hashableに準拠したSectionIdentifierTypeItemIdentifierType なのです。

DiffableDataSourceは、apply(_:animatingDifferences:)メソッドが呼ばれた時に、以前のスナップショットと新しいスナップショットを比較します。
この比較は、SectionIdentifierTypeItemIdentifierTypeHashable に基づいて行われます。LDITechBlogItemcellType が含まれていない場合、titledescription が同じであれば、DiffableDataSource はアイテムが変更されていないと判断し、セルの更新を行いません。

解決策

解決策としてはLDITechBlogCellTypeに差分更新に必要な情報を持たせ適切にハッシュ値が更新できるようにすることです。

struct LDITechBlogItem: Hashable {
    private let id: Int
    let title: String
    let description: String
+   let cellType: LDITechBlogCellType
}

このようにすることでLDITechBlogCellTypeを切り替えた時に適切にハッシュ値が更新され表示も更新できるようになります。

最後に

今回はDiffableDataSource対応におけるHashableに準拠する上で注意したいことを記事にしました。
Hashableオブジェクトに差分更新に必要な情報をしっかり持たせるようにすることの重要性についてはご理解いただけたでしょうか。
逆にHashableオブジェクトには差分更新に適用させたくない情報をあえて持たせないことや、Hashableに準拠する上でhash(into:)メソッドを自前で実装してハッシュ値に含めないなど要件によっては実装の幅も広がりそうですね。 developer.apple.com

ここまで記事を読んでいただきありがとうございます!
今後もローソンデジタルイノベーションでは技術ブログを更新していきますので、是非「読者になる」で応援していただけますと幸いです。