HMDT Blog | ページ 20

deprecatedには、こまめに対応しないといかん


UIImageを引き延ばして利用するために、昔からstretchableImageWithLeftCapWidth:topCapHeight:っていうメソッドがあった。これ、iOS 5.0でdeprecatedになって、resizableImageWithCapInsets:を使え、ってことになっている。

Deprecatedになったメソッドの嫌らしいところは、この宣告がなされたとしても、とりあえずは使える。ビルドは通るし、実行もできる。だけど、バージョンアップを繰り返してある日気付くと、あれ?なんか動作がおかしい。調べてみると、deprecatedのせいだったりする。

最近問題になっていたのは、UISliderのsetMinimumTrackImage:とsetMaximumTrackImage:。スライダーの背景画像を設定するためのメソッドだ。これが、iOS 5.0までは問題なかったのに、6.0からどうも挙動が怪しい。描画が乱れるときがある。でも、毎回必ずって訳でもなくて、ちゃんと動いているときもある。

なんでだろう、とあちこちいじってたら、設定する画像をstretchableで引き延ばしていたのが問題だった。resizableにしたら、期待通りの動作になった。分かりにくいよー。

一見動作しているように見えても、後のバージョンのことを考えると、deprecatedにはこまめに対応するのが必要だ、と思わせる事でした。

Google Maps SDK for iOSがきた


Google Maps SDK for iOSがきたー!

ということで、ダウンロードして絶賛テスト中。

SKStoreProductViewControllerでハマったこと


iOS 6から、SKStoreProductViewControllerっていうクラスが追加された。アプリ内でApp Storeの購入画面を表示できるスグレもの。ユーザの導線をアプリ内から逃がさずに、他のアプリを紹介できる。

こいつには、loadProductWithParameters:completionBlock:っていうメソッドが用意されていて、これにストアで表示させる項目をいろいろと設定する。

    // Create controller
    SKStoreProductViewController*   controller;
    controller = [[SKStoreProductViewController alloc] init];
    controller.delegate = self;

    // Set parameter
    NSDictionary*   parameters;
    parameters = [NSDictionary dictionaryWithObjectsAndKeys:
            @"500860946", SKStoreProductParameterITunesItemIdentifier,
            nil];

    // Present controller
    [self presentViewController:controller animated:YES completion:^() {
        // Load product
        [controller loadProductWithParameters:parameters
                    completionBlock:^(BOOL result, NSError* error)
        {
            // For success
            if (result) {
                //...
            }
            // For error
            else {
                //...
            }
        }];
    }];

このコードで、表示できる事が確認できた。

ちなみにドキュメントの方を確認すると、このメソッドの使い方としてこう書いてある。

In most cases, you should load the product information and then present the view controller.
(多くの場合、プロダクト情報を読み込んでから、ビューコントローラを表示するべきでしょう)

これ、そのまんまの通りに解釈すれば、loadProductWithParameters:を呼んで、それが成功したらpresentViewController:する、って思うでしょ。始めそれでやってみたんだけど、それだとアクセスに失敗したときにエラーも何も返ってこない。completion blockがまったく呼ばれないの。最初に実験したとき、間違ったプロダクトIDを与えていて、それでなんの音沙汰もなし。

なんでじゃー、といろいろいじくっていたら、presentViewController:を呼んでおかないと、エラーのときのcompletion blockは呼ばれなかった。ということで、なにはともあれpresentViewController:することにした。でもそれなら、ドキュメントに書いてある事まぎらわしくねー?

モーダルビューとは何なのか


そもそもの発端は、UIViewControllerをモーダルで表示して、それを半透明にしたかった。ほら、モーダル表示すると、下からシュワンと出てきて、重なって表示されるでしょ。このとき、上のビューを半透明にしたらかっこいいんじゃないか、って思ったんだ。

やってみたら、できなかった。なんか、真っ黒になった。Google先生に聞いてみた。modalPresentationStyleをUIModalPresentationCurrentContextにすればいいよ、っていう記事が見つかる。確かにこれでできた。でも、なんか微妙に動きが怪しい。UIModalPresentationCurrentContextってやつも、何なのか、ドキュメントを読んでも、いまひとつ要領を得ない。

てか、そもそもモーダル表示って何なのよ?addSubview:するのと何が違うの?気になったので、少し調べてみた。モーダル表示を何度も重ねる事のできるサンプルを作った。あるUIViewControllerをモーダル表示して、そいつに対してまたモーダル表示して、ということでモーダルのモーダルのモーダルを実現する。その状態で、ビュー階層を調べてみた。

