スクロールビュー内のスクロールビュー

この記事では、 UIScrollViewの子孫であり、複数のスクロールビュー、テーブル( UITableView )またはコレクション( UICollectionView )を1つのコンテナーに追加できるOLEContainerScrollViewを紹介します。



可能な応用



OLEContainerScrollViewを使用して、次の目標を達成できます。



テーブルおよびコレクション内のセルの再利用



クラスの実装を見る前に、テーブルまたはコレクションの一般的な動作を見てみましょう。 UITableViewUICollectionViewの両方のクラスは、 UIScrollViewの子孫であり、同様に動作します。 ただし、主な違いは、テーブルとコレクションがセルを再利用することです。 ビューがスクロールしてセルが画面を超えて広がると、このセルはビューの階層から削除され(つまり、メッセージremoveFromSuperviewを受信し )、再利用キューに転送されます。 同時に、新しいセルが表示される前に、テーブルは再利用のために待機リストから空のセルを呼び出し、ビュー階層に再配置します。 このアプローチにより、大量のメモリ消費が回避され、新しいタイプのメモリの作成と割り当て(割り当て)の高価な操作の数が最小限に抑えられ、最速のスクロールが保証されます。







UITableViewでのセルの再利用の図。 非表示のセルは、ビュー階層から削除され(青い点線で表示)、スクロールの結果として表示される前にテーブルビューに追加されます。 通常、スクロールビューの場合と同様に、テーブルフレーム (水色の長方形)はコンテンツサイズ(コンテンツサイズ、赤い点線で囲まれたサイズ)よりも小さいことに注意してください。 ビデオ(H.264形式)をダウンロードします


コンテナ実装の簡単なアプローチ



スクロールビューでもある1つの共通コンテナに複数のスクロールビューを配置することは、非常に簡単なタスクです。





したがって、ネストされた各スクロールビューにコンテンツのサイズ以上のフレームサイズを設定すると、これらのスクロールビューが決してスクロールしないという状況が発生します。スクロール可能なオブジェクトはコンテナのみです。 これにより、ネストされたスクロールビューとそのコンテナー間のタッチ処理の干渉が回避されます。



このスキームは確かに機能しますが、重大な欠点が1つあります。過剰なメモリの浪費です。 ネストされたスクロールビューがテーブルまたはコレクションである場合、それらは理解しているすべてのセルが表示されるため、行ごとにセルを作成します。 コレクションに数百または数千のセルが含まれている場合、効果は非常に劇的です。スクロールが遅くなり、アプリケーションはデバイス上の使用可能なメモリをすべて消費します。







複数のスクロールビューのコンテナを実装する簡単なアプローチの例。 2つのテーブルが、1つの共通コンテナにサブビューとして追加されます 。それ自体がスクロールビュー(黒いストロークの長方形)です。 テーブルのフレーム (明るい青と明るい黄色の長方形)は、各テーブルのコンテンツを完全に含むように変更されます(赤い破線)。 これがセルの再利用にどのように影響したかに注目してください。セルが表示されるかどうか(コンテナのフレーム内、黒い長方形)に関係なく、各行のすべてのセルが同時に停止します。 ビデオ(H.264形式)をダウンロードします


OLEContainerScrollView



次に、スクロールビューのコンテナについて説明します。 OLEContainerScrollViewUIScrollViewの子孫であり、それに囲まれたビューをデッキまたはスタックのように自動的に配置します(OS XのNSStackViewと同様)。 このコンテナは、スクロールビューを特別な方法で処理しますが、スクロールビューだけでなく、すべてのタイプのビューで機能します。



ビューを追加する



ビューをコンテナに追加するには、 addSubviewToContainer:メソッドを使用する必要があります。 既存のaddSubview:/ removeFromSuperviewペアに依存するのではなく、コンテナにビューを追加および削除する新しいメソッドを作成することを余儀なくされました。ビューをコンテナに直接ではなく、プライベートcontentViewに追加したかったからです。 この手法により、スクロールインジケーターを表示するために作成されたUIScrollView自体のプライベートなサブ種からの干渉を避けることができました。後でネストされたビューをソートしてサイズを調整します。



ビューがコンテナに追加されるとすぐに、次のことが起こります。





スクロール中の位置合わせ



スクロール中、コンテナはそれに接続されているビューのフレームを次の方法で継続的に整列ます。





これは、前述の単純なアプローチと一貫しています。 次に、コンテナ内すべてのスクロールビューのフレームを 、コンテナのビューポートを満たす最小サイズになるように( 境界に従って )位置合わせする必要があります。





このアルゴリズムがどのように機能するかについては、以下のビデオをご覧ください。 最初は、最初のテーブルがコンテナの可視性全体を塗りつぶします(黒の線でマークされています)-テーブルフレーム (水色の長方形)はコンテナの境界に正確に等しくなります。 この場合、2番目のテーブルは完全に範囲外です- フレームの高さは0で、テーブルは見えません。 その結果、その中にセルを作成する必要はありません(黄色の点線)。



ユーザーがコンテナの内容をスクロールし、2番目のテーブルがスコープに入るとすぐに、テーブルフレーム (明るい黄色の長方形)の高さは、コンテナの高さと等しくなるまで、スコープの下の境界から増加し始めます。 同時に、テーブルが画面外に削除されている間、最初のテーブルのフレームはゼロに圧縮されます。 両方の表は、制限や条件なしでセルを再利用する可能性を制限なく使用できます。





OLEContainerScrollViewの動作のデモビデオ(H.264形式)をダウンロードします


コード



OLEContainerScrollViewクラスインターフェイスは次のようになります。

