blog

iOS開発におけるスクロール・ビューの包括的な理解

標準的な UIView との違いがそれほど大きくないこと、スクロールビューにはさらにいくつかのメソッドがあることを信じるのは難しいかもしれません。UIViewがどのように動作するかを理解するには、UI...

Jul 16, 2025 · 8 min. read
シェア

UIScrollView が標準的な UIView とそれほど変わらないということを信じるのは難しいかもしれません。スクロールビューにはさらにいくつかのメソッドがありますが、これらのメソッドは UIView のプロパティの表面をなぞったものに過ぎません。UIScrollView がどのように動作するかを理解するには、UIView を理解する必要があり、特にビューのレンダリングプロセスの 2 つのステップを理解する必要があります。

ラスタライズと組み合わせ

レンダリングプロセスの***部分は、よく知られているラスタライズで、これは単に描画命令のセットを生成し、イメージを生成することを意味します。例えば、丸みを帯びた長方形、絵付きのUIButton、中央揃えのキャプションを描画します。これらの絵は画面には描画されず、次のステップまで独自のビューに保持されます。

各ビューがそれぞれラスタライズされたイメージを生成すると、これらのイメージが次々に描画され、画面サイズのイメージが生成されます。ビューのイメージは親のイメージの上に合成されます。合成されたイメージは、親ビューの親イメージの上に合成されます。最終的なビュー階層の一番上にあるのがウィンドウで、その合成されたイメージがあなたの目に見えるものです。

インターフェイスをレイアウトするとき、ビューのフレームを扱う必要があります。 ビューのフレームとバウンズのサイズは常に同じですが、その原点は異なる場合があります。この2つの仕組みを理解することが、UIScrollViewを理解する鍵です。

ラスタライズステップ中、ビューはこれから起こる結合ステップを気にしません。つまり、ビューはフレーム(ビューを配置するために使用されるイメージ)やビュー階層内の位置を気にしません。この時点でビューが気にする唯一のことは、各ビューのdrawRect:メソッドで行われる自身のコンテンツの描画です。

drawRect:メソッドが呼ばれる前に、ビューがコンテンツを描画するための空白のイメージが作成されます。このイメージの座標系はビューの境界です。したがって、打ち切りイメージの左上に何かを描くときは、常にboundsの原点({x:0,y:0})に描画することになります。イメージの右下隅に何かを描くときは、{x:width, y:height}で描画します。ビューの境界を越えて描画した場合、その余分は変形されたイメージの一部ではなく、破棄されます。

結合ステップでは、各ビューは、親ビューのラスタライズされたイメージの上に独自のラスタライズされたイメージを結合します。ビューのフレームは、親ビューのどこに描画されるかを決定し、フレームの原点は、親ビューのラスタライズされたイメージの左上隅に対する、ビューのラスタライズされたイメージの左上隅のオフセットを示します。したがって、{x:20,y:15}の原点を持つフレームは、親ビューの左から20ポイント、親ビューの上から15ポイントのイメージを描画します。ビューのフレームとバウンズの矩形は常に同じサイズであるため、ラスタライズされたイメージは結合時にピクセル整列されます。このため、ラスタライズされたイメージが引き伸ばされたり縮小されたりすることはありません。

ビューとその親ビューの組み合わせ操作についてのみ説明しました。この2つのビューが結合されると、結合結果のイメージは親ビューの親ビューと結合されます。雪だるま式です。

イメージを組み合わせる際の公式を考えてみましょう。ビューのイメージの左上隅は、そのフレームの原点に基づいてオフセットされ、親ビューのイメージに描画されます:

CompositedPosition.x = View.frame.origin.x - Superview.bounds.origin.x;  
   
CompositedPosition.y = View.frame.origin.y - Superview.bounds.origin.y;  

数種類のフレームに目を通すことができます:

この方法は理にかなっています。ボタンのframe.originを変更すると、紫の親ビューからの相対位置が変わります。ボタンの一部がすでに紫の親の境界の外に出るまでボタンを移動すると、ラスタライズされたイメージが切り捨てられるときと同じように、その部分が切り捨てられることに注意してください。しかし、技術的には、iOSのコンポジションの処理方法のため、子ビューを親の境界の外にレンダリングすることはできますが、ラスタライズ中にビューの境界の外に描画することはできません。

スクロールビューのコンテンツオフセット

もちろん、スクロールビューには多くの代表的なビューがあります。このパン機能を実装するためには、ユーザーの指の動きに合わせて、各ビューのフレームを常に変更する必要があります。ビューのラスタライズイメージを親ビューのどこかに結合することを提案する場合は、この式を覚えておいてください:

CompositedPosition.x = View.frame.origin.x - Superview.bounds.origin.x;  
   
CompositedPosition.y = View.frame.origin.y - Superview.bounds.origin.y;  

Superview.bounds.originの値を減らします。しかし、0でない場合はどうすればよいでしょうか?前の凡例と同じフレームを使いますが、紫色のビュー境界の原点を{-30,-30}に変更します。下のイメージを取得します:

さて、巧妙なのは、この紫色のビューの境界を変更することで、個々の子ビューが移動することです。実は、これがスクロールビューの仕組みなのです。contentOffsetプロパティを設定すると、scroll view.boundsの原点が変更されます。コードは次のようになります:

