Appleのドキュメントもだいぶ読み進めてきました。
今回は「[PDF] iOS Scroll Viewプログラミングガイド」を読んだメモです。
実はまだ案件で使ったことがないUIScrollView
。
経験の浅さを痛感します。
##ScrollViewで大事なプロパティたち
プロパティ | 意味 |
---|---|
contentSize | ScrollViewが内包するコンテンツのサイズ。スクロール領域 |
contentInset | いわゆる余白。contentSize にcontentInset を足したものが最終的なScrollViewのサイズになる |
scrollIndicatorInsets | スクロール時に表示されるインジケータのinset。contentInset を設定したらこちらも設定しないとスクロールバーが変なところに表示されるので注意 |
##UIScrollViewDelegate
スクロールに関するイベントに関してはデリゲートメソッドで受け取ります。
デリゲートメソッドには以下のものが用意されています。
デリゲートメソッド | 説明 |
---|---|
- (void)scrollViewDidScroll:(UIScrollView *)scrollView | UIScrollViewが スクロールしている間 呼び出される。 |
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView | アニメーション完了後に呼び出される。(setContentOffset:animated: メソッドのanimated がNO の場合は呼び出されません。 |
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView | ドラッグ開始時に呼ばれる |
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate | ドラッグ後、指がデバイスが離れた時に呼ばれる。慣性が効いて動く場合はdecelerate がYES になる |
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView | ドラッグ後、慣性が効いて動いたあとに止まった場合に呼ばれる。(慣性なしでドラッグを終えた場合は呼ばれない) |
##プログラムからスクロールさせる
プログラムからスクロールさせるには、setContentOffset:animated:
メソッドを利用します。
// CGPointの位置が左上角になるようにスクロールする
[aScrollView setContentOffset:CGPointMake(50, 50) animated:YES];
// 以下はどちらも意味は同じ、アニメーションなしでスクロール
[aScrollView setContentOffset:CGPoint(50, 50) animated:NO];
// -------------------------- or --------------------------
aScrollView.contentOffset = CGPoint(50, 50);
##矩形範囲の表示
[aView scrollRectToVisible:(CGRect) animated:(BOOL)]
矩形が見える位置までスクロールします。その際、追跡プロパティとドラッグプロパティはどちらもNO
になります。
##最上部へのスクロール
ステータスバーをタップしたときに一番上までスクロールする機能です。
この動作もUIScrollView
の処理です。
デリゲートメソッドをオーバーライドすることによって、この機能をオフにすることもできます。
例えばこれを利用することで、画面内に複数のScrollViewがある場合に、ステータスバータップ時にどのScrollViewをスクロールさせるかを制御することが可能となります。
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView;
また、この動作によってアニメーションが完了したときに以下のデリゲートメソッドが呼ばれます。
- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView;
##スクロール中の状態追跡
スクロールにはユーザ操作のものもあれば、プログラムによる操作もあります。
しかしデリゲートメソッドはどれも同様にメッセージを受信するので、状態を把握する必要が出てきます。
そのときに使えるのが以下の状態プロパティです。
状態プロパティ | 説明 |
---|---|
tracking | ユーザの指がデバイスに触れている場合にYES (つまりリアルタイムな状態) |
dragging | ユーザの指がデバイスに触れていた場合にYES (フリップなど、ユーザの指が離れても、ユーザ操作によるものと判断できる状態) |
decelerating | フリックジェスチャ後、またはScrollViewのフレームを超えてドラッグし、バウンスしてScrollViewが減速している場合にYES
|
zooming | zoomScaleプロパティを変更するためにScrollViewがピンチジェスチャを追跡している場合にYES
|
contentOffset | ScrollViewの境界の左上角を定義するCGPointの値 |
##スクロールの開始と完了の追跡
開始と完了もデリゲートメソッドで追跡することができます。
※デリゲートメソッド一覧に記載しています。
##デリゲートメッセージシーケンスの全体
デリゲートシーケンスはユーザの画面タッチから開始されます。
シーケンスは以下の状態をたどります。
- trackingプロパティが
YES
に - ユーザの指がデバイスに触れた状態で静止していて、コンテンツビューがタッチイベントに応答するものの場合、シーケンスは完了しています
- その後、ユーザがデバイスに触れたまま指を動かせばシーケンスが継続します
- スクロールを開始すると、進行中のタッチ操作をすべて取り消すよう試みます。(筆者注:例えばボタンなどをタッチし反応している状態で、指を動かすとそのアクションが取り消されるのをイメージすると分かりやすい)
- ScrollViewの
dragging
プロパティがYES
に設定される -
scrollViewWillBeginDragging:
メッセージが送信される - (ユーザのドラッグ中)
scrollViewDidScroll:
メッセージがデリゲートに送り続けられる - ユーザがフリックジェスチャを行うと、
tracking
プロパティはNO
に設定される - その後、
scrollViewDidEndDragging:willDecelerate:
メッセージがデリゲートに送られる
###減速スピードの制御
フリックジェスチャなどで慣性で動いている状態のScrollViewの減速については、以下のプロパティで制御することができます。
aScrollView.decelerationRate = UIScrollViewDecelerationRateNormal;
// -------------------- or -----------------------
aScrollView.decelerationRate = UIScrollViewDecelerationRateFast;
##ピンチジェスチャを使った基本的なズーム
UIScrollViewにはピンチジェスチャを簡単に扱う仕組みが実装されています。
ピンチジェスチャを使うには以下のデリゲートメソッドを実装する必要があります。
また同時に
-
minimumZoomScale
プロパティ -
maximumZoomScale
プロパティ
のどちらか、あるいは両方を設定する必要があります。(設定しないとデリゲートメソッドが呼ばれません)
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
// ズーム対象のビュー
return self.aView;
}
###プログラミングによるズーム
上記はユーザ操作によるズームでした。
ScrollViewは、ダブルタップなどの操作によりプログラムからズームを実行する実装を用意しています。
setZoomScale:animated:
zoomToRect:animated:
というふたつのメソッドです。
どちらのメソッドもズームを操作するメソッドです。
ズームは、ズーム対象の「中心位置」の周りをズームします。
そのため、意図した箇所をズームする場合は位置と拡大率の調整を行う必要があります。
###デリゲートへのズーム完了の通知
ズーム操作や処理が完了したとき、scrollViewDidEndZooming:withView:atScale:
メソッドが呼ばれます。
このメソッドは、ScrollViewインスタンス、スクロールされたScrollViewのサブビュー、ズーム完了時の拡大縮小率が引数として渡されます。
###ズームでハマった話
Appleのドキュメントからははずれますが、実際にドキュメントを読みつつ作っていてハマった点を書いておきます。
(おそらく、しっかりと理由があると思うんですが原因が分かっていないので書いていることは間違っているかもしれません)
####ズーム後、contentSize
が変更される
動作の仕組みを理解するために色々なViewを、ScrollViewに適当にaddSubview:
してたんですが、どうもズームをすると表示がおかしくなる場合が。
ズームすると、どうやらcontentSize
が変更されている模様。理由分からず。
ちなみにScrollViewは以下のような感じになってました。
- UIImageViewを3つ並列に追加
- UIViewを最後に追加
- 最初に
addSubview:
したUIImageViewがScrollView内で一番サイズが大きい - 上のimageのサイズを
contentSize
に設定
という状況。
で、viewForZoomingInScrollView:
メソッドで一番サイズが大きいUIImageViewを返している場合は想定通り動くものの、それ以外を返すとcontentSize
の値が想定通りになっていない。
よくよく見てみると、ズーム対象のコンテンツのサイズがcontentSize
になるように変更されていました。
なのでズーム完了時に、改めてscrollViewのcontentSize
を一番サイズの大きいUIImageViewのimageのサイズにするようにしたところ、想定通りの動きとなりました。
(多分、ズーム対象のViewのサイズにする、みたいな動作がデフォルトなのだと思います)
微妙に想像していた動作と違うので要注意です。
##タップによるズーム
ピンチジェスチャに関しては基本的な実装をフレームワーク側で行っていますが、タップによるズーム(ダブルタップでズーム、など)を実装するにはアプリケーション側での実装が必要です。
タップによるズームを適切に実装すれば、ユーザビリティを向上させることもできると思います。(2本指でダブルタップするとズームアウトとか)
###実装方法
アプリケーション側での実装が必要ですが、ScrollViewのサブクラス化は必要なく、viewForZoomingScrollView:
デリゲートメソッドで返すクラス側でタッチ処理を実装します。
※コードサンプルも書いていて長くなったので別記事にしました。
##ページングモードを使用したスクロール
UIScrollViewは、iOSでよく見られるページングタイプのスクロールもサポートしています。
(ある決まった幅(1画面分)のコンテンツをスクロールするようなやつ。App Storeのスクリーンショットとかの感じです)
###ページングモードの設定
ページングモードをサポートするには、ScrollViewのpagingEnabled
プロパティにYES
を設定する必要があります。
contentSize
は表示したいコンテンツがちょうど収まるサイズにしておくといいでしょう。
また、通常はインジケータ(スクロールバー)は必要なくなるので非表示にしておいたほうがより自然になります。
UIScrollView *aScrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
aScrollView.pagingEnabled = YES;
// スクロールインジケータを非表示に
aScrollView.showsHorizontalScrollIndicator = NO;
aScrollView.showsVerticalScrollIndicator = NO;
###ページングモードのScroll Viewのサブビューの設定
Appleのプログラミングガイドでは、サブビューの設定についても言及されています。
色々書いてありますが、要は小さいコンテンツはひとつのサブビュー、大きいコンテンツやページ数の多いコンテンツはページ単位でビューを分け、表示すべきビューと隣接するビューのみ生成し、それ以外は位置を動的に判断して生成しよう、というものです。
一言で言えば、メモリ使用量を減らそう、ってことですね。
TableViewのセルも似たような仕組みで動作しています。(Identifierを渡してインスタンス生成を省略する方法ですね)
モバイルはメモリが潤沢にあるとは言えないので、できるだけ最適化しよう、というようなことが書いてあります。