SimpleBoxes

汎用コードを複数の iOS アプリケーション開発で共有利用する手法

いくつかの iOS アプリケーションを開発していると、自然と共有できるコードが増えてきます。

そうした共有可能な汎用コードを複数のアプリケーションのプロジェクトで利用する方法はいくつか考えられます。

アプリケーションのプロジェクトに組み込む

別リポジトリで管理している汎用コードを svn externals や git submodule などを使ってリンクして、それをそのままアプリケーションのプロジェクトに組み込みます。

svn や git などバージョン管理ソフトウェアの機能だけを利用しているので、仕組み自体はとてもシンプルになります。

構成はシンプルになりますが、その分運用でカバーしなくてはいけない点があります。

  • 汎用コード部分のビルドがアプリケーションのプロジェクトに依存するので、汎用コード部分の独立性を保つのに注意が必要。

  • 汎用コード部分でファイルの追加・削除があった場合、それを参照している全てのアプリケーションプロジェクトを逐次変更しなくてはならない。

  • 汎用コードを利用する新規アプリケーションプロジェクトを作成して、リンクしたリポジトリを取り込む際、ターゲットに含めるファイルとそうでないファイルの切り分けが必要。

総じて独立性が乏しい分、手順が増える傾向があります。

静的ライブラリとして生成する

汎用コード部分を静的ライブラリとして作成して、アプリケーションのプロジェクトにライブラリとして登録します。

静的ライブラリを作成するプロジェクトファイルがアプリケーションのプロジェクトとは独立した形で作成されるので、アプリケーションのプロジェクトに依存することなく、ビルド・ユニットテストを行うことができます。

Xcode は外部プロジェクトもプロジェクトに組み込めるので、アプリケーションをビルドする際に都度静的ライブラリを生成するような形で運用することも可能です。

Xcode の基本機能だけで完結しますし、手順もそれほど難しくはないのですが、イメージや xib などのバイナリリソースが静的ライブラリには、そのまま組み込めないという制約があります。

イメージなどのバイナリリソースは静的ライブラリとは別にアプリケーションのプロジェクトに組み込む必要があります。

その場合、汎用コード部分で利用されるリソースファイルの追加・削除の手間は前述の組み込み型と同等になります。

フレームワークとして生成する

静的ライブラリではリソースは組み込めないのですが、フレームワークという仕組みを利用するとリソースも込みでライブラリを生成することができます。

UIKit などはフレームワークの形で配布されて、ほぼ全てのネイティブ iOS アプリケーションで利用されています。

汎用コード・リソースの共有化という点では最適な方法なのですが、Xcode の標準な方法ではサポートされないという欠点があります。

Mac OS X 向けアプリケーションに対してはフレームワーク生成がサポートされていますが、iOS プラットフォームではサポートされていません。

iOS ユニバーサルフレームワークなどを利用すると iOS プラットフォーム向けフレームワークを作成することができます。

github での説明を見ても分かる通り、現状ではややハック的な運用にならざる得ないようです。Fake Framework という仕組みではバンドルを利用していたりするみたい。

静的ライブラリをリソース込みで生成する

拙作 SBPullToRefreshHeaderView のようにリソース込みな汎用コードがある場合、リソースがあるのでやはりフレームワークを使う方法になると思います。

ハック的な運用を避けて、Xcode 標準な機能だけでなんとかならないか、と探していたところ、Compiling Image Resources into a Static Library という記事を見つけました。

静的ライブラリにはバイナリリソースは組み込ませんが、バイナリを C コードの形でダンプしてコンパイルしてしまえば、データとして組み込むことが可能になります。

  1. イメージなどのバイナリを xxd -i コマンドを使って C コードに変換
  2. コード化されたデータを UIImage など Objective-C クラスに変換するローダーを生成
  3. リソースはローダーを介して読み込む

Compiling Image Resources into a Static Library では、イメージだけ処理していますが、同様に nib ファイルも処理できます。具体的な手順については後日改めて。

このエントリーをはてなブックマークに追加

スポンサーリンク

Fastbook は UIWebView でも軽快に動作する件

iOS 版 Facebook の実装が HTML5 アプリケーションからネイティブアプリケーション (CocoaTouch フレームワークを使ったアプリケーション) に変更されたのは記憶に新しいところ

それに対して「Facebookのモバイルアプリが失敗した理由は HTML5 のせいじゃない。HTML5 でサクサク動く Facebook アプリを作って見せた Sencha Touch 開発チーム」という記事がありました。

元ネタは The Making of Fastbook: An HTML5 Love Story (以下、元記事) です。

簡単に言うと、「HTML5 だから遅いんじゃないよ、デモアプリケーション Fastbook のようにネイティブアプリケーション同等な (むしろそれを上回る) パフォーマンスでるよ」ということになるでしょうか。

動画も挙がっていて、自信の程が伺えます。

これに対して、いくつか指摘が挙がっています。

え?これSafariじゃね?WebViewとSafariじゃあスピード全然違うよ

[→はてなブックマーク - hazisarashi のブックマーク - 2012年12月20日]

