複合「データソース」オブジェクトと機能的アプローチの要素

かつて、特定のタイプのセルを使用してUICollectionView完全に異なるタイプの1つのセルを追加し、「上」で処理されUICollectionView直接依存しない特別な場合にのみこれを行うタスクに直面しました。 このタスクは、メモリが私に役立つ場合、いくつifUICollectionViewDataSourceなりUICollectionViewDelegate- else ifUICollectionViewDataSourceおよびUICollectionViewDelegate内でブロックしUICollectionViewDelegate 。これは「プロダクション」コードに安全にUICollectionViewDelegate 、おそらくそこからどこにも行きません。

前述のタスクのフレームワーク内では、よりエレガントなソリューションを考えたり、これに「思慮深い」エネルギーを浪費したりする意味がありませんでした。 それでも、私はこの話を思い出しました。他の「データソース」オブジェクトをいくつでも単一の全体に構成できる特定の「データソース」オブジェクトを実装しようと考えていました。 当然、ソリューションは一般化され、任意の数のコンポーネント(0および1を含む)に適したものであり、特定のタイプに依存するものではありません。 これは現実的であるだけでなく、それほど難しくないことが判明しました(ただし、コードを「美しく」することはもう少し難しくなります)。

UITableView例で行ったことを示しUITableView 。 必要に応じて、 UICollectionView同様のコードを記述することは難しくありません。

「アイデアは、その具体化よりも常に重要です」


この格言は偉大な漫画本作家アラン・ムーア「キーパーズ 」、「Vはヴェンデッタの略 「卓越した紳士のリーグ」 )に属しますが、それはプログラミングに関するものではありませんか?

私のアプローチの主なアイデアは、 UITableViewDataSourceオブジェクトの配列を保存し、セクションの総数を返し、元の「データソース」オブジェクトのどれがこの呼び出しをリダイレクトするかをセクションにアクセスするときに決定できるようにすることです。

UITableViewDataSourceプロトコルには、セクション、行などの数を取得するための必要なメソッドが既にありますが、残念ながら、この場合、引数の1つとしてUITableView特定のインスタンスへの参照を渡す必要があるため、使用するのは非常に不便であることがわかりました。 そこで、標準のUITableViewDataSourceプロトコルをいくつかの追加の単純なメンバーで拡張することにしました。

 protocol ComposableTableViewDataSource: UITableViewDataSource { var numberOfSections: Int { get } func numberOfRows(for section: Int) -> Int } 

そして、複合「データソース」は、 UITableViewDataSourceの要件を実装し、 ComposableTableViewDataSourceの特定のインスタンスの1つの引数だけで初期化される単純なクラスであることが判明しました。

 final class ComposedTableViewDataSource: NSObject, UITableViewDataSource { private let dataSources: [ComposableTableViewDataSource] init(dataSources: ComposableTableViewDataSource...) { self.dataSources = dataSources super.init() } private override init() { fatalError("\(#file) \(#line): Initializer with parameters must be used.") } } 

現在は、 UITableViewDataSourceプロトコルのすべてのメソッドの実装を、対応するコンポーネントのメソッドを参照するような方法で記述するだけです。

「それは正しい決断でした。 私の決断


これらの言葉は、 ロシア連邦の初代大統領である ボリス・ニコラエヴィッチ・エリツィンのものであり、実際には以下のテキストを参照していない。

私は、 Swift言語の機能を利用するという正しい決定を下したように思われ、本当に便利であることが判明しました。