コード
@interface OLEContainerScrollView : UIScrollView - (void)addSubviewToContainer:(UIView *)subview; - (void)removeSubviewFromContainer:(UIView *)subview; @end
      
      







そして、ここにすべての作業を行うlayoutSubviewsメソッドの実装があります

 @implementation OLEContainerScrollView ... - (void)layoutSubviews { [super layoutSubviews]; // Translate the container view's content offset to contentView bounds. // This keeps the contentview always centered on the visible portion of the container view's // full content size, and avoids the need to make the contentView large enough to fit the // container view's full content size. self.contentView.frame = self.bounds; self.contentView.bounds = (CGRect){ self.contentOffset, self.contentView.bounds.size }; // The logical vertical offset where the current subview (while iterating over all subviews) // must be positioned. Subviews are positioned below each other, in the order they were added // to the container. For scroll views, we reserve their entire contentSize.height as vertical // space. For non-scroll views, we reserve their current frame.size.height as vertical space. CGFloat yOffsetOfCurrentSubview = 0.0; for (UIView *subview in self.contentView.subviews) { if ([subview isKindOfClass:[UIScrollView class]]) { UIScrollView *scrollView = (UIScrollView *)subview; CGRect frame = scrollView.frame; CGPoint contentOffset = scrollView.contentOffset; // Translate the logical offset into the sub-scrollview's real content offset and frame size. // Methodology: // (1) As long as the sub-scrollview has not yet reached the top of the screen, set its scroll position // to 0.0 and position it just like a normal view. Its content scrolls naturally as the container // scroll view scrolls. if (self.contentOffset.y < yOffsetOfCurrentSubview) { contentOffset.y = 0.0; frame.origin.y = yOffsetOfCurrentSubview; } // (2) If the user has scrolled far enough down so that the sub-scrollview reaches the top of the // screen, position its frame at 0.0 and start adjusting the sub-scrollview's content offset to // scroll its content. else { contentOffset.y = self.contentOffset.y - yOffsetOfCurrentSubview; frame.origin.y = self.contentOffset.y; } // (3) The sub-scrollview's frame should never extend beyond the bottom of the screen, even if its // content height is potentially much greater. When the user has scrolled so far that the remaining // content height is smaller than the height of the screen, adjust the frame height accordingly. CGFloat remainingBoundsHeight = fmax(CGRectGetMaxY(self.bounds) - CGRectGetMinY(frame), 0.0); CGFloat remainingContentHeight = fmax(scrollView.contentSize.height - contentOffset.y, 0.0); frame.size.height = fmin(remainingBoundsHeight, remainingContentHeight); frame.size.width = self.contentView.bounds.size.width; scrollView.frame = frame; scrollView.contentOffset = contentOffset; yOffsetOfCurrentSubview += scrollView.contentSize.height; } else { // Normal views are simply positioned at the current offset CGRect frame = subview.frame; frame.origin.y = yOffsetOfCurrentSubview; frame.size.width = self.contentView.bounds.size.width; subview.frame = frame; yOffsetOfCurrentSubview += frame.size.height; } } self.contentSize = CGSizeMake(self.bounds.size.width, fmax(yOffsetOfCurrentSubview, self.bounds.size.height)); } @end
      
      







コードの残りの部分は非常に定型的なものであり、GitHubでそれよく理解できます



自動レイアウト



自動レイアウトに関するいくつかの言葉: OLEContainerScrollViewはこの関数を内部的に使用せず、 自動レイアウトスペーサーを使用してこの動作を実装することはおそらく不可能です(とにかく、 UIScrollView自動レイアウトはあまり良い友達ではありません )。 ただし、内部のレイアウト自動レイアウトを使用する他のオブジェクトでこのクラスを使用しても問題はありません。 前述したように、手動レイアウトと自動レイアウトを非常に自由に組み合わせることができます。



おい、ポッドスペックはどこ?



私は意図的に(まだ) OLEContainerScrollViewからCocoaPodを 作成していません。 このクラスを作成して、非常に具体的な問題を解決しました。共通のコンポーネントに成長するのに十分な可能性があると思います。 もちろん、これまでのところそうではありません。 制限は次のとおりです。





このクラスの使用に興味がある場合は、そのコードを調べて独自の目的に使用していただければ幸いです。 私もあなたの改善を追加させていただきます( プルリクエストを行います )。 このクラスをCocoaPodとして見たい場合は、私に書いてください。



おわりに



1つの一般的なコンテナースクロールビューに複数の(スクロールを含む)ビューを配置することは日常的な手法ではありませんが、このアプローチにより、テーブルとコレクションのデータソースでの作業を簡素化し、レイアウトとレイアウトを簡素化して、セクションを別の種。



OLEContainerScrollViewは現在、完全に機能するコンポーネントではありませんが、皆さんの助けを借りてそうなることを願っています。 いずれにせよ、このコンポーネントを書くことで、 UIScrollViewデバイスとUIKit座標系の理解を深めることができました。



脚注:



  1. これは、将来のバージョンでより柔軟にしたいものです。 現在、このクラスは垂直レイアウトのみをサポートしています。
  2. これまでは、 UIScrollViewを継承するビューに対してのみこれを行いました。 ビューがスコープ内にない場合でも、通常のUIViewフレームの高さは変わりません。 ビューの寸法をゼロ化することで生じるレイアウトの変更(または自動レイアウトのエラー)を回避するために、そのように操作しました。 この振る舞いを変更することは将来難しくありません。




翻訳者から
さらに深く理解するには、 「UIScrollView理解する」の記事を読むことをお勧めします。




All Articles