
iPhoneアプリ開発者なら一度はCoverflowを実装してみたいと思うはず。
coverflowの実現方法は、以前からエリカ様の書籍にて公開されていましたが、undocumentedなAPIを使っているため、おそらくあの方法では申請したところでAppleに蹴られて終わりでしょう。最近はundocumented API検出ツールのようなものをAppleが使って審査してるという話もあるので、審査の目は前よりも厳しくなってる気がします。
となると、自力で実装していくしかないのですが、ちょうどよいライブラリがあったので、早速試してみる。
続きを読む



新作でTwitter,はてな,Google Readerから横断的に情報収集し、マルチポストするアプリを作っているのですが、そこでTwitterのリプライPush機能を実装したので、Push Notificationを実装する方法をまとめてみます。
Push Notificationの流れ
Push Notificationに関する登場人物は、- iPhone
- Apple Push Notification Service(APNs)
- Provider
iPhoneはいわずもがな、みんなの手元にあるiPhone。APNsはAppleが用意しているPushしてくれるやつです。Providerは、開発者が用意するもので、こいつがPushしたい情報を送る役割を持ちます。
この3者間でのデータの流れは大きくデバイス登録とPush通知の2つのフェーズに分かれます。
デバイス登録
デバイス登録フェーズではAPNsへPush通知するiPhoneを登録します。このときAPNsから一意なデバイストークンが発行され、これをProviderが保持することで、Push通知を送りたいiPhoneをProviderが特定できるようになります。流れはこんな感じ。
- iPhoneにアプリをインストール
- アプリ起動時にiPhoneからAPNsへデバイスの認証通知をする
- APNsから認証されると、デバイストークンを受け取る
- iPhoneからデバイストークンをProviderへ送る
Push通知
Push通知フェーズでは、ProviderからAPNsへPush情報と共に認証情報を送ります。APNsから認証されると、送信先iPhone宛にPush通知が送られ、晴れてiPhone上であのAlertが表示されるようになります。流れはこんな感じ。
- 任意のタイミングでProviderからPush情報と認証情報をAPNsへ送る
- APNsから認証されるとiPhoneへPush通知が送られる
- iPhone上でポップアップ!
鍵を作る
デバイス登録手順1のアプリインストールは飛ばすとして、2のiPhoneからAPNsへデバイス認証通知をするためには、鍵が必要です。なのでApple Developer Connection – iPhone Dev Center – Overviewへ行って鍵を作りましょう。
手順はこんな感じです。
- 左メニューApp IDsからNew App ID
- Descriptionには自分のわかりやすい説明を
- Bundle Seed IDはGenerate NewでOK
- Bundle Identiferは*は使えません。net.longearth.earth等、一意な値となるようにドメイン名を逆から綴ったものにしておく
- 以上を入力してsubmit
- App IDsのManage画面で登録したApp IDのConfigureリンクを押す
- ひとまず開発用の設定をするため、Development Push SSL CertificateのConfigureを押す
- Lightbox風に鍵作れといわれるのでキーチェインを起動
- キーチェーンアクセス→証明書アシスタント→認証局に証明書を要求…
- ユーザのメールアドレスは登録したメールアドレスを
- コモンネームは他の証明書の時と同じ名前を
- ディスクに保存をチェック→続ける→保存
- ブラウザに戻りContinueを押す
- さっき作った証明書を選択してGenerate
- 認証されたらContinue
- Downloadなうして、Done
- Lightbox風が終わるので画面上でステータスがenabledになっていることを確認
- ダウンロードした証明書をダブルクリックしてキーチェーンに保存
- キーチェーンの「自分の証明書」分類にあるApple Development Push Servicesを選択して、メニュー→ファイル→書き出す…→保存(apns-dev-cert.p12)
- 同様にキーチェーン画面からApple Development Push Services横の三角を開き、秘密鍵を選択→メニュー→ファイル→書き出す…→保存(apns-dev-key.p12)
ここまでがApple上のドキュメントに書いてあるのですが、p12ファイルとやらをどう使えばよいのかよくわからないので、pemフォーマットに変換(正直この部分はわかってない。男は黙って以下のコマンド群を打ち込む。)
- openssl pkcs12 -clcerts -nokeys -out apns-dev-cert.pem -in apns-dev-cert.p12
-
openssl pkcs12 -nocerts -out apns-dev-key.pem -in apns-dev-key.p12
- openssl rsa -in apns-dev-key.pem -out apns-dev-key-noenc.pem
-
cat apns-dev-cert.pem apns-dev-key-noenc.pem > apns-dev.pem
鍵ができたら、ここで作成したApp IDを指定したProvisioningファイルを作成して、Xcodeにインストール。code signに設定しておくことも忘れずに。
デバイス認証通知
認証通知は簡単です。UIApplicationのregisterForRemoteNotificationTypes:で通知処理を設定します。これだけでcode signされた内容でデバイス認証しに行く様子。アプリ起動直後あたりに仕込んでおきます。ちなみにここでPush時にバッヂ表示させるか、音をならすか、アラート画面を出すかを選択できます。以下はすべてを通知させるような例。- (void)applicationDidFinishLaunching:(UIApplication *)application {
[[UIApplication sharedApplication]
registerForRemoteNotificationTypes:
(UIRemoteNotificationTypeBadge|
UIRemoteNotificationTypeSound|
UIRemoteNotificationTypeAlert)];
// 他の処理...
} |
デバイストークンを受け取る
APNsから認証されるとデバイストークンを受け取ることができます。認証後はUIApplicationのapplication:didRegisterForRemoteNotificationsWithDeviceToken:メソッドが呼ばれるのでここで受け取ります。- (void)application:(UIApplication*)app
didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)devToken{
NSLog(@"deviceToken: %@", devToken);
self.devToken;
[self sendProviderDeviceToken:devToken];
} |
sendProviderDeviceToken:メソッド内でProviderへデバイストークンを送る処理をしています。NSLogで送られてきたdevTokenをログ出力してますが、このとき表示される32バイトの値がデバイストークンです。後のproviderでのテストのために控えておきます。
認証エラー時は以下のメソッドが呼ばれます。
- (void)application:(UIApplication*)app
didFailToRegisterForRemoteNotificationsWithError:(NSError*)err{
NSLog(@"Errorinregistration.Error:%@",err);
} |
code signが間違っている等、分かりやすい日本語で表示してくれているので助かります。
Providerへデバイストークンを送る
ここは好きなようにProvider宛にデバイストークンをPostしてあげてください。ちなみにこんな感じでポストできるはずです。
- (void)sendProviderDeviceToken:(NSData *)token {
NSMutableData *data = [NSMutableData data];
[data appendData:[@"device=" dataUsingEncoding:NSUTF8StringEncoding]];
[data appendData:token];
self.request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"ホスト名"]];
[request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
[request setHTTPMethod:@"POST"];
[request setHTTPBody:data];
[NSURLConnection connectionWithRequest:request delegate:self];
} |
サーバ側では、このデバイストークンを控えておきます。実用的な所でいうと、このデバイストークンと何かしらのユーザを識別するIDをサーバへ送ることになると思います。
ここまでで、デバイスの登録が完了です。
ProviderからAPNsへPush通知を送る
ProviderからAPNsへの通知は以下のようなフォーマットで送る必要があります。
始めの3バイトは固定で、0,0,32。16進では0×00,0×00,0×20です。その後デバイストークンと続き、0×00,送信データサイズ、送信データという流れです。
このあたりはバイナリにまとめたものをファイルに書き出し、ターミナルから
xxd ファイル名
として中身を確認することをおすすめします。アプリ側のNSLogで出力したデバイストークンは16進表記なので、その値がきれいに上のデバイストークンの場所に収まっていればだいじょぶだと思います。
送信処理をrubyで書くとこんな感じです。以下のスクリプトを実行する時は、作成した証明書と秘密鍵を同じディレクトリに置いておいてください。
#!/usr/bin/ruby require 'openssl' require 'socket' device = ['デバイストークンのバイナリ値'] socket = TCPSocket.new('gateway.sandbox.push.apple.com',2195) context = OpenSSL::SSL::SSLContext.new('SSLv3') context.cert = OpenSSL::X509::Certificate.new(File.read('apns-dev.pem')) context.key = OpenSSL::PKey::RSA.new(File.read('apns-dev-key-noenc.pem')) ssl = OpenSSL::SSL::SSLSocket.new(socket, context) ssl.connect payload = <<-EOS { "aps":{ "alert":"New Message!", "badge":1, "sound":"default" } } EOS (message = []) << ['0'].pack('H') << [32].pack('n') << device.pack('H*') << [payload.size].pack('n') << payload ssl.write(message.join('')) ssl.close socket.close |
上の例では、Alert表示に”New Message!”を出して、アプリアイコンバッヂに1を表示させ、デフォルトの音をならすような通知設定をしています。設定方法の詳細はApple Push Notificationサービス プログラミングガイドにあります。
証明書と鍵さえ正しいものを使っていれば、ローカルのmacからでもPushすることができるので、ローカルでごにょごにょ確認するのがいいと思います。ここまでくれば、送る相手のデバイストークンさえ分かればPushできるので、登録されたデバイストークンを全取得する方法を用意しておけば、ローカルから最新情報告知なんていう使い方もできそうです。
特に気の知れた相手のデバイストークンが分かっていれば、「うんこ」なんて通知もできてしまいそうです。もちろん自重します。絶対。
これから
あとはアプリの任意のタイミングで上記のように送りたい相手のデバイストークンを取得して、メッセージを送信することでPushすることができます。麻雀アプリなら上家がパイを切った後に、Providerへ通知させて、次の相手のデバイストークンを引っ張ってきてPushという形になるでしょう。
Twitterのリプライ通知をどのようにしようかすごく悩んでいたのですが、Stream APIが使えそうです。
Twitter API Wiki / Streaming API Documentation
試してみた所、まだアルファテスト版ということでだいぶ不安定な気もします。Twit Pushとかどうやっているのか気になります。
長いことおつかれさまでした。
しかし、めんどくさい。
もうちょっと楽にできないかな。
参考
後で気づいたけど日本語のドキュメントがあった。iPhone Dev Center
cocoa*life – Apple Push Notification Serviceを利用した、iPhone クライアントと、Rubyによるサーバの作成。
How to build an Apple Push Notification provider server (tutorial) « Boxed Ice Blog
daichi1128’s DCAtomPub at master – GitHub
はてなブックマークの登録をiPhoneアプリから行うにははてなのAtomPub APIを叩く必要がありますが、WSSE認証やらsha1やらbase64エンコードやらISO8061やらXMLポストやらでハマリ倒してしまったので、二度とつまずかないためにもライブラリ化してみた。
ライブラリというほど大げさなものでもないのですが、ソースコード公開デビューということで大目にみてやってください。
Objective-CでAtomPubを簡単に操作するライブラリDCAtomPubをgithubに公開しました。
ライセンスは修正BSDライセンスです。
AtomPubに関する情報はこちらから。
日本語訳はこれ。ソフトウェア分野の研究開発 / RFC 5023 Atom Publishing Protocol 日本語訳 | Ricoh Japan
はてなブックマークAPIはこちらから。
はてなブックマークAtomAPIとは – はてなキーワード
セットアップ
ターミナルから以下のコマンドでダウンロード(要gitクライアント)git clone git://github.com/daichi1128/DCAtomPub.git |
githubからダウンロードしたら、Xcodeで起動し、DCAtomPubグループ内のファイル群を自分のプロジェクトにコピー。はてな用クライアントも必要ならHatenaExample内のファイルもコピー。
使い方
WSSE認証
DCAtomPubClinetの初期化メソッドの引数にユーザ名、パスワードを渡してあげれば面倒なWSSE認証の準備は勝手にDCAtomPubClientがやります。NSString *username = @"user"; NSString *password = @"pass"; DCAtomPubClient *atomClient = [[DCAtomPubClient alloc] initWithUsername:username password:password]; |
正確にはリクエストを投げるまでにユーザ名、パスワードがセットされていればリクエスト送信時にうまいことやってくれます。こんな形でもよいです。
DCAtomPubClient *atomClient = [[DCAtomPubClient alloc] init]; atomClient.username = @"user"; atomClient.password = @"pass"; |
POST
AtomPubのPOSTメソッドはDCAtomPubClientクラスのpost:XMLString:メソッドで行います。はてぶでは登録時にPOSTメソッドでXMLを投げます。
サンプルコードはこんな感じです。
NSString *username = @"user";
NSString *password = @"pass";
/**
*AtomPubクライアントではてなブックマークを追加
*/
DCAtomPubClient *atomClient = [[DCAtomPubClient alloc] initWithUsername:username password:password];
atomClient.delegate = [[DummyDelegate alloc] init];
NSString *hatenaPostURL = @"http://b.hatena.ne.jp/atom/post";
NSString *postXMLTemplate =
@"<entry xmlns=\"http://purl.org/atom/ns#\">"
@"<link rel=\"related\" type=\"text/html\" href=\"%@\" />"
@"<summary type=\"text/plain\">%@</summary>"
@"</entry>";
// POSTメソッドでXMLを送信
[atomClient post:hatenaPostURL XMLString:[NSString stringWithFormat:
postXMLTemplate,
@"http://iphone.longearth.net/itasktimer/",@"DCAtomPubから" ]]; |
PUT
はてなブックマークの編集時にAtomPubのPUTメソッドを使います。使い方はPOST時と同じように、DCAtomPubClientのput:XMLString:を呼べばOKです。DELETE
はてなブックマークの削除時にAtomPubのDELETEメソッドを使います。使い方はPOST、PUTと同じです。DCAtomPubClientのdelete:XMLStringを呼びます。はてなブックマークの場合はXMLStringはnilでOKです。はてなクライアントの使い方
AtomPubの扱いは上のDCAtomPubClientでだいぶ簡略化できたけど、いちいちXMLを調べて組み立てるのが面倒だ。なのではてな用AtomPubクライアントも作りました。DCHatenaClient。DCAtomPubClientを継承してます。
ブックマークを追加する
DCHatenaClientを使ってブックマークを登録するには、DCHatenaClientの- (void)post:(NSString *)bookmarkURL comment:(NSString *)comment;
メソッドで行います。
引数にはブックマークするURLとコメントを渡してあげます。
サンプルはこんな感じです。
/** * はてなブックマーク用クライアントではてなブックマークを追加 */ DCHatenaClient *hatenaClient = [[DCHatenaClient alloc] initWithUsername:username password:password]; [hatenaClient post:@"http://iphone.longearth.net" comment:@"DCHatenaClientから"]; |
だいぶすっきりします。
ブックマークを編集する
ブックマーク編集は- (void)edit:(NSString *)eid title:(NSString *)title comment:(NSString *)comment;
で行います。
引数に編集するエントリID、変更後のタイトル、変更後のコメントを渡します。はてぶAPI的にタイトルとコメントいずれか必須らしいのでそうしてください。変更したくないものはnilを渡します。
DCHatenaClient *hatenaClient = [[DCHatenaClient alloc] initWithUsername:username password:password]; [hatenaClient edit:@"15151515" title:@"変更後のタイトル" comment:nil]; |
すっきりですね。
ブックマークを削除する
ブックマーク削除は- (void)delete:(NSString *)eid;
で行います。
DCHatenaClient *hatenaClient = [[DCHatenaClient alloc] initWithUsername:username password:password]; [hatenaClient delete:@"15151515"]; |
もうAtomPub恐るるに足らずです。
その他
デリゲートとしてDCAtomPubDelegateを用意しています。呼べるものはNSURLConnection系を周到してます。こんな感じです。- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response; - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data; - (void)connectionDidFinishLoading:(NSURLConnection *)connection data:(NSData *)data; - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error; |
リクエストを投げる前に
dcAtomPubClinet.delegate = self;
などとしてセットしてあげればNSURLConnectionと同じ感覚で使えるはずです。
mixiの隠しAPIもAtomPubらしいので、うまくいけば使えるかもしれません。まだ試してない。
※本来ならAtomPubのルートエンドポイントからアクセス可能なURLを引っ張ってくるのがAtomPub的に正しいことらしいのですが、面倒なので今回はそこまでしてません。
参考
はてなブックマークAtomAPIとは – はてなキーワードCocoaでWSSE認証 – 24/7 twenty-four seven
cocoa_crypto_hashing: Summary
NSDataにBase64のエンコード・デコード機能を追加する
Web APIを使うiPhoneアプリにはXMLのパースは必要不可欠。アプリ開発者のみなさんはXMLのパースはどのようにしているのでしょう。NSXMLParser、libxml、TouchXML、KissXMLいろいろ方法はありますが、今回使ってみて結構使い勝手がよかったKissXMLの使い方を書いてみます。
続きを読む
いろいろカスタマイズして便利にします。
外観
デバッグコンソールなどを1つのウィンドウに統合する
シミュレータでアプリを起動すると、デバッガコンソールがXcodeの後ろに隠れてしまい、いちいちフォーカスを切り替えるのが面倒。そんなときはこの設定を。Xcode→環境設定→全般→レイアウト→オールインワン
アプリ実行時にデバッガを自動的に表示する
Xcode→環境設定→デバッグ→開始時→コンソールとデバッガを表示__MY_COMPANYNAME__を変更する
ファイルを新規に作成する時にファイル作成者の情報等が自動生成されるが__MY_COMPANYNAME__が気持ち悪い。そんな時は、コンソールから以下のコマンドを叩く。YourNameHereを表示したい名前に置き換えて。defaults write com.apple.Xcode PBXCustomTemplateMacroDefinitions
'{ORGANIZATIONNAME="YourNameHere";}' |
プリプロセッサ
デバッグ用マクロ
デバッグ時はNSLogメソッドをよく使うが、リリースビルドにはNSLogを使いたくない。そんな時はプリプロセッサでDebugモードの時だけ、NSLogを吐くマクロを定義すればいい。Global.h
#ifdef DEBUG #define LOG(...) NSLog(__VA_ARGS__) #else #define LOG(...) #endif |
info.plistでDebug構成時のみ、GCC_PREPROCESSOR_DEFINITIONS項目にDEBUG文字列を設定する。
すると、LOG(@”hogehoge”)でDebug構成時のみ、NSLogを吐けるようになる。
テンプレート
IBを使わないテンプレートを作成する
Interface Builderは最近は使わなくなったので、新しいプロジェクトを作る場合に、いちいち削除したりするのが面倒。そんな時はプロジェクトテンプレートをカスタマイズする。Xcodeのプロジェクトテンプレートは、以下の場所においてある。
/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/Project Templates/Application
ユーザ定義のテンプレートは以下の場所に置くとXcodeに認識される。
~/Library/Application Support/Developer/Shared/Xcode/Project Templates/Application/
Windowベースのアプリケーションをベースにカスタマイズする。オリジナルのWindow-based Applicationをユーザ定義の方にコピーしてWindow-based-non-IB Applicationにリネーム。
ディレクトリ内の___PROJECTNAME___はすべてプロジェクト名に置き換えられる。
変更するのは以下。
- info.plistのMain nib file base name→空に
- main.m内のUIApplicationMainメソッドの第4匹数に@”___PROJECTNAME___AppDelegateを”
- MainWindow.xib→削除
- ___PROJECTNAME___.xcodeproj/project.pbxproj→MainWindow.xibに関する部分をすべて削除
個人的に___PROJECTNAME___AppDelegateという名前が長ったらしくてあまり好きではないので、これをAppDelegateにした。
この時の変更箇所は以下。
- main.m→@”___PROJECT_NAME___AppDelegate”を@”AppDelegate”へ
- ___PROJECT_NAME___AppDelegate.h→ファイル名をAppDelegate.hへ。中身も該当箇所を修正
- ___PROJECT_NAME___AppDelegate.m→ファイル名をAppDelegate.mへ。中身も該当箇所を修正
- ___PROJECT_NAME___.xcodeproj/project.pbxproj→___PROJECT_NAME___AppDelegate部分をすべてAppDelegateへ修正
.gitignore、.gitattributeをテンプレートに含める
gitを使いだすとgitの設定ファイルである.gitignoreでXcode用ファイル達を除外したくなるが、これを毎回プロジェクトを作る度にコピーしてくるのは面倒なので、テンプレートに含めてしまう。やり方は上のテンプレートディレクトリに.gitignore、.gitattributeファイルを入れるだけ。.gitignoreは
# xcode noise build/* *.pbxuser *.mode1v3 # old skool .svn # osx noise .DS_Store profile |
.gitattributesは
*.pbxproj -crlf -diff -merge |
ここまでのテンプレート設定ディレクトリをいちおのせておく。これをダウンロードして、ローカルの
~/Library/Application Support/Developer/Shared/Xcode/Project Templates/Application/
に置けば、使えると思う。
Window-based-non-IB Application
これには次のログマクロテンプレートも含まれている。
ログマクロをテンプレートに含める
さきほど作ったログマクロをテンプレートに含めるのは、テンプレートディレクトリ内にログマクロを記述したファイルを追加すればOKだが、ファイルを開発時にファイルを新規作成するたびに、#import “Global.h”
とするのは面倒だ。
なので、ファイル作成時に既に#import “Global.h”を追記されているファイルテンプレートを作成する。
オリジナルのファイルテンプレートは以下に置いてある。
/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/File Templates/Cocoa Touch
このCocoa Touchディレクトリをユーザ定義用ファイルテンプレートディレクトリである
~/Library/Application Support/Developer/Shared/Xcode/File Templates/
へコピー。
あとはファイルを好きなように変更すればそれがテンプレートになる。
Cocoa Touchのすべてのファイル作成時にGlobal.hを含むように設定したファイルテンプレートはこれ。
Cocoa Touch Class
これを
~/Library/Application Support/Developer/Shared/Xcode/File Templates/
へ入れれば、ユーザ定義ファイルとして使える。
よく使うフレームワークをテンプレートに含める
例えば、Three20だったり、JSONフレームワークだったり、GTMだったり、こういうよく使うフレームワークはあらかじめ使える準備の整ったプロジェクトテンプレートが欲しくなる。これも上のやり方同様、コピーしたローカルのテンプレート周りをいじくり倒して設定できる。マクロ
よく使うメソッドをマクロとして登録する
Xcodeでは、Ctrl+.でマクロを呼び出せる。例えばinitと打って、Ctrl+.を押すと、NSObjectのinitメソッドが挿入される。他にもlog、deallocなどが予めマクロとして登録されている。
が、ViewController周りのメソッド達は登録されていないので、いちいちviewWillAppear〜などとよく使うメソッドを入力しなければならない。これは面倒だ。ということでマクロを登録する。
オリジナルのマクロ定義ファイルは以下にある。
/Developer/Applications/Xcode.app/Contents/PlugIns/TextMacros.xctxtmacro/Contents/Resources/ObjectiveC.xctxtmacro
これをユーザ定義のマクロファイル置き場である以下にコピーする。
~/Library/Application Support/Developer/Shared/Xcode/Specifications
マクロとして
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
を追加する場合は、ファイル内に以下のような記述を追加する。
{
Identifier = objc.didselect;
BasedOn = objc;
IsMenuItem = NO;
Name = "didSelectRowAtIndexPath";
TextString = "- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {¥n¥t<#statements#>¥n}";
CompletionPrefix = didselect;
}, |
内容の説明
- Identifier→ユニークな識別ID。
- Name→Xcodeで編集→テキストマクロを挿入→Objective Cと行った時に表示される名称
- TextString→マクロ実行時に挿入される文字列
- CompletionPrefix→マクロ発動文字列。例の場合、didselectと入力してCtrl+.を押すとマクロが実行される
ひとまず
viewWillAppear, viewDidAppear, viewDidLoad, viewWillDisappear, viewDidDisappear, cellForRowAtIndexPath, didSelectRowAtIndexPathなどを登録してみた。なかなか悪くない。
vwaと入力してCtrl+.でviewWillAppear発動。他もvda、vdl、vwd、vdd、cellfor、didselectで発動するのでいい感じ。
この設定済みマクロはこちらから。
ObjectiveC.xctxtmacro
参考
iPhoneアプリで画面遷移させる場合、コントローラをalloc/initしてナビゲーションコントローラのスタックにプッシュしたり、modalしたりする流れになるわけですが、複数の画面から呼ばれる画面だったりするとインスタンスの管理が面倒だったり、決まり文句をいちいち書くのが面倒になります。
そこでThree20のTTNavigatorを使うと、すっきり画面遷移できるようです。
webアプリ感覚で”http://about”のように遷移先を指定するだけで、その画面を表示できるようになります。
httpの部分は任意の文字列を定義できます。
そのためには、まずURLとコントローラのマッピングが必要です。
簡単にサンプルを示します。
#import "DCAppDelegate.h"
#import "DCTwitterController.h"
#import "DCGoogleReaderController.h"
#import "DCAboutController.h"
#import "DCTabBarController.h"
@implementation DCAppDelegate
- (void)applicationDidFinishLaunching:(UIApplication *)application {
TTNavigator *navigator = [TTNavigator navigator];
navigator.supportsShakeToReload = YES;
navigator.persistenceMode = TTNavigatorPersistenceModeTop;
TTURLMap *map = navigator.URLMap;
[map from:@"*" toViewController:[TTWebController class]];
[map from:@"dc://tab" toSharedViewController:[DCTabBarController class]];
[map from:@"dc://twitter" toViewController:[DCTwitterController class]];
[map from:@"dc://reader" toViewController:[DCGoogleReaderController class]];
[map from:@"dc://web" toViewController:[TTWebController class]];
[map from:@"dc://about" toViewController:[DCAboutController class]];
[map from:@"dc://compose?title=(compose:)" toModalViewController:[DCAboutController class]];
[navigator openURL:@"dc://tab" animated:NO];
}
- (BOOL)application:(UIApplication*)application handleOpenURL:(NSURL*)URL {
[[TTNavigator navigator] openURL:URL.absoluteString animated:NO];
return YES;
}
@end |
まずTTNavigatorクラスのインスタンスを取得します。
TTNavigator.supportsShakeToReloadをYESにすると、iPhoneを振ると、リロードをしてくれるようになります。
TTNavigator.persistenceModeはアプリ終了時にどの画面を開いていたかを記憶する設定です。persistenceModeAllを指定すると常に最後に開いていた画面を記憶し、次回起動時にその画面を表示するようになります。
その後のTTURLMapがURLとコントローラのマッピング情報を保持するクラスです。
from:toViewControllerメソッドでは、fromに指定されたURLが呼ばれるとtoViewControllerの画面を表示する、というような設定になります。
fromに”*”を指定すると、マッピングにないURLが指定された場合のコントローラを指定できます。
URLを指定したら、TTNavigatorのopenURL:animated:メソッドを呼べば画面遷移が行われます。
TTNavigatorはシングルトンなため、どの画面からでも同一インスタンスを取得でき、遷移したい時にopenURL:animated:メソッドを呼んでやればよい、という感じです。
TabBarコントローラのような全画面で共通で使いたいコントローラの場合は
from:toSharedViewController:メソッドを使うと一つのインスタンスを使い回せるようです。
また、コントローラに渡すパラメータも指定できます。
[map from:@"dc://compose?subject=(compose:)" toModalViewController:[DCAboutController class]]; |
の部分がそれで、これはDCAboutControllerクラスのcompose:メソッドが呼ばれます。
パラメータの受け取りは以下のように行います。
#import "DCAboutController.h"
@implementation DCAboutController
- (void)createModel {
self.dataSource = [TTSectionedDataSource dataSourceWithObjects:
@"About",
[TTTableTextItem itemWithText:@"サイト" URL:@"http://iphone.longearth.net"],
[TTTableTextItem itemWithText:@"ご意見ご要望" URL:@"dc://compose?subject=%5biSlot%20Pro%5d%e6%84%8f%e8%a6%8b%2f%e8%a6%81%e6%9c%9b"],
[TTTableTextItem itemWithText:@"友達にすすめる" URL:@"dc://compose?subject=&body=http%3a%2f%2fitunes%2eapple%2ecom%2fWebObjects%2fMZStore%2ewoa%2fwa%2fviewSoftware%3fid%3d304074830%26mt%3d8"],
@"関連アプリ",
[TTTableTextItem itemWithText:@"iPachi" URL:@""],
[TTTableTextItem itemWithText:@"iSlot Pro" URL:@""],
[TTTableTextItem itemWithText:@"iTaskTimer" URL:@""],
nil];
}
- (UIViewController *)compose:(NSString *)subject query:(NSDictionary *)param {
TTMessageController* controller =
[[[TTMessageController alloc] init] autorelease];
controller.subject = subject;
controller.body = [param objectForKey:@"body"];
return controller;
}
@end |
第一引数にはsubjectで指定したものが、第二引数以降にも&繋ぎで複数パラメータが指定でき、中身はDictionaryとして取り出すことができます。
上の例ではパラメータを受け取り、それらの値を使って、メール送信画面を起動しているところです。
createModelメソッドはTTTableViewControllerでテーブルデータを表示する際に呼ぶメソッドで、ここではあまりふれません。
が、少しだけ触れておくと、TTTableViewControllerのdatasourceがテーブルデータとなり、TTSectionDataSourceを入れています。TTSectionDataSourceは引数にNSStringがあればそれをセクション名とし、それ以降のものをセクション内に属するデータとして扱います。それからそのデータとしてTTTableTextItemがあり、textで表示する文字を、URLでタップした時の遷移先を指定しています。以上でこんな画面になります。