- (void)setContentOffset:(CGPoint)offset  
{  
    CGRect bounds = [self bounds];  
    bounds.origin = offset;  
    [self setBounds:bounds];  
}  

注:前の図では、境界の原点を十分に変更すると、紫色のビューとボタンの組み合わせであるイメージの範囲外にボタンが移動します。また、スクロールビューを十分に動かすと、ビューが消えます!

世界の窓:コンテンツサイズ

スクロールビューのコンテンツサイズは、その境界について何も変更しないので、スクロールビューがそれ自身のサブビューを組み合わせる方法には影響しません。スクロールビューのデフォルトのコンテンツサイズは{w:0,h:0}です。スクロール可能な領域がないので、ユーザーはスクロールできませんが、スクロールビューはその境界内にあるすべての子ビューを表示します。

コンテンツのサイズが境界よりも大きく設定されている場合、ユーザーはビューをスクロールすることができます。スクロールビューの境界は、スクロール可能な領域上のウィンドウと考えることができます:

content offset が {x:0,y:0} の場合、可視ウィンドウの左上隅がスクロール可能領域の左上隅になります。これは content offset の最小値でもあり、ユーザーはスクロール可能領域の左や上に移動できなくなります。何もない場合はスクロールしないでください!

コンテンツオフセットの***値は、コンテンツサイズとスクロールビューサイズの差です。これは理にかなっています: 左上隅から右下隅までスクロールし、ユーザーが停止すると、スクロール領域の右下端は、スクロールビューの境界の右下端と同じ高さになります。コンテンツオフセットの***値は、次のように書くことができます:

contentOffset.x = contentSize.width - bounds.size.width;  
contentOffset.y = contentSize.height - bounds.size.height;  

コンテンツ・インセットでウィンドウのサイズをわずかに変更

contentInset プロパティは、スクロール可能な領域をスクロールアウトできるように、コンテンツオフセットの *** と最小値を変更します。このプロパティはUIEdgeInsets型で、{top, left, bottom, right}の4つの値を持ちます。インセットを導入すると、コンテンツ・オフセットの範囲が変更されます。例えば、content inset top の値を 10 に設定すると、content offset の y の値が 10 になります。 これにより、スクロール可能な領域の周囲にパディングが導入されます。

これは一見すると何の役にも立たないように見えます。実際、なぜコンテンツサイズを大きくしないのでしょうか?他に選択肢がない場合を除き、スクロールビューのコンテンツサイズを変更することは避ける必要があります。テーブルビューのスクロール可能な領域は、各セルに合わせて慎重に計算されています。テーブルビューのセルの *** または *** の境界を越えてスクロールすると、テーブルビューはコンテンツのオフセットを戻してリセットします。

UIRefreshControlを使用してプルリフレッシュを実装したい場合はどうなりますか?UIRefreshControlをテーブルビューのスクロール可能な領域内に配置することはできません。したがって、スクロール可能な領域の上にリフレッシュコントロールを配置する必要があります。これにより、コンテンツのオフセットが、リフレッシュ・コントロールではなく、最初に***行にポップバックするようになります。

しかし、テーブルビューがコンテンツインセットを設定するときに、十分に大きな距離をスクロールしてプルトゥリフレッシュ機構を初期化すると、コンテンツオフセットがリフレッシュコントロールをスクロール可能な領域に戻すことができます。リフレッシュアクションが初期化されるとき、コンテンツインセットはすでに修正されているので、コンテンツオフセットの最小値は完全なリフレッシュコントロールを含みます。ここでは、コンテンツ・サイズのために数学的な計算をする必要はありません。

自分のコードでコンテンツ・インセットを使うには?キーボードが画面上にあるときに、これを使う良い方法があります:画面にぴったりと収まるユーザー・インターフェースを設定したい場合です。キーボードが画面上にあると、数百ピクセルのスペースが失われ、キーボードより下はすべて遮られてしまいます。

今、スクロールビューの境界は変化せず、コンテンツのサイズも変化しません。コンテンツオフセットの***値は、コンテンツサイズとバウンズサイズと同じではありません。これらが等しい場合、コンテンツオフセットの***値は{x:0,y:0}になります。

さて、トリックですが、インターフェイスをスクロール・ビューに入れます。スクロール・ビューのコンテンツのサイズは、スクロール・ビューの境界と同じサイズのままです。キーボードが画面に表示されたら、キーボードの高さと同じ高さのコンテンツが挿入されます。

これにより、スクロール領域の外側の領域をコンテンツオフセットの***値で表示することができます。表示可能領域の上部はスクロールビューの境界の外側にあるため、インターセプトされます。

これでスクロールビューの内部構造がある程度理解できたと思いますが、ズームに興味がありますか?今日はその話はしませんが、面白いヒントがあります。viewForZoomingInScrollView: メソッドが返すビューの transform プロパティをチェックしてみてください。繰り返しますが、スクロールビューはUIViewに既に存在するプロパティを巧みに利用しているだけです。

出典:answer_huangのブログ [].

Read next

ミッコ・ハプニン:オープンソースソフトウェアは世界をより安全にする

先週エジンバラで開催されたLinuxCon and CloudOpen Europeカンファレンスの基調講演で、セキュリティ専門家のMikko Hapnin氏は、"オープンソースソフトウェアは、罪のない住民の世界的な監視に対抗する手段として使うことができる "と述べました。

Jul 15, 2025 · 2 min read