Safariとかずるすぎワロタ。WebviewはSafariの約3.3倍遅いんですが、Webviewで動かすとどうなるんでしょうね(ゲス顔

[→はてなブックマーク - fikah のブックマーク]

同様に元記事のコメントでも……

The demo is really promising, but it is in MobileSafari. When you make an iOS app, you can only use WebView, which is a second-class and slower browser.

[意訳] デモはとても前途有望だけれど、MobileSafari で動いているね。iOS アプリを作る時には WebView しか使えなくて、それは遅いブラウザになってしまうよ。

[→元記事コメント @hlb]

A sticking point with iOS is that embedded WebKit (in Facebook’s old native app) performs horribly compared to Mobile Safari (what Sencha used here) due to Apple's selfish hoarding of the Nitro JavaScript engine.

[意訳] 問題は Facebook の前バージョンでも利用されてた組み込み WebKit で、このデモで使っている Mobile Safari よりもひどく遅いけど。Apple の勝手な囲い込みで Nitro JavaScript エンジンは使えないから。

[→元記事コメント @Ron Waldon]

Mobile Safari では Nitro と呼ばれる JavaScript エンジンでスクリプトが処理されるのですが、CocoaTouch フレームで利用できる UIWebView ではその高速な JavaScript エンジンが (セキュリティ上の理由から) 使われません。

私もこの点 (デモが Safari 上で動いている) は少し気になっていました。

それに対して、Sencha Team のメンバーの何人かが答えています。

@hlb the issue is NOT JavaScript. It's the DOM. The DOM engine is the *same* for both use cases.

[意訳] @hlb JavaScript が問題ではないんだ。DOM が問題で、DOM エンジンは (Safari も UIWebView も) 同じなんだよね。

[→元記事コメント @Jay Garcia]

@hlb Simply save the app to your homescreen to force it to use a WebView.

[意訳] @hlb ホームに置くと、WebView を使うようになると思う (iOS 5.1 以降では Mobile Safari を利用するようになったという指摘あり)。

[→元記事コメント @Jamie Avins]

@Ron Waldon It doesn't matter for such apps like this. Again as we mentioned the bottleneck is never JavaScript execution. We wrapped the app in native webviews for you to try it out yourself: github.com/extjs/fastbook

[意訳] @Ron Waldon このアプリではそこは問題にならないんだ。(これまで) 指摘している通り、ボトルネックは JavaScript の実行にあるわけではないから。ネイティブな webview で実装した例を github に挙げたので、試してみてね。

[→元記事コメント @Jacky Nguyen]

Jacky Nguyen さんと Jamie Avins さんは動画にも登場されています。

コメント受けて、WebView を使った利用例が github に公開されました

コードは実にシンプルで、

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    self.window.backgroundColor = [UIColor whiteColor];
    
    UIWebView* view = [[UIWebView alloc] initWithFrame:[UIScreen mainScreen].applicationFrame];
    [self.window addSubview:view];
    [view loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://fb.html5isready.com/"]]];
    [self.window makeKeyAndVisible];
    return YES;
}

メインのコードはこれだけ。UIWebView を作って http://fb.html5isready.com/ を開いているだけです。

動作させてみると、私が思っていた以上に速い。iPhone 4S / iOS 6.x で確認。確か iOS6 から WebView に対して動作割当メモリが増えたので、それも影響している可能性あり。

UIWebView 上の WebKit でも、スクロールとかきびきびと軽快に動作します。

iOS のアプリケーションとしては、ステータスバーをタップしてもトップにスクロールバックしない、ローディングアニメーションが表示されるべきところで表示されない、未実装な機能 (例えば検索機能) がある、などブラッシュアップが必要な部分はありますが、デモアプリケーションとしてはなかなか興味深い。

もっともこれでもって「HTML5 使うべき」とも言えない部分もあって、

スクロール速度だけを取り上げても。Facebookのエンジニアが指摘した第一の問題はそこじゃないですよね。クラッシュの原因が解らない等 http://lists.w3.org/Archives/Public/public-coremob/2012Sep/0021.html

[→はてなブックマーク - Teshのブックマーク]

という Tesh さんのブックマークコメントや

The premise of this argument is wrong. It's not a simple question of "slow" versus "fast." Application performance is only one factor in building an HTML5 app. Device support, engineer productivity, testability, measurement, tooling/debugging, and native integration are all points that should be factored in. Anyone who has burnt the hours trying to debug a perf issue on a device that doesn't support Webinspector knows this pain. As does anyone who has gotten the bug report about the keyboard not popping up correctly on insert random Android 2.X no one has ever heard of here. As does anyone who has gotten the layout performance awesome in one test environment only to test another and find it terrible. The argument that HTML is ready while the device landscape is not is pretty fraught.

As far as fast goes, even as well done as this demo is, the scrolling framerate is noticeably slower on the Nexus 4 compared against the native application. I feel like all the demo is proving is what we already knew - somethings can be done reasonably well in HTML while many other can't. I'm a person who would rather have the web win. I'm pretty sure it will win in time. It's a pretty clear fact that until the browsers/webviews get faster and/or the device's have faster hardware, HTML is the second best option for smartphones.

[意訳] 議論の前提が違っているよね。単に「遅い」か「速い」という問題じゃない。アプリケーション性能は HTML5 アプリを構築するひとつファクタに過ぎないよ。どのデバイスをサポートするか、エンジニアの生産性、テストのしやすさや計測しやすさ、ツールの使い勝手、デバッグのしやすさ、ネイティブなサポート、など全てのファクタを考える必要がある。ウェブインスペクタをサポートしないデバイスでパフォーマンスをデバッグするのがどんなに大変か知っている人はいる?Android 2.X でキーボードが正しく表示されずにランダムな文字が入力されてしまうというバグレポートを見たことがある人は?あるテスト環境では素晴らしく性能がいいのに、別の環境ではめちゃくちゃだというのを経験したことがある人は?この議論を実際デバイスを横向きにして見ると見づらいという事実についてはどう?

[意訳] 速さという点に関しては、このデモはとても良くできている。スクロールのフレームレートは Nexus 4 で見るとネイティブアプリに比べて遅いけど。このデモは Sencha Team が HTML に関して深く理解している (そしてそれは他の多くの人はできない) こと証明しているに過ぎないと思うんだ。私個人的にはむしろ web が勝って欲しいし、いつかその日がくると信じているけれども、それはブラウザとデバイスが十分に速いというの明快になった時で、HTML はスマートフォンに対して二番目にベストなオプションだと思う。

[→元記事コメント @Jackson Gabbard]

元記事に寄せられた Jackson Gabbard さんのコメントのご指摘の通り、単純に速い・遅いという問題ではなくて、もちろんそれはとても重要な部分ではあるけれども、あくまでも全体の一部の問題でしかありません。

ツールの使いやすさ、デバッグのしやすさ、習得に対する敷居の低さ、などはソフトウェアを恒常的にメンテナンスする上で、重要なポイントになってきます。

Objective-C が習得に対して敷居が低いかどうかはまた別の議論になってしまいそうですが、ネイティブサポートのため、ハック的な知識がなくてもある程度の性能はいけるという印象はあります。もちろん実装次第ですけれども。

これは SenchaTitanium だけでなく、他のクロスプラットフォームなフレームワークでも大きな課題になっているポイントのような気がします。

このエントリーをはてなブックマークに追加

スポンサーリンク

Visual Studio 2010 Express for Windows Phone を Windows 8 にインストール

[図] Visual Studio 2010 Express の起動画面

Visual Studio 2010 Express for Windows Phone を Windows 8 にインストールする手順について、備忘録

元記事 に全ての手順ならびに要因が詳しく説明してあります。

  1. Windows Phone SDK 7.1 をダウンロード・インストール
  2. Games for Windows Marketplace Client をダウンロードして・インストール
  3. 1. でダウンロードしたインストーラを再度起動して、修復 (repair) を選択
  4. Windows Phone SDK 7.1.1 Update をダウンロードして、SDK 7.1.1 にアップグレード

3. のステップが終了した段階で、Visual Studio 2010 Express は起動できるようになりますが、エミュレータが正しく起動しません。SDK 7.1.1 にアップグレードすることでエミュレータも動作するようになります。

このエントリーをはてなブックマークに追加

スポンサーリンク

iOS デバイス一覧表

iOS デバイスの一覧表を自分用に作成 (2014/11 現在)。

デバイス発売年CPU解像度iOS
バージョン2345678
iPhone2007armv6320 x 4801.0 - 3.1.3 ×××××
iPhone 3G2008armv6320 x 4802.0 - 4.2.1 ××××
iPhone 3GS2009armv7320 x 4803.0 - 6.1.4 ×××
iPhone 4 (GSM)2010armv7640 x 960 *4.0 - 7.1.2 ×××
iPhone 4 (CDMA)2011armv7640 x 960 *4.2.5 - 7.1.2 ×××
iPhone 4S2011armv7640 x 960 *5.0 - ×××
iPhone 52012armv7s640 x 1136 *6.0 - ××××
iPhone 5c2013armv7s640 x 1136 *7.0 - ×××××
iPhone 5s2013arm64640 x 1136 *7.0 - ×××××
iPhone 62014arm64750 x 1334 *8.0 - ××××××
iPhone 6 plus2014arm641242 x 2208 **8.0 - ××××××
  
iPod touch2007armv6320 x 4801.1 - 3.1.3 ×××××
iPod touch (2nd)2008armv6320 x 4802.1.1 - 4.2.1 ×××××
iPod touch (3rd)2009armv7320 x 4803.1 - 5.1.1 ××××
iPod touch (4th)2010armv7640 x 960 *4.1 - 6.1.4 ××××
iPod touch (5th)2012armv7640 x 1136 *6.0 - ××××
  
iPad2010armv7768 x 10243.2 - 5.1.1 ××××
iPad 22011armv7768 x 10244.3 - ××
iPad (3rd)2012armv71536 x 2048 * 5.1 -×××
iPad (4th)2012armv7s1536 x 2048 * 6.0 -××××
iPad Air2013arm641536 x 2048 *7.0 - ×××××
iPad Air 22014arm641536 x 2048 *8.1 - ××××××
  
iPad mini2012armv7768 x 10246.0 - ××××
iPad mini Retina2013arm641536 x 2048 *7.0 - ×××××
iPad mini 32014arm641536 x 2048 *8.1 - ××××××

"CPU" とあるのは対応する CPU アーキテクチャを示しています。解像度で強調表記されているのは Retina ディスプレイを示しています。** は 3x 解像度です。

以下の iOS は特定のデバイスにのみ対応したバージョンになります。

3.2.*iPad (初代) 専用バージョン。
4.0 - 4.1iPad 未対応バージョン。
4.2.5 - 4.2.10iPhone 4 (CDMA) 専用バージョン。
参考
このエントリーをはてなブックマークに追加

スポンサーリンク

Safari で Reeder

[画像] Reeder on Safri

reederGoogle Reader を快適に利用できるアプリケーションです。

その reeder からインスパイアされて、似たルック & フィールを Chrome 上で実現する機能拡張が Reeder for Chrome (Unofficial) (以下、RFC) です。

残念ながら RFC の開発は継続されないことになりましたが、ソースは公開されています

RFC の最も肝となるルック & フィールは基本 css のみで実現されていて、Stylish を使えば、他のブラウザでも利用できそうです。

他に fork している人がいたので、その内容を確認しようと思っただけだったんですが、間違えて自ら fork してしまい、それをきっかけに中身を確認することに……。

  1. Stylish for Safari より Stylish の機能拡張をダウンロード・インストールします。
  2. ツールバー上にある「Stylish」ボタンから管理画面 (Manage) を開きます。
  3. 「Edit」を選ぶと新規スタイル編集画面が開きます。
    Title
    RFC
    CSS
    GitHub から CSS の内容をコピー
    Applies to
    "Regexp" を選択して、https?://www.google.*/reader/.*

その後、Google Reader を見ると Reeder のようなインタフェースに変わっていると思います。オススメは 3 カラム表示。

(Webkit に特化したスタイルを利用しているため) CSS の一部変更が必要になりますが、Firefox でもほぼ同様に Stylish から利用できます。

このエントリーをはてなブックマークに追加

スポンサーリンク

バースデーカードを作ってみた

[写真] 今年の誕生日カード

先日、娘が誕生日を迎えました。

一応毎年カードみたいなものを用意していて、去年はカードの代わりに iPad で動く「誕生日アプリ」……マイクを使って息を吹きかけるとロウソクが消えるエフェクト付き……を作りました。

今年はどーしよーかなぁと思っていたんですが、なにやら最近少しだけ「ペーパークラフト」に興味を持っているようなので、ちょっとした工作をしてみることに……。

ざっと調べると、ペーパークラフトで文字が浮き出るような効果を出すために使える POPUP フォントなるものがいくつか見つかりました (Balloon Tales | Articles | Tutorial: Pop-Up LettersPop-Up Font | dafont.com)。

フォントを見てみると、専用フォントを使わなくても、ある程度線が太ければ利用できそうな感じだったので、自作してみることに。

ポイントは文字が浮き出たときに補助となる出っ張り部分。切り抜きのひな形を作るのはそれほど難しくありません。

[図] 紙半分の折り目にあたる部分に線を引く

まず、紙の半分、(印刷したときに) 折り目にあたる部分に直線を引きます。

[図] 折り目にまたがるように文字を置く

折り目をまたがるように文字を置きます。今回使ったのは Optima の Extra Bold 。個人的に気に入っているフォントで FloatyMemo のロゴにも使っています。

[図] 出っ張る長さを測る

折り目から文字の底にあたる部分の長さが、文字が浮き出てくる長さに相当します。

[図] 文字の上に出っ張り補助部を付ける

先に測った「折り目から文字底までの長さ」の分だけ文字の上に補助部をつけます。

[図] 他の文字も同じようにして補助をつける

他の文字にも同じようにして補助線を付けていきます。今回は文字を反転させています。これはカードの裏面に作ったひな形を印刷するためで、そうすることによって印刷した線が見えなくなります。

[図] 折り目になる部分

印刷した線は折り目以外は切り取ります。

  • 一番最初に引いた直線 (紙半分、カード自身の折り目にあたる)
  • 文字の底 (文字下部の折り目)
  • 文字の天で補助部と接する線 (文字上部の折り目)
  • 補助部の天 (出っ張りの折り目)

がそれぞれ折り目になります。このうち、「文字の天で補助部と接する線」以外は同じ方向に折ります。裏面に印刷するので、印刷面から見て「山折り」になって、「文字の天で補助部と接する線」だけは「谷折り」になります。

[写真] カードの裏面に印刷

少し厚手の紙に印刷します。印刷面がカードの裏面になります。

[写真] 線に沿って切っていきます

定規とカッターで丁寧に切っていきます。曲線部分はやや慎重に。折り目にあたる部分は山側になる方に軽く切れ込みを入れます。「文字の天で補助部と接する線」は表面が山側なので、そこだけは印刷面とは反対側から切り込みを入れます。

[写真] ゆっくり折っていきます

実のところ、折る作業が一番気を使うところかもしれません。写真だと分かりづらいですが、今回「T」の文字がやや失敗してしまって、横棒と縦棒のつなぎの部分が少し折れてしまいました。

このエントリーをはてなブックマークに追加

スポンサーリンク

WWDC 2012 雑感

Retina ディスプレイが搭載された新しい MacBook Pro が発表されました。

15 インチ、薄型で 13 インチ MacBook Pro より軽い、DVD ドライブはおろか HDD ドライブもなし。メモリもオンボード直付けで後からの換装は不可能。…… MacBook Air 15 インチのような存在というと語弊があるでしょうか。

Retina ディスプレイ MacBook Pro では、Tunderbolt を二口搭載しつつ、さらに HDMI を搭載しています。つまり外部ディスプレイ用ポートが標準で 3 つあることに。

とりあえず、現在の仕様上では、外部ディスプレイ二つまではサポートしているようです (つまり本体ディスプレイと合わせて 3 つのディスプレイ表示)。

折角、グラフィックチップも Intel HD Graphics 4000 と NVIDIA GeForce GT 650M の二つ搭載していますし、将来的にはソフトウェアアップデートで 3 つの外部ディスプレイサポートなんてこともあるんでしょうか……。

今回の WWDC の発表では、何といっても iOS6 が気になります。さっそくベータをダウンロードして使っていて、色んな変化があって嬉しいやら (今後の対応を考えると) 辛いやら……。

NDA があるので、どこまで触れていいのか分からないのが悩みどころ。Apple から公にされている部分は触れても構わないと思うので、そこを中心に……。

大幅に更新された Map アプリは、もうただ驚き。まだベータ版っぽい動作で粗はいっぱいありますが、使っていて楽しいアプリであることには間違いないです。……前職だったので、ちょっと指摘するのが辛いところもあるんですが、ナビ関係のアプリ開発はこれから大変そうです……。標準添付だもんなぁ、これ。

メールアプリ、Apple 公式の Pull-to-Refresh が来ましたね。この挙動、WWDC でデモしたのかな。それとメールや設定アプリのスクリーンショットで気付いた方もいるかもしれませんが、バッテリー状態などを表示するステータスバーのアピアランスが変わっています。グレーじゃなくなって、バッテリーアイコンが単色に……。結構雰囲気が変わって見えますね。

Facebook 対応は結構嬉しい。私が利用するかどうかというよりも、おそらく Twitter 同様 Apple からフレームワークとしてサポートされると思うので。そうなると、Facebook 対応アプリを作りたいという時に開発負担が激減すると思います。

初代 iPad が対応していないのはちょっと残念。それより前にリリースされている iPhone 3GS が対応しているので余計。メモリ的に厳しいのかなと予想。やはり Siri が……。

WWDC 基調講演で発表された以外にもいろいろアップデート (Mac Pro とか、AirPort Express とか) があったようで、盛りだくさんな感じですね。

このエントリーをはてなブックマークに追加

スポンサーリンク

Objective-C / ARC で unsafe_unretained 利用時のインスタンス破棄タイミングについて

ここ最近のコーディングでは iOS5 SDK と同時に登場した ARC (Auto Reference Counting) を利用しています。

ARC の導入と注意点については、iOS 開発ブログ Natsu's note さんの一連の記事がとても参考になります。

C++ with boost に慣れた人なら、ARC はスマートポインタ (SmartPtr, AutoPtr) に近い動作をするという認識でよいと思います。

代入 (アサイン) 動作で参照カウンタが加算 (インクリメント) され、スコープを抜ける段階で release が呼ばるようなイメージで動作します。参照カウンタが 0 になった時点でオブジェクトが破棄されるのはこれまでの retain / release を使った従来の手法 MRC (Manual Retain-Release) と同様です。

明示的に retain / release を呼び出す必要がなくなり、巡回参照さえ気をつければ、メモリリークも軽減できる ARC はとても便利です。

そんな便利な ARC ですが、unsafe_unretained / weak 関連で不思議な動作をするのに気づいたので、忘れないようにメモしておきます。

Delegate 時の参照

[iOS5] ARC (Automatic Reference Counting) : Overview で触れられている通り、Cocoa プログラミングで多用される Delegate パターンでは「弱い参照 (weak reference)」を使って巡回参照によるメモリリークを防ぎます。

ただし、weak 修飾子は iOS5 以降でしか利用できません。iOS4 でも動作するアプリケーションにするためには、weak 修飾子を利用するところで unsafe_unretained 修飾子を使う必要があります。

unsafe_unretained 修飾子を使った場合、MRR で assign を利用したときと同様にアサインしたオブジェクトを適切なタイミングで nil 化してあげる必要があります。

しかしながら、unsafe_unretained 修飾子を利用する場合、ある条件下でオブジェクトが不用意に解放されてしまう場合があります。この場合、適切と思われる箇所で nil 化していても EXC_BAD_ACCESS の要因になりうるので注意が必要です。

多段 Delegate

さて、その「ある条件」について考察していきます。

以下は私が見つけたパターンですが、他にも同様なケースがあるかもしれません。

  1. ある Object A が Object B を作成し、その Delegate として動作します。

  2. Object B は内部で Object C を作成していて、その Delegate として動作していて、Object C からの通知を起点に Object B から Object A にメッセージが送信されます。

  3. Object A は Object B の Delegate メッセージを受け取り次第、Object B を利用しないので、Object B を破棄します。

文章にすると若干ややこしい感じがしますが、例えば、

ネットワーク越しのデータを処理するあるクラス (Object B) は内部で NSURLConnection (Object C) を利用していて、データ受信終了を起点に Object B でデータ処理を行い、Object A に通知する

というケースなどが該当します。

[図]多段 Delegate の基本パターンダイアグラム

コードで表現すると、

@class SampleClassA;
@class SampleClassB;
@class SampleClassC;

@protocol SampleDelegateB <NSObject>

- (void)callFromClassB:(SampleClassB *)object;

@end

@protocol SampleDelegateC <NSObject>

- (void)callFromClassC:(SampleClassC *)object;

@end

#pragma mark -

@interface SampleClassA : NSObject <SampleDelegateB>

@end

@interface SampleClassB : NSObject <SampleDelegateC>

@property (nonatomic, unsafe_unretained) id<SampleDelegateB> delegate;

@end

@interface SampleClassC : NSObject

@property (nonatomic, unsafe_unretained) id<SampleDelegateC> delegate;

@end

というような形で、SampleClassB では SampleDelegateC のデリゲートメソッド callFromClassC: 内で SampleDelegateB デリゲートメッセージを通知します。

- (void)callFromClassC:(SampleClassC *)object
{
  NSLog(@"  -> started  [%p] with '%@'",self,self.message);
  [self.delegate callFromClassB:self];
  NSLog(@"  -> endded   [%p] with '%@'",self,self.message);
}

つまるところ、オブジェクト C からのメッセージをフォワードするような感じのイメージ。

多段 Delegate のワナ

この形だけでは特になんてことはないのですが、

Object A は Object B の Delegate メッセージを受け取り次第、Object B を利用しないので、Object B を破棄します。

という条件が加わると、オブジェクトが破棄されるタイミングが実装によって変わってきます。

  • weak 参照を利用している

  • unsafe_unretained 参照を使っていて、プロパティ (メソッド) を通してデリゲートにアクセスしている

  • unsafe_unretained 参照を使っていて、メンバ変数でデリゲートにアクセスしている

オブジェクトが破棄されるタイミングが weak 参照をつかっているか、unsafe_unretained 参照を使っているか……さらにはプロパティを使っているかなどの条件で変わってきます。

サンプルコードを github に上げたので、それを元に挙動を確認することができます。

以下の実行結果は、Xcode 4.3 / iOS5 SDK を利用して行なったものです。

weak 参照を利用

SBSampleDelegate.h において

  • kUsingWeak 1
  • kAsseccingViaProperty 0
  • kUsingRetainAutoreleaseHack 0

のように設定した場合。

[A]===> initialized [0x6a67830]
[C]  -> initialized [0xXXXXXXX]
[B]  -> initialized [0xXXXXXXX] with 'msg0'
[C]  -> started     [0xXXXXXXX]
[B]  -> started     [0xXXXXXXX] with 'msg0'
[A]===> called      [0xXXXXXXX]
[A]===> removed     [0xXXXXXXX]
[B]  -> endded      [0xXXXXXXX] with 'msg0'
[C]  -> ended       [0xXXXXXXX]
[B]  -> destroyed   [0xXXXXXXX] with 'msg0'
[C]  -> destroyed   [0xXXXXXXX]

C → B と作成され、逆に B → C と破棄されています。期待通りの動作と言えそうです。

unsafe_unretained 参照を使っていて、プロパティ (メソッド) を通してデリゲートにアクセス

SBSampleDelegate.h において

  • kUsingWeak 0
  • kAsseccingViaProperty 1
  • kUsingRetainAutoreleaseHack 0

のように設定した場合。

[A]===> initialized [0xXXXXXXX]
[C]  -> initialized [0xXXXXXXX]
[B]  -> initialized [0xXXXXXXX] with 'msg0'
[C]  -> started     [0xXXXXXXX]
[B]  -> started     [0xXXXXXXX] with 'msg0'
[A]===> called      [0xXXXXXXX]
[A]===> removed     [0xXXXXXXX]
[B]  -> endded      [0xXXXXXXX] with 'msg0'
[B]  -> destroyed   [0xXXXXXXX] with 'msg0'
[C]  -> ended       [0xXXXXXXX]
[C]  -> destroyed   [0xXXXXXXX]

C → B と作成され、逆に B → C と破棄されていますが、分かるでしょうか、[B] -> destroyed[C] -> ended の呼び出される順番が逆になっています。

SampleClassC からデリゲート B を呼び出しているメソッド callDelegate のスコープにまだいる最中に SampleClassC のデリゲートである SampleClassB のインスタンスが破棄されています。

これですぐに問題になるケースはぱっと思いつきませんが、ちょっとだけ不安な感じ。

unsafe_unretained 参照を使っていて、メンバ変数でデリゲートにアクセス

SBSampleDelegate.h において

  • kUsingWeak 0
  • kAsseccingViaProperty 0
  • kUsingRetainAutoreleaseHack 0

のように設定した場合。

[A]===> initialized [0xXXXXXXX]
[C]  -> initialized [0xXXXXXXX]
[B]  -> initialized [0xXXXXXXX] with 'msg0'
[C]  -> started     [0xXXXXXXX]
[B]  -> started     [0xXXXXXXX] with 'msg0'
[A]===> called      [0xXXXXXXX]
[A]===> removed     [0xXXXXXXX]
[B]  -> destroyed   [0xXXXXXXX] with 'msg0'
[B]  -> endded      [0xXXXXXXX] with '(null)'
[C]  -> ended       [0xXXXXXXX]
[C]  -> destroyed   [0xXXXXXXX]

明らかにおかしい挙動になります。

まだ C からのデリゲート callFromClassC: のスコープにいるのにも関わらず、dealloc が呼び出されています。

SampleClassB のインスタンスが破棄された後、callFromClassC: のメンバである mMessage にアクセスしているので、(null) と表示されています。場合によっては EXC_BAD_ACCESS の要因になります。

retain-autorelease ハック

という訳で unsafe_unretained 参照を利用すると、weak 参照とは異なった挙動になってしまいます。

これは MRR で assign を利用した場合でも起こりうる現象です。MRR ではこれに対して、

[[obj retain] autorelease]

retain と autorelease を併用することで、当該オブジェクトを autorelease プールに登録して、少なくともスコープ内はオブジェクトが破棄されないようにすることができました。

ご承知の通り、ARC では retain / autorelease メソッドの呼び出し自体が禁止されているので、同じハックは利用できません。

ただ、ローカルな変数に対しては、__autorelesing 修飾子を利用することによって autorelease プールを利用することを明示できます。

__autorelasing id temp = obj;

これでスコープ内では temp を利用すると、インスタンス obj に対して retain-autorelease で呼び出したのと同じような効果が得られます。

SBSampleDelegate.h において

  • kUsingWeak 0
  • kAsseccingViaProperty 0
  • kUsingRetainAutoreleaseHack 1

のように設定した場合、SampleClassA において retain-autorelease ハックを利用します。

[A]===> initialized [0xXXXXXXX]
[C]  -> initialized [0xXXXXXXX]
[B]  -> initialized [0xXXXXXXX] with 'msg0'
[C]  -> started     [0xXXXXXXX]
[B]  -> started     [0xXXXXXXX] with 'msg0'
[A]===> called      [0xXXXXXXX]
[A]===> removed     [0xXXXXXXX]
[B]  -> endded      [0xXXXXXXX] with 'msg0'
[C]  -> ended       [0xXXXXXXX]
[B]  -> destroyed   [0xXXXXXXX] with 'msg0'
[C]  -> destroyed   [0xXXXXXXX]

weak 参照と同様に C → B と作成され、逆に B → C と破棄されています。

ログでは同じように見えていますが、こちらの実装では、B は autorelease プールのタイミングで破棄されているので、内部的には異なる挙動になっています。ただ、不用意な破棄がなくなり、アクセスバイオレーションはなくなります。

このエントリーをはてなブックマークに追加

スポンサーリンク

SBPullToRefreshHeaderView - 「引っ張って更新」を再実装してみる

HMDT さんから開発者向け電子書籍がリリースされています ([本] HMDT JOURNAL 創刊 | Cocoaの日々情報局 経由)。

早速、Vol.001 と Vol.002 を購入してみました。木下さんらしい読みやすい文体で、かなり丁寧に書かれています。

……特定の連載記事だけをバラで購入できたら嬉しいなぁ……なんて。ともあれ、今後の展開が楽しみです。

EGOTableViewPullRefresh

さて、この HMDT JOURNAL Vol.001 で紹介されている EGOTableViewPullRefresh は、いわゆる「引っ張って更新」を実現するクラスで、Facebook 謹製のライブラリでも採用されていたりする有名なライブラリです。

組み込みも簡単で、コードも分かりやすく記述されているライブラリなのですが、導入にあたってコントローラ側で UIScrollViewDelegate のメッセージをフォワードする必要がある点がちょっと不満。

例えば、UIButton ではボタンのハイライトのためにタッチイベントをコントローラ側からフォワードしなくても、ボタン側で勝手に処理してくれるように、EGOTableViewPullRefresh もスクロールイベントを勝手に処理してくれたらなぁと思うわけです。

SBPullToRefreshHeaderView - yet another "pull-to-refresh"

……そこで自分で「引っ張って更新」するクラス SBPullToRefreshHeaderView を自作してみました。

[図] SBPullToRefreshHeaderView の実装例

ARC 対応で __weak 修飾子を使っているので、iOS5 以降でお使いいただけます。

他に EGOTableViewPullRefresh から微妙に変わっている部分に「離して更新」の時にリロード用画像を使っています。OS X 向け Twitter アプリケーションのような感じ。

サンプルデモのコードにある通り、コントローラ側では SBPullToRefreshHeaderView の初期化メソッドにターゲットとなるスクロールビューを渡すと、あとは SBPullToRefreshHeaderView 側でよしなにしてくれます……はずです。

mRefreshHeaderView = [[SBPullToRefreshHeaderView alloc] initOnScrollView:self.tableView
                                                            withDelegate:self];

デリゲート SBPullToRefreshHeaderViewDelegate のメソッドはふたつ。

@protocol SBPullToRefreshHeaderViewDelegate <NSObject>

/// 「引っ張り更新」の発動 (ユーザが更新するためにスクロールビューを離した) を通知します。
/// @param headerView - 関連する SBPullToRefreshHeaderView のインスタンス
- (void)didTriggerRefresh:(SBPullToRefreshHeaderView *)headerView;

/// ターゲットが「ローディング」状態がどうかを確認します。
/// @param headerView - 関連する SBPullToRefreshHeaderView のインスタンス
/// @return ローディング状態にある場合、YES をそうでない場合 NO を返します
- (BOOL)isRefreshStillProcessing:(SBPullToRefreshHeaderView *)headerView;

@end

コントローラ側では基本的にはこのふたつのデリゲートメソッドを実装して、ローディングが終了した後に「resetView:」メソッドを送信します。

SBPullToRefreshHeaderView の実装

SBPullToRefreshHeaderView ではスクロールイベントを処理するために KVO (Key-Value Observing) を使っています。

スクロールが発生すると、UIScrollViewcontentOffset が変化することに着目して、そこでスクロール時に処理する内容を記述しています。

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
  if (object == mScrollView &&
      [keyPath isEqual:@"contentOffset"])
  {
    [self scrollViewDidScroll:mScrollView];
    if (mIsDragging != mScrollView.isDragging)
    {
      if (!mScrollView.isDragging)
      {
        [self stopDragging];
      }
      mIsDragging = mScrollView.isDragging;
    }
  }
}