URL形式での画面遷移でもすごい画期的なのに、まだまだThree20にはすごい部分が盛りだくさんです。
マジすごい。
カメラ系アプリが審査を通り始めた様子なので、他のアプリ達と同じタイミングでこっそり再提出しました。
これまではビューを掘るところで”PLCameraButton”と非公開クラスを明示的に名指しで指定してました。それは通せないですよねAppleさんも。
UIControl *captureButton = (UIControl *)[self uiViewFromViewName:@"PLCameraButton" view:viewController.view]; |
#上のuiViewFromViewNameは引数のビューを再起的に掘り進み、名前と一致するビューがいたらそれを返すようなメソッドです。
これをビュー階層に沿って
UIControl *captureButton = [[[[[[[[[[viewController.view subviews] objectAtIndex:0] subviews] objectAtIndex:2] subviews] objectAtIndex:0] subviews] objectAtIndex:1] subviews] objectAtIndex:0]; |
のように取得するように変えてみました。
それから、明らかに掘り進んでいるようなビュー階層ダンプメソッドはきれいに削除。
これでどうでしょうAppleさん!
え?!ダメ?!
そうですかそうですか。
Three20 JSON datasource implementation – revetkn.com
Three20とjson-frameworkを使って簡単にタイムライン画面を作ってみる。
それぞれセットアップは以下を参考に
【Three20】をプロジェクトで使えるようにする手順 | iphoneアプリで稼げるのか[iPhone] JSON Framework の使い方(準備編) | Sun Limited Mt.
※JSONFramework側の不具合で上記リンクの通り設定した場合でも、OS3.0で使うと実機転送時にcode signエラーが出た。そんな時は「他のリンカフラグ」等の設定はせずに、JSONディレクトリの中身のファイル達を直接プロジェクトに追加で回避できる。6月末に修正されたらしいので最新版を使えばだいじょぶかもしれない。
json-framework – Google Code
最低限のAPI仕様は
- http://twitter.com/statuses/friends_timeline.jsonへ
- ベーシック認証つきで
- GETメソッドでリクエストを投げる
例によってTwitter APIの詳細は公式wikiに譲る。
Twitter API Wiki / Twitter REST API Method: statuses friends_timeline
Three20の中でも今回は
- コントローラとしてTTTableViewController
- データソースとしてTTListDataSource
- テーブルフィールドとしてTTIconTableField
- HTTPアクセスはTTURLRequest
コードは以下。
コントローラ
FirstViewController.h#import
#import "Three20/Three20.h"
@interface FirstViewController : TTTableViewController {
}
@end |
FirstViewController.m
#import "FirstViewController.h"
#import "JSONDataSource.h"
@implementation FirstViewController
- (void)loadView {
self.view = [[[UIView alloc] initWithFrame:[UIScreen mainScreen].applicationFrame] autorelease];
self.view.backgroundColor = RGBCOLOR(240, 242, 245);
self.variableHeightRows = YES;
// STATUS_HEIGHTはTTGlobal.hで定義されてるステータスバーの高さを表す定数
self.tableView = [[[UITableView alloc] initWithFrame:CGRectMake(0, 0, 320, 480 - STATUS_HEIGHT)
style:UITableViewStylePlain] autorelease];
self.tableView.autoresizingMask =
UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.tableView.sectionIndexMinimumDisplayRowCount = 2;
[self.view addSubview:self.tableView];
}
// この辺はTTTableViewControllerのdelegateメソッド
// ここで返したデータソースを使ってTTTableViewControllerが描画もろもろやってくれる
- (id)createDataSource {
JSONDataSource *dataSource = [[[JSONDataSource alloc] init] autorelease];
[dataSource load:TTURLRequestCachePolicyNoCache nextPage:NO];
return dataSource;
}
- (UIImage*)imageForError:(NSError*)error {
return [UIImage imageNamed:@"Three20.bundle/images/error.png"];
}
@end |
データソース
JSONDataSource.h#import
#import "Three20/Three20.h"
@interface JSONDataSource : TTListDataSource {
@private
// 読み込み中フラグ
BOOL _isLoading;
// 読み込み完了フラグ
BOOL _isLoaded;
// ここいらのフラグを見て読み込み中Activityとか出し分けしてる様子
}
@end |
JSONDataSource.m
#import "JSONDataSource.h"
static NSString *username = @"username";
static NSString *password = @"password";
@implementation JSONDataSource
- (id)init {
if (self = [super init]) {
_isLoading = YES;
_isLoaded = NO;
}
return self;
}
#pragma mark TTTableViewDataSource
- (void)load:(TTURLRequestCachePolicy)cachePolicy nextPage:(BOOL)nextPage {
// この辺はTwitterポスト時とほぼ同じ
static NSString *jsonUrl = @"http://%@:%@@twitter.com/statuses/friends_timeline.json";
NSString *url = [NSString stringWithFormat:jsonUrl, username, password];
TTURLRequest *request =
[TTURLRequest requestWithURL:url delegate:self];
request.cachePolicy = cachePolicy;
request.response = [[[TTURLDataResponse alloc] init] autorelease];
request.httpMethod = @"GET";
BOOL cacheHit = [request send];
NSLog((cacheHit ? @"Cache hit for %@" : @"Cache miss for %@"), jsonUrl);
}
#pragma mark TTLoadable
- (BOOL)isLoading {
return _isLoading;
}
- (BOOL)isLoaded {
return _isLoaded;
}
#pragma mark TTURLRequestDelegate
- (void)requestDidStartLoad:(TTURLRequest*)request {
_isLoading = YES;
_isLoaded = NO;
[self dataSourceDidStartLoad];
}
- (void)requestDidFinishLoad:(TTURLRequest*)request {
// 通常通りレスポンスゲット
TTURLDataResponse *response = request.response;
NSString *responseBody = [[NSString alloc] initWithData:response.data encoding:NSUTF8StringEncoding];
// JSONFrameworkのメソッドでArrayとして受け取る。JSONの構造次第でNSDictionaryの可能性もあるのでAPI要確認
NSArray *json = [responseBody JSONValue];
for(NSDictionary *result in json) {
// JSON内のデータ構造はTwitter APIを参照
NSDictionary *user = [result valueForKey:@"user"];
// データソースの要素としてTTIconTableFieldをセット。これでアイコンとテキストがワンセットのテーブルになる。
[self.items addObject:[[[TTIconTableField alloc]
// JSONからテキスト取得
initWithText:[result objectForKey:@"text"]
url:nil
// JSONから画像取得
image:[user objectForKey:@"profile_image_url"]
// 画像読み込み前のデフォルト画像を指定。ここは自分で用意
defaultImage:[UIImage imageNamed:@"DefaultAlbum.png"]] autorelease]];
}
_isLoading = NO;
_isLoaded = YES;
[self dataSourceDidFinishLoad];
}
// この辺はNSURLRequestでおなじみな感じ
- (void)request:(TTURLRequest*)request didFailLoadWithError:(NSError*)error {
NSLog([error localizedDescription]);
NSLog(@"didFailLoadWithError");
_isLoading = NO;
_isLoaded = YES;
[self dataSourceDidFailLoadWithError:error];
}
- (void)requestDidCancelLoad:(TTURLRequest*)request {
NSLog(@"requestDidCancelLoad");
_isLoading = NO;
_isLoaded = YES;
[self dataSourceDidCancelLoad];
}
@end |
以上で、こんな画面の出来上がり。

