はじめに
2023年8月1日、MAMADAYSはトモニテに生まれかわりました。
iOSアプリもトモニテに名前を変え、これまでのメイン機能である「育児記録」「妊娠週数管理」を軸として、家族やパートナー、家族以外の人や社会との接点を作るためのシェア機能やコミュニティ機能などの拡充をめざしていきます。
トモニテのiOSアプリは新規作成画面を中心にSwiftUIの導入を進めています。一方、既存の画面を全面的にSwiftUIに置き換えることは考えていないため、今後もUIKitの画面のメンテナンスも継続していきます。 今回はUIKitの画面にResult Buildersを導入してメンテナンス性を向上する取り組みをご紹介します。
Result Buildersとは
Result BuildersはSwift 5.4で導入されました。 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() } }
以上参考になれば幸いです。