LoginSignup
167

More than 5 years have passed since last update.

[Objective-C] iOS Scroll Viewプログラミングガイドを読んだメモ

Posted at

Appleのドキュメントもだいぶ読み進めてきました。
今回は「[PDF] iOS Scroll Viewプログラミングガイド」を読んだメモです。

実はまだ案件で使ったことがないUIScrollView
経験の浅さを痛感します。

ScrollViewで大事なプロパティたち

プロパティ 意味
contentSize ScrollViewが内包するコンテンツのサイズ。スクロール領域
contentInset いわゆる余白。contentSizecontentInsetを足したものが最終的なScrollViewのサイズになる
scrollIndicatorInsets スクロール時に表示されるインジケータのinset。contentInsetを設定したらこちらも設定しないとスクロールバーが変なところに表示されるので注意

UIScrollViewDelegate

スクロールに関するイベントに関してはデリゲートメソッドで受け取ります。
デリゲートメソッドには以下のものが用意されています。

デリゲートメソッド 説明
- (void)scrollViewDidScroll:(UIScrollView *)scrollView UIScrollViewが スクロールしている間 呼び出される。
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView アニメーション完了後に呼び出される。(setContentOffset:animated:メソッドのanimatedNOの場合は呼び出されません。
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView ドラッグ開始時に呼ばれる
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate ドラッグ後、指がデバイスが離れた時に呼ばれる。慣性が効いて動く場合はdecelerateYESになる
- (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の値

スクロールの開始と完了の追跡

開始と完了もデリゲートメソッドで追跡することができます。
※デリゲートメソッド一覧に記載しています。


デリゲートメッセージシーケンスの全体

デリゲートシーケンスはユーザの画面タッチから開始されます。
シーケンスは以下の状態をたどります。

  1. trackingプロパティがYES
  2. ユーザの指がデバイスに触れた状態で静止していて、コンテンツビューがタッチイベントに応答するものの場合、シーケンスは完了しています
  3. その後、ユーザがデバイスに触れたまま指を動かせばシーケンスが継続します
  4. スクロールを開始すると、進行中のタッチ操作をすべて取り消すよう試みます。(筆者注:例えばボタンなどをタッチし反応している状態で、指を動かすとそのアクションが取り消されるのをイメージすると分かりやすい)
  5. ScrollViewのdraggingプロパティがYESに設定される
  6. scrollViewWillBeginDragging:メッセージが送信される
  7. (ユーザのドラッグ中)scrollViewDidScroll:メッセージがデリゲートに送り続けられる
  8. ユーザがフリックジェスチャを行うと、trackingプロパティはNOに設定される
  9. その後、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を渡してインスタンス生成を省略する方法ですね)
モバイルはメモリが潤沢にあるとは言えないので、できるだけ最適化しよう、というようなことが書いてあります。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
167