これだけで、ネットワーク接続エラー時の対応やデータキャッシュや読み込み中表示などをこちらが意識せずとも勝手にやってくれる。
※表示項目をよりtwitterクライアントにしたい場合は別途カスタマイズが必要。
JSONに関してはアクセス先のURLと取り出し方法の2つが外部APIに依存する部分なので、その辺をdelegateに任せるなりすればこのソースをうまいこと活用できると思う。
Objective-Cのカテゴリという仕組みを使うと、1つのクラスを複数のクラスに分割したり、クラスに新しい機能を追加したりできる。
このカテゴリの強力なところは、NSStringのようなCocoa Frameworkで提供されているクラス達に対しても、メソッドを追加できるところ。
例えば、ベーシック認証をリクエストのヘッダ内に認証情報を持たせて行いたい。その時、ユーザ名、パスワードはbase64エンコードが必要になるのだけど、既存のNSStringにはそんな便利メソッドはない。
そんな時は、このカテゴリを使ってNSStringにbase64エンコードを行うメソッドを追加してしまう。
このカテゴリによるクラス拡張はオープンソースを眺めてると必ず出てくる。
json-framework – Google Code
google-toolbox-for-mac – Google Code
サンプルコードは以下。
NSString+Base64Encode.h
#import @interface NSString (DCEncode) + (NSString*)stringEncodedWithBase64:(NSString*)str; @end |
NSString+Base64Encode.m
#import
#import "NSString+Base64Encode.h"
@implementation NSString (DCEncode)
+ (NSString*)stringEncodedWithBase64:(NSString*)str
{
static const char *tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const char *s = [str UTF8String];
int length = [str length];
char *tmp = malloc(length * 4 / 3 + 4);
int i = 0;
int n = 0;
char *p = tmp;
while (i < length) {
n = s[i++];
n *= 256;
if (i < length) n += s[i];
i++;
n *= 256;
if (i < length) n += s[i]; i++; p[0] = tbl[((n & 0x00fc0000) >> 18)];
p[1] = tbl[((n & 0x0003f000) >> 12)];
p[2] = tbl[((n & 0x00000fc0) >> 6)];
p[3] = tbl[((n & 0x0000003f) >> 0)];
if (i > length) p[3] = '=';
if (i > length + 1) p[2] = '=';
p += 4;
}
*p = '\0';
NSString *ret = [NSString stringWithCString:tmp];
free(tmp);
return ret;
}
@end |
ヘッダファイルでは@interfaceの後にある(DCEncode)というのがカテゴリの名前。
実装ファイルでは@implementationの後にある(DCEncode)と対応してる。
Objective-Cではファイル名とクラス名は特に関連がなくてもOKなのもポイント。
自作クラスでもこのようにカテゴリを分けて記述することで、複数ファイルに分割してプログラムができる。
ただし、変数は親ファイルで定義しないといけないので注意。
これは動的なメソッド追加が可能なことを意味しているのだけど、詳細は以下のサイトに譲る。
【コラム】ダイナミックObjective-C (8) カテゴリ – 動的なメソッドの追加によるクラスの拡張 | エンタープライズ | マイコミジャーナル
base64エンコードのロジックは夏ライオンのソースを参考にさせていただきました。
感謝。
参考
takuma104’s ntlniph at master – GitHub
最近Twitter熱が僕の回りでも加速してきたけど、アプリ標準機能として搭載すべく、iPhoneアプリ内でtwitterポストするためのサンプルコードを書いてみた。
Twitter API Wiki / Twitter REST API Method: statuses update
詳細は公式Wikiに譲るとして、最低限のAPI仕様は
- http://twitter.com/statuses/update.xmlへ
- ベーシック認証つきで
- POSTメソッドを使い
- statusパラメータ=「つぶやき」のリクエストを投げる
そんな条件を満たしたポストコードが以下。
続きを読む

