カテゴリー : 開発

StreetScrollerで無限スクロール


iOSのUIScrollViewで、無限スクロールがやりたかった。フリックしてもフリックしても、どこまでもスクロールできる、ってやつ。

そのためにまず思いついたのは、contentOffsetをとっても広く取ること。でもこれだといつかは端にたどりついてしまうので、却下。次に思いついたのは、UIScrollViewのdelegateを設定して、scrollViewDidScroll:メソッドあたりで、contentOffsetが一定のところまでいったら逆の端に戻してやる、というやり方。これはやってみて、それっぽくはなったんだけど、contentOffsetを戻すところでスクロールが一瞬止まってしまう。残念、却下。

となると、後は自前のクラスを作って対応するしかないのかなー、でもUIScrollViewのあの手触り感とか減衰とかバウンスとか再現すんのは難しいなー、と思ってネットを探していたら、そのものずばりのサンプルがAppleから公開されていた。

それが、StreetScroller。ダウンロードして動かしてみると、おぉっできるぞ!確かに無限スクロールしているぞ!

ソースコードを見てみた。基本的な考え方は、contentOffsetを戻すやり方と変わらないみたいだ。ポイントは、UIScrollViewのサブクラスを作る事。そしてlayoutSuviewsをオーバーライドして、その中でcontentOffsetの処理を行うこと。これだとスクロールを止める事なくcontentOffsetを変更できるようだ。なるほどー。

これ、2011のWWDCで紹介されたネタらしいね。そっかー、やっぱりWWDC行かないとだめだね。

Cocoaのバグ?そんなものはあり得ない


アプリの完成を目指してガシガシとプログラミングをしているとき。順調に進んでるなーと思ってたら、なんてないところでバグにつまずくことがある。小さなバグなら無視して他のところやっちゃうんだけど、たまにこれを解決しないとどこも進める事ができないよ、ってのが出てくる。

関係しそうなソースコードを、ここかなあそこかなと、ためつすがめつ検討しても、原因が分からない。こんなところで時間とられている場合じゃないのに、早く他の箇所の実装にとりかからないといけないのに、と気持ちばかりが焦る。

こんなときにしばしば導きだされる結論は、「これはCocoaのバグだな!」というもの。「だってどう考えたってこのAPIを呼び出すと動きがおかしくなるんだよ」と。いやいや、それはあり得ない。自信を持って断言するけど、ほぼ確実にあり得ない。

「Cocoaのバグだな!」と言い出したとき、結論は次の3つのどれかになる。

  1. 自分のバグでした。ごめんなさい。
  2. APIの使い方を間違っていました。または、適切な使い方じゃありませんでした。
  3. ほんとにバグだったよ!

いままでの経験からいうと、98%くらいは1.ですな。残りは2.。3.は見た事ないい。特に経験のあるプログラマほどこの罠に陥りやすい。これ絶対Cocoaのバグだよ、これだからAppleは信用できないよ、MacもiPhoneも不安定だよ、とどなりちらす人のところに行って、どれどれとソースコードを見てみると、えーっとこの変数って初期化されてないんじゃない?とかいうのが、サクっと見つかったりする。

これは別にその人が不注意だ、という話じゃない。自分がバグを埋め込む可能性と、Cocoaにバグがある可能性を比べてみよう、ってことだ。たったいま自分が書き上げたに過ぎないものと、過去十数年に渡って何十万人というプログラマの目にさらされてきたものがあったとき、どっちを信用する?そりゃ後者だよね。

ソースコードにとって大切なものは、経験値だ。どんなに名前の通った人や会社が作ったものでも、新しいものは弱い。リリースしてユーザの手に触れて、バージョンアップを繰り返す事でのみ、強くなる。経験値を積んだソースコードこそが財産だ。

えっと結論としては、Cocoaを信用して自分を疑おうよということと、自分たちのソースコードも経験を積ませて強くしようよ、ってことかな。

ACAccountStoreのcompletionHandlerはサブスレッドっぽい


iOS 5から、システム側にTwitterアカウントを登録できるようになった。このアカウントは、Accountフレームワークを使って取得できる。その手順は、ACAccountStoreをインスタンス化して、requestAccessToAccountsWithType:withCompletionHandler:を呼ぶ。取得できると、指定したブロックが呼ばれるので、その中で処理を行う。

コード書いてテストしていたら、このブロックはメインスレッドではなくてサブスレッドで呼ばれるようだ。マジすか。ドキュメントにそんなこと書いてねーじゃん。従って、ここからユーザインタフェース周りを触ると、訳分からない挙動になる。すごい罠だ。

UIGestureRecognizerはけっこう鬼門


iOS 3.2以降、つまりiPad登場以降に付け加えられた、UIGestureRecognzierというクラスがある。iPhoneやiPadのタッチパネルを操作する際の、いろんなジェスチャーを認識してくれるものだ。タップ、ピンチ、回転、スワイプ、パン、長押しなんかを認識してくれる。このクラスの登場前は自前で判断ルーチンを作らなくてはいけなかったので、かなり便利だ。

