every Tech Blog

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

UICollectionViewDiffableDataSource / UITableViewDiffableDataSource のsnapshotをResult Buildersを使って宣言的に書く

はじめに

2023年8月1日、MAMADAYSはトモニテに生まれかわりました。

tomonite.com

iOSアプリもトモニテに名前を変え、これまでのメイン機能である「育児記録」「妊娠週数管理」を軸として、家族やパートナー、家族以外の人や社会との接点を作るためのシェア機能やコミュニティ機能などの拡充をめざしていきます。

トモニテのiOSアプリは新規作成画面を中心にSwiftUIの導入を進めています。一方、既存の画面を全面的にSwiftUIに置き換えることは考えていないため、今後もUIKitの画面のメンテナンスも継続していきます。 今回はUIKitの画面にResult Buildersを導入してメンテナンス性を向上する取り組みをご紹介します。

Result Buildersとは

Result BuildersはSwift 5.4で導入されました。 Result Buildersを使うと、リストやツリーなどの構造化されたデータを、より自然で宣言的な構文で作成することができます。

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/advancedoperators/#Result-Builders

Result BuildersはSwiftUIの@ViewBuilderや、RegexBuilderで使われています。

ArrayBuilderを作る

Result Buildersを使って、任意の型の配列を出力する関数を宣言的に書けるようにするArrayBuilderというものを考えてみようと思います。

以下のようなIntの配列を出力する関数を、

var numbers: [Int] {
    var numbers: [Int] = [
        1,
        2
    ]
    if xxxx {
        numbers.append(3)
    }
    return numbers
}

ArrayBuilderを用いて以下のように書けるようにします。

@ArrayBuilder<Int>
var numbers2: [Int] {
    1
    2
    if xxxx {
        3
    }
}

ArrayBuilderを適用した場合、以下のような点で改善されていると思います。

  • 変数宣言、append、returnのような手続き的な記述が不要で、コードが簡潔になった
  • コードの構造と出力する値の構造が一致していて理解しやすい
  • 値の追加、削除などの変更がしやすい

ArrayBuilderの実装は以下のようになります。

@resultBuilder
struct ArrayBuilder<OutputModel> {

    static func buildBlock(_ components: [OutputModel]...) -> [OutputModel] {
        return components.flatMap { $0 }
    }

    static func buildExpression(_ expression: OutputModel) -> [OutputModel] {
        return [expression]
    }

    static func buildExpression(_ expression: ()) -> [OutputModel] {
        return []
    }

    static func buildOptional(_ component: [OutputModel]?) -> [OutputModel] {
        return component ?? []
    }

    static func buildEither(first component: [OutputModel]) -> [OutputModel] {
        return component
    }

    static func buildEither(second component: [OutputModel]) -> [OutputModel] {
        return component
    }

    static func buildArray(_ components: [[OutputModel]]) -> [OutputModel] {
        Array(components.joined())
    }
}

UICollectionViewDiffableDataSource / UITableViewDiffableDataSourceのsnapshotに適用

次に、UICollectionViewDiffableDataSource / UITableViewDiffableDataSourceによみこませるためのsnapshotを作成する処理にArrayBuilderを使うことを考えました。

snapshotを生成する処理は以下のようなイメージです。

class ViewModel {
    
    enum Section: Hashable {
        case .section1
        case .section2
    }

    enum Row: Hashable {
        case .row1
        case .row2
        case .row3
        case .row4
        case .row5
    }

    public var snapshot: NSDiffableDataSourceSnapshot<SectionType, RowType> {
        var snapshot = NSDiffableDataSourceSnapshot<SectionType, RowType>()
        snapshot.appendSections([.section1, .section2])
        snapshot.appendItems([.row1, .row2, .row3], toSection: .section1)
        snapshot.appendItems([.row4, .row5], toSection: .section2)
        return snapshot
    }
}

ArrayBuilderを使用したDiffableTableViewModelプロトコルを作成し、以下のような書き方をできるようにします。

class ViewModel: DiffableTableViewModel {
    typealias SectionType = Section
    typealias RowType = Row

    enum Section: Hashable {
        case .section1
        case .section2
    }

    enum Row: Hashable {
        case .row1
        case .row2
        case .row3
        case .row4
        case .row5
    }

    // この関数が改善されています
    var tableSections: [TableSection<Section, Row>] {
        TableSection(.section1) {
            .row1
            .row2
            .row3
        }
        TableSection(.section2) {
            .row4
            .row5
        }
    }
}

ここでは使用していませんが、tableSections関数の中でfor文やif文を用いることもできます。

DiffableTableViewModelを導入することで以下のような効果が得られたと思います。

  • 手続き的な記述が不要でコードが簡潔
  • 画面要素の構造とコードの構造が一致していて理解しやすい
  • 画面要素の追加/削除/並べ替えなどに容易に対応できる

DiffableTableViewModelプロトコルは以下のように定義しています。

import UIKit

protocol DiffableTableViewModel {
    associatedtype SectionType: Hashable
    associatedtype RowType: Hashable

    @ArrayBuilder<TableSection<SectionType, RowType>>  var tableSections: [TableSection<SectionType, RowType>] { get }
}

extension DiffableTableViewModel {

    var snapshot: NSDiffableDataSourceSnapshot<SectionType, RowType> {
        var snapshot = NSDiffableDataSourceSnapshot<SectionType, RowType>()
        snapshot.appendSections(tableSections.map {$0.sectionType})
        tableSections.forEach { tableSection in
            snapshot.appendItems(tableSection.rowTypes, toSection: tableSection.sectionType)
        }
        return snapshot
    }
}

struct TableSection<SectionType, RowType> {
    let sectionType: SectionType
    let rowTypes: [RowType]

    init(_ sectionType: SectionType, @ArrayBuilder<RowType> rowsBuilder: () -> [RowType]) {
        self.sectionType = sectionType
        self.rowTypes = rowsBuilder()
    }
}

以上参考になれば幸いです。