開発 | HMDT Blog | ページ 3

カテゴリー : 開発

電子書籍でページめくりが最も高速な訳


電子書籍、特に現状の電子書籍に対して批判的な文脈では、ページめくりアニメーションが何かと槍玉に上げられる。いわく、意味なく紙の模倣をしている、無駄に遅い、スクロールの方が電子的だ、など。最近だと、Dailyの廃刊にあわせて取り上げられたりとかね。

ページめくりvsスクロールは、2つの対立軸がある。1つめは、ページネーションを行うか否かだ。紙の本は、その昔にあった巻物ではページネーションの概念はなかったものの、ページという矩形領域に文字を収め、それを重ねる事で長文を表した。電子であれば、スクロールを使う事でページネーションは必要なくなる。Webページがそうだよね。

テキストをスクロールのみで読ませる事が適切か否かは、個人的な経験でいえば、その長さに依存すると思う。ニュースとかブログ記事みたいな、スクリーン1画面から3画面程度ならば、スクロールでいいでしょう。でも、夏目漱石の「こころ」のような長文テキストをスクロールだけで読ませるのは、やっぱり厳しい。これは、一画面に表示される文字数も関係すると思う。一度に目にする文字数が多すぎると、やはり疲れる。

ページネーションするということは、ページサイズという制限が生まれる事につながるので、レイアウトが発展する事になる。たとえば、雑誌だ。特に日本の。1ページという限られた領域に、情報を可読性と美しさに配慮しながらレイアウトする。その上で、ボリュームも突っ込む。これはページネーションという必然から生じた価値で、捨てるのはもったいない。あと、マンガみたいにページがあることが大前提になっている読み物もある。

で、ページネーションされた書籍をどうやって電子で読ませるかという事でも、スクロールとページめくりの対立がある。この場合、スクロールはページ区切りのスクロールってことになる。このとき、ページめくりに対しては、遅いとかアナクロニズムとかいう批判が寄せられる。でも、そんなことはない。実は、ページめくりというユーザインタフェースには、圧倒的な利点があるのだ、というのが今日の本題。

利点というのは、スピードだ。ページめくりというインタフェースは、その仕組み上、スクロールと比較して高速にページ間を移動する事ができる。うん?アニメーションが速いってこと?いや、そうじゃない。連続してめくりをするときに、前のページのめくりが完了する前に、次のページめくりを行うんだ。ちょうど、紙の本をパラパラとめくる感じだ。手前味噌で申し訳ないけど、HMDT BOOKSでめくるとこんな感じになる。

これができると、ページの移動が高速で直感的になる。たとえば5ページ進みたいな、と思えばトトトトトンッと5回連続でタップすればいい。スクロールだとこうはいかない。トーン、トーン、トーン、、、って感じで、ページスクロールが完了してからじゃないと次にいけない。さらに、スクロールはページ全体が動くから、視認性もよくない。ページめくりは、ページの位置自体は固定されているから、めくった隙間からチラリとのぞくことで、かなり確認できる。40ページとか50ページくらいなら、あっという間にめくれる。

ということで、ページめくりアニメーションはただのアナクロニズムじゃなくて、理にかなったユーザインタフェースなんだよ、ってお話でした。もしどっかのデバイスでこの種の連続ページめくりができないのであれば、それはハードとソフトがプアなだけで、ユーザインタフェースの問題とは関係ないね。

CATiledLayerやめました


CATiledLayerっていうレイヤーのクラスがある。主に巨大な画像、スクリーンの縦横4倍以上あるもの、を表示するときに使われる。画像をタイル状に分割して、必要なところだけ表示する事で、メモリの消費量を抑えているのだ。Appleのサンプルで積極的に登場している事もあって、よく使われているクラスだと思う。

でも、これ。使い込むと結構嫌なところが多くて。いちばん嫌なのは、タイル状の分割画像の読み込みをバックグラウンドのスレッドで行い、その生成と破棄の制御が全くできないこと。スレッドを立てるおかげでユーザインタフェースの操作は非常に滑らかなんだけど、いつ作られるか分からないし、作られたスレッドを中断させる事もできない。結果、主となるビューを頻繁に破棄するタイプのアプリだと(電子書籍とか)、結構な頻度でクラッシュが発生する。

どうにか折り合いをつけながらやってきたんだけど、iOS 6になったときかなり致命的に問題になったので、一念発起して使うのをやめる事にした。順次、独自のビュークラスへの置き換えを行っている。