最近になって、Cocoa標準のクラスでこのジェスチャーをバリバリ使うものが出てきた。これがけっこう鬼門でねぇ。苦しんでる。

たとえば、UIPageViewController。ページ表示を行ってくれる便利クラス。このクラスは、ページめくりっぽい操作を提供するために、タップ、スワイプ、パンに対するUIGestureRecognizerを使っている。このクラスだけを使っているうちは、まぁいいんだけど。

UIGestureRecognzierの便利でありやっかいな点は、ビューの階層構造を無視するところ。UIPageViewContorllerで管理するビューの上に、ボタンを置いたとする。すると、スワイプやパンといったジェスチャーは、ボタンをすり抜けてUIPageViewControllerまで届く。うむ。これは便利だ。しかし、スライダーを置いたときも、同様にすり抜けていく。これにより、スライダーを動かしながらページもめくれてしまう、という事態が発生する。これは困るぜ。

または、ページを表示しているときに、シングルタップでメニューを表示したいとする。ページの左右両端タップではページ移動、それ以外をタップしたときはメニューのトグル、っていう電子書籍アプリによくある操作を実現したい場合ね。この場合、左右両端タップはUIPageViewControllerのジェスチャーが管理する。それ以外のタップは、自前のクラスのジェスチャーで管理する。ということで、2つのジェスチャーの競合を管理しなくてはいけない。めんどくさい。

いまのところきれいな解決策が見当たらないので、ケースバイケースで対応している。これだと、新しいユーザインタフェース追加するたびに、ジェスチャーにすり抜けていかないか、競合しないか、ってことを気にしないといけない。うーん、すっきりしない。

iOS 5.1 beta登場


iOS 5.1 beta登場。相変わらずのリリース速度だ。いま忙しくて、インストールできないけど。

バッテリー問題がどうなっているかは、ドキュメントに書いてないから、知らん。ただドキュメントには、新規APIが追加されたことは書いている。ディクテーションに関するもの。たぶん、欧米圏だけの対応。

そっちの人たちにとっては、結構インパクトのあるアップデートになるかもしれない。開発の手間が増える、っていう意味でね。

iCloudその1 NSUbiquitousKeyValueStoreを使う


先日、MSM 2011でiCloudのセッションを行ってきました。参加された方、お疲れ様でした。

iCloudは考慮しなくちゃいけないことがたくさんあって、90分のセッションじゃとても足りねーぜ!および、会場からたくさんの質問があったんだけど、答えられないものがあってすげー悔しい!ということで、この場を借りてそれらをフォローしていくよん。

iCloudのプログラミングスタイルは、大きく分けて2つある。簡単なものと複雑なもの。簡単なものはNSUbiquitousKeyValueStoreを使うもの。複雑なものはUIDocumentを使うもの。まずは、簡単な方から突っ込んでいってみよう。

NSUbiquitousKeyValueStoreは、一言で言ってしまえば、クラウド対応のNSUserDefaults。setObject:forKey:や、objectForKey:といったメソッドを使って、複数デバイスでデータを同期できる。たとえば、クラウドにデータを保存するのはこんな感じ。

// Get KeyValueStore
NSUbiquitousKeyValueStore* store;
store = [NSUbiquitousKeyValueStore defaultStore];

// Set value
[store setObject:@"Steeve" forKey:@"name"];

// Synchronize
BOOL result;
result = [store synchronize];

読み出すのはこうだ。

// Get KeyValueStore
NSUbiquitousKeyValueStore* store;
store = [NSUbiquitousKeyValueStore defaultStore];

// Get value
NSString* name;
name = [store objectForKey:@"name"];

ほら、簡単だ!これだけでクラウド同期できるぜ。

だけどこれをアプリに組み込むとなると、それはそれでいろいろなトピックがあって。と言う話は、次回に続く。

iOS 5でviewWillAppear:がaddSubview:しただけで呼ばれる


iOSアプリで、起動時処理の定型に、UIViewControllerのインスタンスを作成してウインドウにaddSubview:する、というものがある。iOS 4までは、これだとUIViewControllerのviewWillAppear:とかが呼ばれないので、自前で呼ぶというのが必要だった。

// Add controller
[_window addSubview:_viewController.view];

// Call viewWillAppear: and viewDidAppear:
[_viewController viewWillAppear:NO];
[_viewController viewDidAppear:NO];

んが、iOS 5になったら、addSubview:するだけでviewWillController:とかが呼ばれるようになってしまった。ある意味ありがたい。だけど、iOS 4とiOS 5の両方で動かそうとすると、問題が発生する。iOS 5だとviewWillAppear:が連続して2回呼ばれてしまうのだ。ある意味ありがた迷惑である。

