The Issue About Different Behavior Of UIScrollView Over Device Generations

Irodoriの1.1版は嬉しいことにUSのitunes storeのNew and noteworthyに載せてもらうことができた。1.1版の特徴は、要望が多かった既にPhotoAlbumに保存してある画像からの解析機能なんだけど、これを実装するときに、わたしとしては初めて同じバージョンのiPhone OSのデバイスの機種間での挙動の違いを体験したので、メモとして書いておこうと思う。

iPhone OSは基本的なアーキテクチャは一貫している印象があって、iPhoneかiPod touchか、OpenGLES2対応かそうでないか、ARM6か7ぐらいかの違いしか無くて、しかも後方互換性があるので、一番ベーシックなところで作っておけばどれでも特に対策をする必要もなく問題なく動くというものだと思っていた。

特に一番基本的なApplication FrameworkであるCocoa touchなんかは上位レイヤーにいるのでどのデバイス用でも同じものを使ってると(そうしない理由があまり浮かばないので)思っていたのだ。

もちろんシミュレータは今までにも違う挙動を示すことがあった。しかし、iPhoneで開発をした方なら分かると思うのだけど、まあ、シミュレータだしな、というところはあった。

さて、問題になったのはUIScrollViewだ。このクラスは表示サイズより大きいものや、複数あるビューを選ぶようなとき、スクロールや拡大縮小機能を提供してくれる。UITableViewはUIScrollViewの派生クラスだ。こういう前置きも必要無いぐらい基本的なクラスだと思う。

このクラスには、中においたコンテンツビューの端に余白を空けるように出来る機能がある。それがcontentInsetで、それぞれに指定したピクセル分端に余白が出来る。コーディングとしては同じ事がUIScrollViewを小さく作っておいてclipsToBoundsをNOにしておくことでも実現できる(clipsToBoundsをNOにするとコンテンツビューがそのサイズでクリップされなくなるので)。

contentInsetを使った方が便利な点はスケールした場合も座標の計算を行わなくても良いという点。コンテンツビューのスケールに依らず指定ピクセル分の余白を空けてくれる。もちろん、スケールに合わせてコンテンツビューサイズを変更すれば同じ事は出来る。

Irodoriでは指定された画像から解析用の画像を切り出すという処理の切り出し部分を指定するところにUIScrollViewを使用した。初期値は縦または横の短い方がちょうど切り出す領域の大きさになるように拡大または縮小している。切り出し領域は画面端よりオフセットされた位置にあり、その上下左右をcontentInsetで指定している。

このスケールされた状態で領域と同じ大きさの場合スクロールはされない。contentInsetの分とスケールされた辺の長さを足したものが丁度UIScrollViewの大きさと同じになるためだ。正方形の場合は固定された状態に(初期的に)なる。

これが期待されるべき動作で、仕様通りでもある。さらにいうとiPhone 3Gの3.1.3ではその通りに動作した。しかしシミュレータでは動作が異なっていた。拡大縮小およびスクロールを繰り返した後、または最初から。そのタイミングは決まったものではなく、突然「外れる」のだ。まさしく、外れると表現するのがいちばんその状態にしっくりと来る。

シミュレータだけの問題なら放っておいても問題ないのだけど、iPod touch 1st gen.で試してみたら、なんとシミュレータと同じ挙動になったのだ(つまり、「外れた」)。バージョンも確かめてみたのだけど3.1.3だった。

つまりは同じバージョンのOS間でAPIの挙動が異なったというわけだ。

こういう時はたいてい非正規な初期化など、どこかミスをしてることが多い。しかし、色々と試してみたけれども、片方はちゃんと動作し続け、もう片方は「外れた」ままだった。

そのものずばりな解決方法があるのかもしれないが、わたしはとりあえず次のように対処しておいた。

  • UIScorollViewDelegate protocolのscrollViewDidEndDragging:willDecelerate:のdecelerateがNOのとき
  • またはscrollViewDidEndDecelerating:が呼ばれたとき

このときにcontentOffsetの値の値が範囲を超えていた場合に範囲内に戻すようにした。contentOffsetプロパティに直接設定しても良いし(setContentOffset:)、setContentOffset:animated:を使用し、戻るところをアニメーションさせることも出来る。アニメーションさせた場合も終了時にDelegateのメソッドscrollViewDidScroll:が呼ばれるので、そこでスクロール中は禁止していた処理を許可状態などにする。

もっともこれが確認できるのは一部の機種に限られるのだけど。

Irodoriの場合、領域切り出し部分なので、仕様通りの動作を期待すると「外れていた」場合、領域外までふくむことになり、最悪の場合BAD_ACCESSで落ちてしまう(そして落ちた)。UIが絡むところはやはりその値がちゃんと想定した範囲内かどうかはチェックした方が良いな、と、再認識したのだった。