独自のビュークラスっていってもたいした事やる訳じゃなくて、CATiledLayerがやっているのと同じ事を、自前でやるだけだ。画像分割したビュー(tiled viewって呼んでいる)を、画面の表示部分だけaddSubview:する。スクロールしたり、拡大縮小したりしたら、随時ビューの追加と削除を行う。

これを、メインスレッドだけで行っている。もちろん、サブスレッド使った方が動きが滑らかになるんだけど、現状のiOSデバイスの処理能力を持ってすれば、メインスレッドだけで全然いける。プログラミングモデルの単純さと安定性を考えれば、そっちの方が断然有利だ。ただ、表示する画像によってはやっぱりひっかかりが生じるので、そこはテクニックでカバーする。

基本的には力技で、それほど技巧的なものではない。ちょっと気をつけたいのは、tiled viewのtransformか。tiled viewはスクロールビューにaddSubview:するんだけど、普通にやるとスクロールビューのtransformにひきずられて、拡大したときにボケボケになってしまう。そこで、拡大率の逆数(2倍表示なら1/2、4倍表示なら1/4)のtransformを設定してやる。これでちゃんと拡大したときに高精度で表示されるようになる。

UIViewとCALayerとOpenGLテクスチャと


ずーっと長年疑問に思っていた事が、ようやく今日理解のとっかかりを得た。

話の始まりは、UIViewの内容を画像として取り出したい、というところから。普通に説明されるのは、UIImageGraphicsContextを作って、そこにCALayer取得してdrawInContext:しろ、ってことだよね。ざっと書くなら、

    UIGraphicsBeginImageContext(view.frame.size);
    [view.layer drawInContext:UIGraphicsGetCurrentContext()];
    image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

こんな感じ。確かにこれで取得できるけど、なんか嫌じゃない?どこがっていうと、drawInContext:を呼び出して、わざわざもう一回描画させているところ。もうすでに画面に描かれているんだから、それを直接取得する事はできないのか?

iOSでは、UIViewは常に背後にCALayerがいて、表示する画像とかエフェクトとかレンダーツリーとかはそっちが管理している。Layer-backed Viewってやつね。CALayerはGPUを使ってレンダリングするので、何かしらOpenGLのテクスチャと関連するものを持っているはず。

予測はしていたんだけど、それがどこにどんな形であるのかが、ずーっと疑問だった。何度も調べては挫折していた。

それが、こりずにまた調べていたんだけど、ようやくそれっぽいのを見つけた。drawRect:を実装したUIViewを画面に貼付けて、画面に描画された後で、そのビューのCALayerのcontentsを調べる。そうすると、自分では何も設定していないのに、なんか入っているじゃん!

ログ吐かせてみると、CABackingStoreというオブジェクトだった。こいつか!

<CABackingStore 0x1ed909d0 (buffer [640 960] BRGX8888) (buffer [640 960] BRGX8888)>

気づいてみたら、簡単な話だった。しかし簡単な話だから、いままであえて実験する気が起きなかった。プログラミングの調査では、確認していないものはすべて疑え、という教訓だ。

名前さえ分かってしまえば、こっちのものだ。CABackingStoreについて調べると、どうやらこれはCの構造体らしい。Core Foundationのオブジェクトタイプね。いくつかの関数が用意されている中に、CABackingStoreGetCGImage()という魅力的なものを発見。たぶん、こうやって使うんでしょう。

    CGImageRef cgImage;
    cgImage = CABackingStoreGetCGImage(view.layer.contents);

こうやると、確かに画像が取れた!おぉ、いいんじゃね、これ。

さらにCABackingStoreには、 CABackingStoreRetainFrontTexture()って関数もあるらしい。これを呼ぶときっと、このCALayerの描画に使われるOpenGLテクスチャが取得できるんでしょう。試しに呼んでみると、なんか返してくる。ただし、Objective-CやCore Foundationのオブジェクトではないっぽい。

これはなんだろう、とあちこち探しまわると、CA::Render::TextureというC++オブジェクトを発見。これかな?きっとここから、OpenGLテクスチャが取得できるんでしょう。

ということで、UIViewとCALayerとOpenGLの関係性が、コードレベルで分かってきたような気がする。あ、いちおう念のために書いておくけど、これらはとうぜんプライベートAPIなので、良い子は使わないようにお願いします。