ユーザーがターゲットのスクロールビューから指を離したかどうかの判定もここで行っています。スクロールの変化で判定しているので、実際のタッチリリースとは異なるタイミングになりますが、動作させてみた感じ特に違和はない感じ。

オブザーブを開始・終了するタイミングが少しハック的な実装になっています。

……というのもオブザーブを開始した時点で参照カウンタがインクリメントされてしまうため、どこか適当なタイミングでオブザーブを終了してあげないといつまで経ってもオブジェクトが解放されなくなってしまいます。

SBPullToRefreshHeaderView の実装では、ターゲットとなっているスクロールビューの子ビューとして追加されたときにオブザーブを開始、親ビューがターゲットから外れた時点でオブザーブを終了するようにします。

- (void)didMoveToSuperview
{
  if (self.superview == mScrollView)
  { // The view has been added into the target scroll view so it starts 
    // observing contentOffset changes.
    [mScrollView addObserver:self 
                  forKeyPath:@"contentOffset" 
                     options:NSKeyValueObservingOptionNew 
                     context:nil];
  }
  else
  { // The view has been removed from the target scroll view so it stops 
    // observing contentOffset changes.
    [mScrollView removeObserver:self
                     forKeyPath:@"contentOffset"];
  }
}

UIView の 「didMoveToSuperview」メソッドで親ビューの変化をキャッチできるので、そこでオブザーブの開始・終了を制御しています。

