every Tech Blog

株式会社エブリーのTech Blogです。

UITableViewDiffableDataSourceを使ってクラッシュ率を改善しました

f:id:nanakookada:20210521173407p:plain

はじめに

iOSでTableviewやCollectionViewを扱っていると、UIとデータとの間で不整合が起きた際に NSInternalInconsistencyException というエラーを吐いてアプリが落ちるというのはよくある話だと思います。

TableViewに関してはiOS13から UITableViewDiffableDataSource が追加され、Apple曰くこの問題を回避できるらしいので、DELISH KITCHENのiOSアプリで採用してみました。

導入方法

Hashable化

セクションやアイテムに対応するオブジェクトがHashableに適合している必要があります。 今回は対象となるオブジェクトがユニークなIDを既に持っていたので簡単でした。

/// TableViewの各セルに対応するオブジェクト
struct Item: Hashable {

    let id: Int
    ...

    /// 追加1
    static func == (lhs: MessageDetailRowItem, rhs: MessageDetailRowItem) -> Bool {
        return lhs.id == rhs.message.id
    }

    /// 追加2
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

UITableViewDataSourceをUITableViewDiffableDataSourceに変更する

struct Section {
    let items: [Item]
}

このようなセクションがあると仮定して

class SomeClass: UITableViewDataSource {

    let sections: [Section]

    func numberOfSections(in tableView: UITableView) -> Int {
        return sections.count
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return sections[section].items.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        /// cellをデキューして加工して返す処理
    }
}

というUITableViewDataSourceの実装があった場合の変更点を示します。

まず、Section をHashableに適合させます。

次に、UITableViewDataSourceの代わりにUITableViewDiffableDataSourceを使うように変更します。

class SomeClass {
    private var dataSource: UITableViewDiffableDataSource<Section, Item>?

    func setupDataSource(tableView: UITableView) {
        dataSource = UITableViewDiffableDataSource<Section, Item>(tableView: tableView, cellProvider: { [weak self] (tableView, indexPath, item) -> UITableViewCell? in
            /// func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCellと一緒の内容
        }
    }

    func setupSnapshot() {
        var snapShot = NSDiffableDataSourceSnapshot<Section, Item>()
        let sections: [Section] = /// 省略
        snapShot.appendSections(sections)
        sections.forEach { snapShot.appendItems($0.items, toSection: $0) }
        dataSource.apply(snapShot)
    }
}

setupDataSourceはViewControllerのViewDidLoad、setupSnapshotsetupDataSourceより後でデータが取得できたタイミングで実行すれば良いと思います。

また、変更がある場合は現在のスナップショットをdataSourceから取得できるので、それに対して変更を加えて再度applyするだけで良いです。

performBatchUpdateなどの処理

dataSourceにapplyしたらTableViewにも反映されるので不要になります。

結果

日に数件エラーが出ていたのですが、0件になりました。

今回はTableViewに対しての改善でしたがCollectionViewにも同様のAPIが存在するので、そちらも改善していきたいと考えています。

参考

https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/implementing_modern_collection_views

Appleが提供しているサンプルプロジェクトです。 2021年3月18日時点では WiFiSettingsViewControllerTableViewEditingViewController がUITableViewDiffableDataSourceを使っているので参考になると思います!