UICollectionViewCellのサブクラスをiOS 5で共存させる


iOS 6ではUICollectionViewってのが追加された。テーブルビューを超えるとっても便利なクラス。でも、これを使ったアプリをiOS 5でも動作させようとしたら、ちょっとした問題が。

新機能を使ったアプリを過去のバージョンのOSで動作させる場合、いくつかのテクニックがある。新規フレームワークはweakリンクするとか。新規クラスは直接記述せずNSClassFromStringで取得するとか。そうやって一個ずつクリアしていったんだけど、これどうしよう?っていう問題にぶつかった。

UICollectionViewでは、UICollectionViewCellっていうセルクラスを使う。画面に表示する内容をカスタマイズするもので、テーブルビューのセルみたいなやつだ。UICollectionViewCellのサブクラスを作るってのが、普通の使い方になる。

これが問題に。サブクラスを作るということは、ビルド時に親クラスを指定してやらないといけない。指定された親クラスは、バイナリファイルに解決が必要なクラスとして記述されて、起動時にdyldが解決しようとする。このとき、UICollectionViewCellクラスが存在しないと、起動に失敗する。従って、バイナリにUICollectionViewのサブクラスが含まれていると、そのクラスを実行時にインスタンス化するしないに関わらず、iOS 5での起動ができない。

うーむ、困った。どうしよう?やっぱりUICollectionViewを使うのをやめるとか、iOS 5は対応しないとか、後ろ向きな解決策も考えた。または、iOS 5でも動作する互換クラスを使うとか(PSTCollectionViewみたいな)。けど、釈然としない。おれは、iOS 6ではUICollectionViewを使って、iOS 5では使わないっていう、そういうバイナリを作りたいんだよ。

いろいろ悩みまくったあげく、トリッキーな解決策にたどり着いた。要は、UICollectionViewCellのサブクラスが起動時に存在するから悪いのだ。ならば、始めはUIViewのサブクラスにでもしておけばいい。そして、起動した後で、その親クラスを差し替えればいいのだ。

「親クラスを差し替える?」なんだそりゃ、と思われたそこのあなた。Objective-Cではそんなことだってできてしまいます。だって、変態言語だもの。

Objective-CのランタイムAPIには、class_setSuperclassというのがある。文字通り、親クラスを設定するためのAPIだ。これを使えば、あるクラスの親クラスを動的に変更することができる。もちろん、下手にこんなことをすればとんでもないことになるので、使用には細心の注意が必要だ。ドキュメントには一言、「You should not use this function.」とだけ書いてある。そんなら、こんなAPI用意しておくなよ。

実験してみたloadメソッドに次のように書く。

+ (void)load
{
    // UICollectionViewCellのクラスを取得
    Class   collectionViewCellClass;
    collectionViewCellClass = NSClassFromString(@"UICollectionViewCell");

    // 存在する場合
    if (collectionViewCellClass) {
        // 親クラスを差し替える
        Class   magazineCollectionCellClass;
        magazineCollectionCellClass = NSClassFromString(@"HJMagazineCollectionCell");
        class_setSuperclass(magazineCollectionCellClass, collectionViewCellClass);
    }
}

これを使えば意図通りの結果を得ることができた。とっても気持ち悪いが、とりあえずこれでしのげるかどうか、しばらく試してみる。しかし、Objective-Cフリーダムすぎる。

iCloudの呪い


iCloudを使おうとして開発を進めて、諸般の事情によりiCloud機能をドロップしたプロジェクトがいくつかあるんだよね。iCloudに関するソースコードは、コメントアウトして通らないようにしてある。

そんなプロジェクトのアプリをテスト中、急に動かなくなる事態が発生している。これ、固まる訳じゃなくて、動かなくなるんだけど10秒くらいすると復活する。しばらくは問題なく動いているんだけど、また急に止まってしまう。

どこで止まっているんだ、とデバッガを使って調べたところ、iCloud関係のメソッドの中でブロックされているようだ。想像するに、データにアクセスするところでiCloudサーバを調べにいっちゃってるんじゃないかと。でも、もうiCloud機能ドロップしているんで、アプリのiCloud設定も全部落としてある。それで止まっちゃっている。

呪いだ!iCloudの呪いだ!