まず、セクションの数を返すメソッドを実装します-これは難しくありません。 上記のように、コンポーネントのすべてのセクションの総数が必要です。

 func numberOfSections(in tableView: UITableView) -> Int { // Default value if not implemented is "1". return dataSources.reduce(0) { $0 + ($1.numberOfSections?(in: tableView) ?? 1) } } 

(標準関数の構文と意味については説明しません。必要に応じて、インターネットはこのトピック に関する 優れた 入門 記事 いっぱいです。 かなり良い本をお勧めすることもできます。)

UITableViewDataSourceすべてのメソッドをUITableViewDataSource見てみると、引数として、テーブルへのリンクと、セクション番号または対応するIndexPath行の値のみを受け入れることがIndexPathます。 他のすべてのプロトコルメソッドの実装に役立ついくつかのヘルパーを作成します。

まず、すべてのタスクを「汎用」関数に減らすことができます。この関数は、特定のComposableTableViewDataSourceへの参照とセクション番号またはIndexPath値を引数としてIndexPathます。 便宜上、簡潔にするために、これらの関数のタイプにエイリアスを割り当てます 。 さらに、読みやすくするために、セクション番号のエイリアスを宣言することをお勧めします。

 private typealias SectionNumber = Int private typealias AdducedSectionTask<T> = (_ composableDataSource: ComposableTableViewDataSource, _ sectionNumber: SectionNumber) -> T private typealias AdducedIndexPathTask<T> = (_ composableDataSource: ComposableTableViewDataSource, _ indexPath: IndexPath) -> T 

(選択した名前を以下に説明します。)

次に、特定のComposableTableViewDataSourceと対応するセクション番号をComposedTableViewDataSourceセクション番号によって決定する単純な関数を実装します。

 private func decompose(section: SectionNumber) -> (dataSource: ComposableTableViewDataSource, decomposedSection: SectionNumber) { var section = section var dataSourceIndex = 0 for (index, dataSource) in dataSources.enumerated() { let diff = section - dataSource.numberOfSections dataSourceIndex = index if diff < 0 { break } else { section = diff } } return (dataSources[dataSourceIndex], section) } 

おそらく、あなたが私のものより少し長いと思うなら、実装はよりエレガントで簡単ではないことが判明します。 たとえば、同僚はこの関数にバイナリ検索を実装することをすぐに提案しました(以前は、たとえば初期化中に、セクション数のインデックス- 整数の単純な配列を作成することで )。 または、セクション番号の対応表のコンパイルと保存にも少し時間を費やしますが、 時間の複雑さ O(n)またはO(log n)のメソッドを常に使用する代わりに、O(1)のコストで結果を得ることができます。 しかし、明らかな必要性と適切な測定なしに時期尚早の最適化に関与しないように、私は偉大なドナルド・クヌースの助言を受けることにしました。 はい、この記事についてではありません。

最後に、上記のAdducedSectionTaskおよびAdducedIndexPathTaskを受け入れ、特定のComposedTableViewDataSourceインスタンスに「リダイレクト」する関数:

 private func adduce<T>(_ section: SectionNumber, _ task: AdducedSectionTask<T>) -> T { let (dataSource, decomposedSection) = decompose(section: section) return task(dataSource, decomposedSection) } private func adduce<T>(_ indexPath: IndexPath, _ task: AdducedIndexPathTask<T>) -> T { let (dataSource, decomposedSection) = decompose(section: indexPath.section) return task(dataSource, IndexPath(row: indexPath.row, section: decomposedSection)) } 

そして今、これらすべての機能のために私が選んだ名前を説明できます。 簡単です。機能的な命名スタイルを反映しています。 つまり 文字通りほとんど意味がありませんが、印象的な音。

最後の2つの関数は双子のように見えますが、少し考えた後、コードの重複を取り除くことをあきらめました。利点よりも不便をもたらしたためです。変換関数を出力またはセクション番号に転送して元の型に戻す必要がありました。 さらに、この一般化されたアプローチを再利用する確率はゼロになる傾向があります。

これらすべての準備とヘルパーは、実際には、プロトコルメソッドの実装に信じられないほどの利点をもたらします。 テーブル構成方法:

 func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return adduce(section) { $0.tableView?(tableView, titleForHeaderInSection: $1) } } func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { return adduce(section) { $0.tableView?(tableView, titleForFooterInSection: $1) } } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return adduce(section) { $0.tableView(tableView, numberOfRowsInSection: $1) } } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { return adduce(indexPath) { $0.tableView(tableView, cellForRowAt: $1) } } 

行の挿入と削除:

 func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { return adduce(indexPath) { $0.tableView?(tableView, commit: editingStyle, forRowAt: $1) } } func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { // Default if not implemented is "true". return adduce(indexPath) { $0.tableView?(tableView, canEditRowAt: $1) ?? true } } 

同様に、セクションインデックスヘッダーのサポートを実装できます。 この場合、セクション番号の代わりに、ヘッダーインデックスを操作する必要があります。 また、ほとんどの場合、 ComposableTableViewDataSourceプロトコルに追加のフィールドを追加すると便利です。 この部分は素材の外に置いた。

「不可能な今日は明日可能になる」


これらはロシアの科学者コンスタンチン・エドゥアルドヴィチ・ツィオルコフスキーの言葉で、理論宇宙飛行学の創始者です。

まず、提示されたソリューションは行のドラッグアンドドロップをサポートしていません。 元の計画には、構成する「データソース」オブジェクトの1つでのドラッグアンドドロップのサポートが含まれていましたが、残念ながら、これはUITableViewDataSourceのみを使用して達成することはできません。 このプロトコルのメソッドは、特定の行を「ドラッグアンドドロップ」し、ドラッグの最後に「コールバック」を受信できるかどうかを決定します。 また、イベント自体の処理は、 UITableViewDelegateメソッド内で暗示されていUITableViewDelegate

第二に、そしてさらに重要なこととして、画面上のデータを更新するメカニズムを検討する必要があります。 これは、 ComposableTableViewDataSourceデリゲートプロトコルを宣言することによって実装できると思います。そのメソッドはComposedTableViewDataSourceによって実装さComposedTableViewDataSource 、元の「データソース」が更新を受信したという信号を受信します。 2つの質問が残っています: ComposedTableViewDataSource内でどのComposableTableViewDataSourceが変更されたかを確実に判断する方法と、それが別個の最も単純なタスクではなく、いくつかのソリューション(たとえば)を持っている方法です。 そしてもちろん、 ComposedTableViewDataSourceデリゲートComposedTableViewDataSourceが必要になりComposedTableViewDataSource 。このComposedTableViewDataSourceは、複合「データソース」を更新するときに呼び出され、クライアントタイプ( コントローラービューモデルなど )によって実装されます。

これらの問題を長期にわたって調査し、記事の第2部で取り上げたいと思います。 それまでの間、あなたはこれらの実験について読むことに興味がありました!

PS


先日、私はその修正のために導入部で言及されたコードに入らなければなりませんでした:私はこれらの2つのタイプのセルを交換する必要がありました。 簡単に言えば、私は自分自身を苦しめ、さまざまな場所に絶えず出現する「冒”」するIndex out of bounds 。 説明したアプローチを使用する場合、イニシャライザ引数として渡された配列内の2つの「データソース」オブジェクトを交換するだけで済みます。

参照:
- 完全なコードと例でPlaygroud
- 私のTwitter

Source: https://habr.com/ru/post/J442138/


All Articles