GitHub にサンプルデモと一緒に置いてみましたので、もしよろしければお使いください (MIT-License)。

このエントリーをはてなブックマークに追加

スポンサーリンク

Xcode 4.3 リリース

二月になってしまいましたが、2012 年もよろしくお願いします。

Xcode 4.3 がリリースされました。

Mac App Store との親和性を高めるため、Xcode 4.3 からはインストール先が変更されています。

  • Xcode 4.2 まで : /Developer
  • Xcode 4.3 から : /Applications

インストール方法も変わりました。Xcode 4.2 までは何らかの方法で Xcode のインストーラを起動する必要がありました。

Xcode 4.2 では Mac App Store からダウンロードされるものは「Xcode Install」で Xcode そのものではありませんでした。

Xcode 4.3 からはインストーラは目に見えては存在しません。App Store からダウンロードして、そのまま他のアプリケーションと同じように起動するだけです。

Xcode 4.3 はインストールされる場所が最大の違いで、機能的な違いはほとんどない印象です。

Xcode に付録されるユーティリティ

Xcode 4.2 まで /Developer/Applications の中にあった Instruments や Icon Composer, FileMerge といったユーティリティは Xcode 4.3 から Xcode アプリケーション内に内包されています。

Dashcode, Quatz Compozer などこれまで Xcode に付録 (?) として付いてきたいくつかのアプリケーションは Xcode 4.3 には含まれていません。