この呪いの嫌らしいところは、ビルドのクリーンをしたり、デバイスからアプリを削除しても治らないこと。iCloudサーバにデータが残っていると、サーバに接続して同期しようとするみたい。なにせクラウドだからな!

じゃサーバのデータを削除しようか、と思ってもその手段がない。developer.cloud.comとかを使ってサーバのデータを確認すると、確かに怪しいデータが残っている。でもこれ、見るだけで削除できないんだよね。なんてこったい。

「mkinoは呪いでアプリが動かない!」

これは教会でお祓いしてもらわないとダメかもしんない。

ちなみにこの問題は、開発中に使ったアカウントでしか発生しないので、リリースする分には問題ない。あと、iOS 5でしか発生しない。iOS 6は大丈夫っぽい。iOS 6は何気にiCloudが改善されている。

AppleにIn-App Purchase Contentをホストさせる


iOS 6のIn App Purchaseでは、ダウンロードするコンテンツをAppleのサーバにホストしてもらうことができる。それの実験中。

まずは、iTunes Connectに行くと、「Add Content」っていうボタンが増えている。なるほど。これでコンテンツファイルを追加できるのね。ってことで、Add Contentして、いくつかの設定をしたら、「パッケージファイルをアップロードしろ」って言われた。パッケージファイルってなに?

真っ先に思い浮かんだのは、Package Makerのこと。インストーラパッケージを作るためのアプリだ。最近使ってねーな。気がついたら、Package Makerはもう標準添付されていないし。Appleのサイトにいってダウンロードしないといかんのね。たぶんこれは違うだろ。

ドキュメントを探しまくってたら、「Xcodeを使って.pkgを作れ」って書いてあった。Xcodeで作れるのね。で、Xcodeのどこだ?

今度はXcodeの中をかけずり回って、やっと見つけました。[File]→[New]→[Target…]メニューを選択する。するとターゲットテンプレートの中に、「In-App Purchase Content」があったぜ。

これすか!これを作った後は、コンテンツファイルを追加して、アーカイブすると、XcodeからApp Storeにアップロードできる。とりあえず、コンテンツを送り込むところまでは成功したっぽい。

次は、アプリ側の変更だな。SKDownloadを使うようにしないといけない。これを使うと、待望のバックグラウンドダウンロードができるはずなんだよね。

iOS 6でEvent Kitでイベント追加には、追加のメソッド呼び出しが必要


Event Kitを使ってカレンダーにイベントを追加する場合、iOS 5までは特にユーザに同意を求めなくともできた。iOS 6からは、ユーザに同意を求める事が必須になる。

そのために、EKEventStoreにrequestAccessToEntityType:completion:というメソッドが追加された。これを呼ぶと、ユーザに同意を求めるアラートが、OS側から表示される。ここでOKをもらうと、カレンダーへのアクセスが可能になる。これを呼ばないiOS 5のソースコードでは、iOS 6でまったくカレンダーにアクセスできなくなるので注意。defaultCalendarForNewEventsでnilが返ってくる。

このメソッドは、アラートを表示するため、続きをcompletionブロックの中でやらないといけないので注意。さらに、completionブロックは、メインスレッド以外で実行されるようなので、さらに注意。iOS 5と共存するコードを書くには、ちょっと手を入れる必要あり。

Xcode 4.5がMac App Storeに登場、あとUIRefreshControl


iOS 6が、一般に公開。同時に、Xcode 4.5もMac App Storeに登場。これで、iOS 6ネタは解禁ということでいいのかな。

ということで、iOS 6ネタをひとつ。iOS 6からは、UI KitにUIRefreshControlが追加された。これは、いわゆる「引っ張って更新(Pull to Refresh)」を実現するコントロール。サードパーティアプリの機能を、本家が取り込んだ格好だ。

UIRefreshControlは、引っ張ると、うにょーんとのびる。この辺はAppleらしさが追加されたようだ。

UIRefreshControlの使い方は簡単で、インスタンスを作成して、スクロールビューにaddSubview:してやればよい。位置とか大きさとかは気にしなくとも、放り込んでおけば、あとは勝手に面倒を見てくれる。

UIRefreshControl* refreshControl;
refreshControl = [[UIRefreshConrol alloc] init];
[refreshControl addTarget:self action:@selector(refreshAction:) 
        forControlEvent:UIControlEventValueChanged];

[_scrollView addSubview:refreshControl];