すると、いくらモーダルを重ねても、ビュー階層に残っているのは、一番上に表示されているUIViewControllerのビューだけだ、ということが分かった。モーダルの下になったコントローラ、つまりpresentViewController:animated:completion:を呼び出した側は、モーダルが表示されたらビュー階層から外れるんだ。知らなかったー。

これで、冒頭の半透明表示がうまくいかない理由が分かった。半透明にはなってたんだけど、下に透けて見えて欲しかったビューが、もう外れてしまっていたんだね。なるほど、納得。

じゃ、modalPresentationStyleをUIModalPresentationCurrentContextにするとどうなるかっていうと、この場合はビューが外れなかった。モーダルを重ねたら、その分すべてのビューが追加されたままになっている。うーむ、なるほど。それはそれで、嫌な事が起きそうだなー。

半透明にしたかったら、自分で制御可能な分、addSubview:した方が安全な気がする。ただその場合、viewWillAppear:とかviewDidAppear:とか、addChildViewController:とかを自前で呼ばないといけない。その変が面倒だし気をつけないといけないところだ。

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


電子書籍、特に現状の電子書籍に対して批判的な文脈では、ページめくりアニメーションが何かと槍玉に上げられる。いわく、意味なく紙の模倣をしている、無駄に遅い、スクロールの方が電子的だ、など。最近だと、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なので、良い子は使わないようにお願いします。

HMDT JOURNAL Vol.010配信開始


Vol.009から約5ヶ月、ようやくHMDT JOURNAL Vol.010の配信を開始しました。あまりに間が空いてしまったので、ひっそりといきます。

Vol. 010では、3つの記事を掲載。1本目は『iOS API探訪:第8回 Core Image(3) ブレンドモードフィルタ(続)』。Core Imageでのブレンドモードフィルタの続きだ。フィルタを紹介しているんだけど、今回取り上げるのはHard LightとかSoft LightとかDeifferenceとかExclusionとか。使い方によっては、派手というかサイケデリックな効果になりますぜ。

2本目は『Store Kitによるコンテンツのホスティング』。連載記事じゃなくて、単発記事になるよ。iOS 6から可能になった、Appleサーバへのコンテンツホスティング。その手順とコーディングを詳しく解説した。

ちなみに、HMDT BOOKSアプリでもコンテンツホスティングをやろうとしていろいろ試したんだけど、結局見送った。そのときの考察した結果が、この記事の結論部分に反映されてるよ。

3本目は『フォントとコードの話:第6回 UI Kitで属性付き文字列』。これもまたiOS 6で、UILabelとかUITextFieldで属性付き文字列が使えるようになった。その話を取り上げているぜ。

どうにか再開にこぎつけたので、これからもがんばって続けていきます。絶対毎週出す!とは、なかなか断言しづらいけど、がんばりますです、はい。

HMDT BOOKS 2.0登場〜iOS 6とiPhoneに対応


HMDTが送る電子書籍ストアアプリHMDT BOOKSですが、このたび2.0が登場!

2.0では、ようやくiOS 6に対応。遅くなってしまってご迷惑をかけました。ついでなので、iPad miniでも動作確認済み。HMDT JOURNALはiBooks Authorでオーサリングしているんで、iPad miniで表示させると、ジャストフィットだよ。miniは、ほんと電子書籍と相性がいいねぇ。

miniっぽさを強調するために、手と一緒に写真撮ってみた。

さらに、いままで要望の多かったiPhoneへの対応もやりました。画面が小さいのでちょっと読みにくいけど、ちゃんと読む事はできるよ。

あと、いままで出した全部のコンテンツを更新しました。すでに購入したユーザの方も、ライブラリ画面のボタンが「更新」になっているはず。これを押すと、第二版が手に入りますぜ。電子的なアップデートこそ、電子書籍の優位点だ。

その他にも細かい修正が色々とあるので、既存ユーザの方はアップデートを、もちろん新規ユーザの方もどうぞ。

え?HMDT JOURNALの続きはどうなっているかって?これも、中断してしまっていてすいません。最新号のVol.010は、近日登場します!本当は、HMDT BOOKS 2.0の登場と同時に出したかったんだけど、予想より速く審査が通ってしまったんで、後になってしまったです。とにかく、Vol.010ももう少しで出るので、あと少しだけお待ちを。

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フリーダムすぎる。