可能な応用
OLEContainerScrollViewを使用して、次の目標を達成できます。
- スクロール中の通常の動作が影響を受けないように、いくつかのスクロールビュー(またはテーブル、コレクション)を上下に配置します。 テーブルまたはコレクションの場合、セルの再利用メカニズムの操作性を維持することについて話します。
- 複数のセクションで構成されるテーブルまたはコレクションを、次々に配置される複数の単一セクションのテーブルまたはコレクションに分割することにより、1つの複雑なUITableViewDataSourceまたはUICollectionViewDataSourceをいくつかの単純なデータソースに変換します。
- レイアウトを管理することなく、コレクションの上または下にヘッダーまたはフッター( headerまたはfooter )を追加します。 この場合、単純なUIScrollViewsまたはUIViewsになります。
テーブルおよびコレクション内のセルの再利用
クラスの実装を見る前に、テーブルまたはコレクションの一般的な動作を見てみましょう。 UITableViewとUICollectionViewの両方のクラスは、 UIScrollViewの子孫であり、同様に動作します。 ただし、主な違いは、テーブルとコレクションがセルを再利用することです。 ビューがスクロールしてセルが画面を超えて広がると、このセルはビューの階層から削除され(つまり、メッセージremoveFromSuperviewを受信し )、再利用キューに転送されます。 同時に、新しいセルが表示される前に、テーブルは再利用のために待機リストから空のセルを呼び出し、ビュー階層に再配置します。 このアプローチにより、大量のメモリ消費が回避され、新しいタイプのメモリの作成と割り当て(割り当て)の高価な操作の数が最小限に抑えられ、最速のスクロールが保証されます。
UITableViewでのセルの再利用の図。 非表示のセルは、ビュー階層から削除され(青い点線で表示)、スクロールの結果として表示される前にテーブルビューに追加されます。 通常、スクロールビューの場合と同様に、テーブルフレーム (水色の長方形)はコンテンツサイズ(コンテンツサイズ、赤い点線で囲まれたサイズ)よりも小さいことに注意してください。 ビデオ(H.264形式)をダウンロードします 。
コンテナ実装の簡単なアプローチ
スクロールビューでもある1つの共通コンテナに複数のスクロールビューを配置することは、非常に簡単なタスクです。
- ネストされたスクロールビューのコンテナになるUIScrollViewを作成します。
- コンテナにネストされたスクロールビューを追加します。 単純なスクロールビュー、またはテーブルまたはコレクションのいずれかです。
- そのようなフレームにコンテンツ全体( contentSize )が含まれるように、スクロールビューを設定します。 スクロールビューの位置を上下に設定します。
- コンテナのコンテンツサイズ( contentSize ) を、ネストされたビューのフレームの合計として設定します。
したがって、ネストされた各スクロールビューにコンテンツのサイズ以上のフレームサイズを設定すると、これらのスクロールビューが決してスクロールしないという状況が発生します。スクロール可能なオブジェクトはコンテナのみです。 これにより、ネストされたスクロールビューとそのコンテナー間のタッチ処理の干渉が回避されます。
このスキームは確かに機能しますが、重大な欠点が1つあります。過剰なメモリの浪費です。 ネストされたスクロールビューがテーブルまたはコレクションである場合、それらは理解しているすべてのセルが表示されるため、行ごとにセルを作成します。 コレクションに数百または数千のセルが含まれている場合、効果は非常に劇的です。スクロールが遅くなり、アプリケーションはデバイス上の使用可能なメモリをすべて消費します。
複数のスクロールビューのコンテナを実装する簡単なアプローチの例。 2つのテーブルが、1つの共通コンテナにサブビューとして追加されます 。それ自体がスクロールビュー(黒いストロークの長方形)です。 テーブルのフレーム (明るい青と明るい黄色の長方形)は、各テーブルのコンテンツを完全に含むように変更されます(赤い破線)。 これがセルの再利用にどのように影響したかに注目してください。セルが表示されるかどうか(コンテナのフレーム内、黒い長方形)に関係なく、各行のすべてのセルが同時に停止します。 ビデオ(H.264形式)をダウンロードします 。
OLEContainerScrollView
次に、スクロールビューのコンテナについて説明します。 OLEContainerScrollViewはUIScrollViewの子孫であり、それに囲まれたビューをデッキまたはスタックのように自動的に配置します(OS XのNSStackViewと同様)。 このコンテナは、スクロールビューを特別な方法で処理しますが、スクロールビューだけでなく、すべてのタイプのビューで機能します。
ビューを追加する
ビューをコンテナに追加するには、 addSubviewToContainer:メソッドを使用する必要があります。 既存のaddSubview:/ removeFromSuperviewペアに依存するのではなく、コンテナにビューを追加および削除する新しいメソッドを作成することを余儀なくされました。ビューをコンテナに直接ではなく、プライベートcontentViewに追加したかったからです。 この手法により、スクロールインジケーターを表示するために作成されたUIScrollView自体のプライベートなサブ種からの干渉を避けることができました。後でネストされたビューをソートしてサイズを調整します。
ビューがコンテナに追加されるとすぐに、次のことが起こります。
- 新しいビューがUIScrollView (または継承)のインスタンスである場合、 subview.scrollEnabled = NOを使用して独自のスクロールが無効になります。 。 そのため、コンテナのみがスクロールジェスチャの処理に関与します。
- 添付ビューのサイズの変更に関するKVO通知をサブスクライブします。 通常のUIViewsの場合、 framesおよびboundsプロパティの変更を観察します。 スクロールビューの場合、 contentSizeプロパティを使用してコンテンツのサイズの変化を観察します。 これは、外部からサイズを変更するときに、ネストされたビューを後で適合(再配置)するために実行する必要があります。
スクロール中の位置合わせ
スクロール中、コンテナはそれに接続されているビューのフレームを次の方法で継続的に整列します。
- ネストされたすべてのビューを追加の順序で繰り返し、スタック内に配置します。次の各ビューは前のビューの後に配置されます。 すべてのビューの幅は、ビューがコンテナ1に収まるようにコンテナの幅に調整されます。 通常のUIViewでは、 フレームの高さに応じてスペースが割り当てられます。 コンテンツに合わせてスクロールビューに十分なスペースが割り当てられ、高さはcontentSize.heightから取得されます。 これは、スクロールビューに続くビューがコンテナに配置され、スクロールビューの内容全体がその前に収まるようにすることを意味します。
- コンテナのコンテンツサイズは、ネストされたすべてのビューのコンテンツサイズで構成されます。
これは、前述の単純なアプローチと一貫しています。 次に、コンテナ内のすべてのスクロールビューのフレームを 、コンテナのビューポートを満たす最小サイズになるように( 境界に従って )位置合わせする必要があります。
- これを行うには、コンテナ2のネストされたスクロールビューごとに、コンテンツ領域がコンテナの現在のスコープと交差する方法を定義し、これに従ってフレームに設定します。 これは、スクロールビューのフレームがコンテナのサイズより大きくなることはなく、現在スコープにないビューのフレームの高さが0であることを意味します。
このアルゴリズムがどのように機能するかについては、以下のビデオをご覧ください。 最初は、最初のテーブルがコンテナの可視性全体を塗りつぶします(黒の線でマークされています)-テーブルフレーム (水色の長方形)は、コンテナの境界に正確に等しくなります。 この場合、2番目のテーブルは完全に範囲外です- フレームの高さは0で、テーブルは見えません。 その結果、その中にセルを作成する必要はありません(黄色の点線)。
ユーザーがコンテナの内容をスクロールし、2番目のテーブルがスコープに入るとすぐに、テーブルフレーム (明るい黄色の長方形)の高さは、コンテナの高さと等しくなるまで、スコープの下の境界から増加し始めます。 同時に、テーブルが画面外に削除されている間、最初のテーブルのフレームはゼロに圧縮されます。 両方の表は、制限や条件なしでセルを再利用する可能性を制限なく使用できます。
OLEContainerScrollViewの動作のデモ 。 ビデオ(H.264形式)をダウンロードします 。
コード
OLEContainerScrollViewクラスインターフェイスは次のようになります。
コード
そして、ここにすべての作業を行うlayoutSubviewsメソッドの実装があります 。
@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座標系の理解を深めることができました。
脚注:
- これは、将来のバージョンでより柔軟にしたいものです。 現在、このクラスは垂直レイアウトのみをサポートしています。
- これまでは、 UIScrollViewを継承するビューに対してのみこれを行いました。 ビューがスコープ内にない場合でも、通常のUIViewのフレームの高さは変わりません。 ビューの寸法をゼロ化することで生じるレイアウトの変更(または自動レイアウトのエラー)を回避するために、そのように操作しました。 この振る舞いを変更することは将来難しくありません。
翻訳者から