スクロールビューであれば、UITableViewだろうが、UIWebViewのやつだろうが(scrollViewプロパティから取得する)、何でもオッケー。便利だ。

引っ張った後の更新は、アクションとして受け取る。UIControlEventValueChangedイベントが発行されるので、そいつを受け取ればいい。

iPhone 5からはarmv7sアーキテクチャ


iOS SDK 6がGMになったんで、過去のプロジェクトを再ビルドしまくっているけど、アーキテクチャにarmv7sが追加になったようだ。

ざっと説明しておくと、アーキテクチャはどのCPU命令セットに対してバイナリを作成するかを指定するもので、初代の頃のiPhoneはarmv6、その後、3GSあたりからarmv7になった。で、どうやらiPhone 5とiPod touch 5th generationからは、armv7sになるらしい。

Xcode 4.5を使うと、armv7とarmv7sのバイナリを作成できる。iPhone 5は、armv7sバイナリだと最高のパフォーマンスを発揮できる。armv7バイナリのみでも、動く。たぶん。でもarmv7sバイナリを用意するのが望ましい。

アーキテクチャの設定は、Xcodeのプロジェクトが、自前のソースコードしか使っていないのであれば、特に気にかける必要はない。でも、どっかからダウンロードしてきたり、他の会社から提供された.aファイルを使っている場合は、とても注意が必要。その.aファイルもarmv7sバイナリを含んでいないと、リンク時にエラーが出る。「file is universal but dose not contain an armv7s slice」って感じの。

そんなときは、アーキテクチャの設定からarmv7sを削除してやる。Xcodeプロジェクトの、ビルド設定で、「Valid Architectures」っていう項目があるので、そこからarmv7sを削除する。

こういうの、サイレントに追加してくるよな。CPUのコアはなんだっていいから、アーキテクチャの方を教えてくれ。

mergeChangesFromContextDidSaveNotification:って何のために?


先日は、MOSAでCore Dataセミナーをやってきました。参加された方々、お疲れさまでした。

で、そのとき質問があって、NSManagedObjectContextのmergeChangesFromContextDidSaveNotification:に関する事だったんだけど、そのとき「このメソッド、あんまりよく分からないんだよねー」と答えてしまいました。そしたら、アンケートで「このことが聞きたかったのに残念だ」という意味のことを書かれてしまったので、ちょっとこの場でフォローします。

まず、前提。状況としては、複数スレッドからCore Dataにアクセスしている。スレッド毎にmanaged object contextを作成し、同一のpersistent store coordinatorを使う。

まず、サブスレッドを立てて、managed objectを追加したとする。そうすると、そのスレッドのmanaged object contextでは、追加される。でもこの追加は、メインスレッドのmanaged object contextからは検知されない。どうもオブジェクトの追加は、managed object contextレベルで行われているらしい。サブスレッドが終了すると、この追加もそのまま消えてしまう。

そこで、サブスレッドでオブジェクトを追加した後、保存を行う。そうすると、追加されたオブジェクトがデータベースに保存される。データベースに保存されるので、永続化される。この変更は、メインスレッドのmanaged object contextからも知る事ができる。保存後にフェッチすれば、追加されたオブジェクトを取得できる。

で、保存すると、NSManagedObjectContextDidSaveNotificationが通知される。それを捕まえて、Appleのドキュメントによれば、managed object contextのmergeChangesFromContextDidSaveNotification:を呼んでやれば、変更がマージされる、ということになっている。でもねー、保存した時点でデータベースに変更が反映されているんだから、マージする必要あんの?ってのが疑問としてあった。だから、「Appleのドキュメントに、このメソッドを呼べって書いてあるから呼ぶけどさー、これ別に呼ばなくても構わないんじゃないの?なんのためにやるのか、よく分かんないんだよねー」って思ってた。

そのときの質問は、mergeChangesFromContextDidSaveNotification:はメインスレッドで呼ぶ事になっているけど、そのときに時間がかかったりしないか、パフォーマンスは大丈夫か、というものだったんだけど、基本的には重い処理は保存のところなので、それは別スレッドで呼ばれるから大丈夫だと思う。

この、mergeChangesFromContextDidSaveNotification:を呼んだときの、別managed object contextのオブジェクトのマージって、どんなことが行われるんだろか。いまメモリ上に保持しているmanaged objctに限ってマージされる、ってことなのかな。であれば、リフェッチすれば、そもそも気にしなくていいのかな。