となると、実行時にOSのバージョン調べて呼び分けるしかねーな。たとえば、iOS 5から追加されたメソッドを使って、判定する事ができる。

// Add controller
[_window addSubview:_viewController.view];

// For prior to 5.0
if (![_viewController respondsToSelector:@selector(willMoveToParentViewController:)]) {
    // Call viewWillAppear: and viewDidAppear:
    [_viewController viewWillAppear:NO];
    [_viewController viewDidAppear:NO];
}

これで期待通りの動きになる。ま、viewWillAppear:を2回連続で呼ばれても問題ないようにするべきなのかもしれんが、それはそれでいろいろあるし。

iOS 5専用にしちまえば問題はなくなるんだけど、現実問題そうもいかん。Appleの前バージョン切り捨ては容赦ないな。

Xcode 4.2 + armv6で浮動小数点のバグ


うちの会社で話題になったんだけど、Xcode 4.2 + armv6デバイスの組み合わせで、浮動小数点の演算にバグがあるらしい。演算結果が正しくないときがあるらしい。

どうやら最適化に関連する不具合のようで、最適化のレベルによって発生したりしなかったりするのがややこしい。

対策としては、Apple LLVM compilerで、Other C Flagsのところに、-mno-thumbを追加するらしい。

こんな感じに設定すると、armv6だけ設定できる。

stackoverflowなんかでも話題になっている。

iOS5 Xcode4.2 floating-point byte align error?

自分の関わっているプロダクトを思い返すと、心当たり、あるなぁ。チェックしないと。

UIPageViewControllerとジェスチャー


iOS 5から追加されたUIPageViewControllerクラス。こいつを使うとページめくりができる。ひとつのページがひとつのビューコントローラになって、それを連ねる事で本みたいなものができるという仕組み。ビューコントローラベースなのが、ありがたい。将来的には、ページめくり以外にもいろいろなエフェクトが追加されて、Keynoteのスライドエフェクトみたいなものができるかも。

UIPageViewControllerは、デフォルトでタップやパンといったジェスチャーに対応している。画面の両端をシングルタップするとページめくり。パンすると追随するページめくり、といった感じ。これは便利。でもアプリに組み込んでいくと、独自のジェスチャーも追加したくなる。シングルタップでツールバー表示とかね。

そういったときは、UIPageViewControllerのgestureRecognizersプロパティを使う。これを経由して、UIPageViewControllerが管理しているジェスチャーにアクセスできる。ということは、こいつらのデリゲートとして自分のクラスを設定して、そこでgestureRecognizerShouldeBegin:といったようなUIGestureRecognizerDelegateのメソッドを実装してやる。そうすれば制御できる。

たとえばシングルタップ。ページ両端だったらページめくりで、それ以外だったらツールバー表示にしたい。となると、gestureRecognizerShouldeBegin:で受けて、タッチしたポジションを取得して、それが両端だったらYESを返してUIPageViewControllerに処理を任せる。そうでなければNOを返して、自前の処理にする。両端をタップしたかどうか?ってのは自分で判断しないといけないような気がする。

または新規にUIGestureRecognizerのインスタンスを作って、requireGestureRecognizerToFail:でどうにかするっていう手もあるかもしれない。でもこれ、うまくいくときといかないときがある感じ。どんなジェスチャーを追加したいかによる。

正直、UIPageViewControllerのAPIはまだこなれていない感じがするね。これ、新規に追加されたクラスではよくある。基本的な機能はきちんと実装されている。でもカスタマイズのためのものは、まだ粗削。ジェスチャーの制御に対してきめ細かなAPIを提供するのではなく、gestureRecognizersをポンっと投げて、これさえあれば後はそっちでどうにでもできるでしょ、って感じ。必要ではあるが充分ではない。どうにかしてそれを使って苦労して実装すると、次のバージョンですっごい便利なAPIを追加してきたりする。ムキーッてなる。

iOS 5 SDKでドキュメントが見れない


iOS 5の正式版がリリースされたということで、iOS 5 SDKの話も解禁ということでいいのかな?いいよね?Mac App StoreにもXcode 4.2が登場したし。

しかし、XcodeはDev Centerからダウンロードするものと、Mac App Storeからインストールするものの2系統を管理しなくてはいけないので、めんどくさい。

では、早速文句を。iOS 5 SDKを新しいマシンにインストールした。通常の動作はできる。だけど、オーガナイザからドキュメントを見ようとしたら、Apple IDを入力してログインしろ、と言ってくる。それじゃあ、といってパスワードを打ち込もうとすると、「index.htmlがロックされていて編集できない」とか言われる。

そんなアホな。しょうがないからUnlockをクリックしてみると、Xcodeがクラッシュする。そんなアホな!

ということで、ドキュメントがまったく見れません。この問題、iOS 5 GMから発生したような気がする。すでにXcodeが入っているマシンで上書きした場合、この問題はなかった。どうにかしてくれ。