developer.apple.comダウンロードセンターから別途ダウンロードする必要があります。

  • Dashcode → Dashcode for Xcode - February 2012
  • Quartz Composer → Graphics Tools for Xcode - February 2012

Instruments など Xcode 4.3 に内包されているアプリケーションを起動するには Xcode を立ち上げて、Xcode メニューより「Open Developer Tool」を利用します。

[図] デベロッパツールメニュー。

Xcode に内包されているアプリケーションでも起動させた状態で Dock に保持するようにすれば、Dock から (Xcode を通さずに) 立ち上げることができます。Xcode に内包されているアプリケーションは Spotlight では拾ってくれないようです。

コマンドラインツール

Xcode 4.3 にはもちろんコンパイラは付いてきますが、Xcode 4.2 まで /Developer/usr/bin に含まれていたいくつかのツールは含まれていません。前述のダウンロードセンターからダウンロードするか、設定画面の「Downloads」より「Command Line Tools」をダウンロードします。

[図] Xcode 4.3 設定内のダウンロード。

Xcode 4.3 からの仕様変更により OS X のパッケージマネージャ Homebrew は Developer Preview の時には動作しなくなってしまいましたが、Command Line Tools をちゃんと入れれば、大丈夫な様子。

以前の Xcode との共存

Xcode 4.3 はこれまでの Xcode と同様、以前のバージョンの Xcode と共存が可能です。一番最初に Xcode 4.3 を立ち上げたときにインストールされている Xcode をゴミ箱に移動するかどうか聞かれます。

[図] Xcode 4.3 最初の起動時に表示されるダイアログ。

該当するディレクトリを単にゴミ箱に移すだけのようで、/Developer/Library/uninstall-developer-folder は実行されない様子。

ヘルプでは「(古い Xcode は) 後からでもいつでもゴミ箱に入れられます」とあって、uninstall-developer-folder の実行には触れられていません。ゴミ箱に移して削除すればアンインストールされるんでしょうか。

このエントリーをはてなブックマークに追加

スポンサーリンク

2/24