ラベル Cocoa の投稿を表示しています。 すべての投稿を表示
ラベル Cocoa の投稿を表示しています。 すべての投稿を表示

2012年1月29日日曜日

xxd を使って画像などのバイナリデータをソースコードに含める方法

iOS向けのライブラリやフレームワークを作成しているときに、どうしても画像などのバイナリデータをライブラリやフレームワークに含めたくなる時があります。たとえばUI系のフレームワークなどですね。このようなときに、たとえば静的ライブラリ(.aと.h)やフレームワーク(.framework)とセットで画像を一緒に同梱し、ユーザーのXcodeプロジェクトに一緒に含めてもらうという方法もあるのですが、この方法だと画像名がユーザーのプロジェクトに含まれている画像とかぶったりしてはいけませんし、管理が面倒になってしまいます。また、ライセンスがプロプライエタリなライブラリでは、画像などのリソースをあまり積極的にユーザーに公開したくないというニーズがあったりします。

そこでxxdツールのご紹介です。岸川先生に教えていただいたのですが、xxdというツールを使えばバイナリデータをC言語のヘッダファイルとして簡単に出力することができるらしいのです。これを使ってバイナリデータをライブラリ内部のソースコードの一部として配布してみましょう。

xxdはvimに同梱されているので、最初からMac OS Xについてきます。使い方も非常に簡単です。
xxd -i Sample.png
とすると、
unsigned char Sample_png[] = {
  0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
  0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x07, 0xfd, 0x00, 0x00, 0x04, 0x73,
//中略
unsigned int Sample_png_len = 589903;
このような感じでバイナリデータがC言語のヘッダファイルになって出力されます。あとはこれをNSDataにして、UIImageを生成することができます。NSDataを作る際には余計なデータコピーが発生しないdataWithBytesNoCopy:length:freeWhenDone:を使うことをお勧めします。
NSData *data_Sample_png = [NSData dataWithBytesNoCopy:Sample_png length:Sample_png_len freeWhenDone:NO];
UIImage *image = [UIImage imageWithData:data_Sample_png];

2011年12月7日水曜日

CocoaPods に対応していないライブラリを集めた自分用リポジトリを作る方法

この記事はiOS Advent Calendar 2011の7日目の記事になります。ということでもうすぐクリスマスですね。クリスマスプレゼントの準備はお済みですか?まだの方はちょっとオシャレに、今年のプレゼントをCocoaPodsでご用意してみてはいかがでしょうか?


■ご存じ、ないのですか!?

さて念のためCocoaPodsについておさらい。要するにiOS/OS X用のmavenです。以上。細かい点については以下の記事が詳しいのでそちらをご参照ください。っていうかMac Dev JP Advent CalendarとネタがもろかぶりこのCocoaPodsを使うと今まで大変面倒くさかったライブラリの管理が嘘のように簡単になります。たとえば、新しいプロジェクトを始めるときに、
  • 通信したいからASIHTTPRequestを使おう
  • APIのレスポンスがJSONだからJSONKitも必要だな
  • DBにはCore Dataを採用したいから、MagicalRecordも欲しいな
  • Blocksバリバリ使うからBlocksKitは常識だよね
と思ったら、さくっと以下のような設定ファイル(Podfile, mavenで言うところのpom.xml)を書いてやれば、あとはCocoaPodsが指定されたライブラリを取ってきてビルド設定までやってくれるわけです。
platform :ios

dependency 'ASIHTTPRequest' ,'~> 1.8'
dependency 'JSONKit'        ,'~> 1.4'
dependency 'BlocksKit'
dependency 'MagicalRecord'
ARCあり・なしのライブラリを混ぜても全く問題ありません。素晴らしい!!


■公開なんて、あるわけない

とまぁ実に素晴らしいツールなのですが、問題もあります。
  • つい最近できたばかりのツールなので、対応しているライブラリが少ない
  • 対応しているライブラリでも、もともと依存関係処理をするという文化があまりなかったせいか、一部のライブラリ(Reachabilityとか)が内包された状態で出回っていたり、バージョンタグが一つや二つしか付いていないので上手くバージョン管理が出来ない(しかも極端に古かったりバグがあったり、さんざん)
  • CocoaPods自体が開発途中ということもあり、機能がどんどん追加されているようなのだがドキュメントが追いついていない
例を挙げると以下の画像のような感じで、バージョンが一つしかなかったり、あるんだけれど飛んでいたりなどなど。要するに自分の使いたいコードがCocoaPodsの中央リポジトリで管理されていないということがままあります。



■俺達は、自分たちでpodspecを用意することを......強いられているんだ!

ありがたいことに、CocoaPodsには中央リポジトリ以外の任意のリポジトリをライブラリ管理用のリポジトリとして追加する機能があります。この機能を使って、自分で対応していないライブラリのpodspecファイルを書いて、CocoaPodsで使えるようにすることができます。またCocoaPods 0.3.0以降であれば、設定ファイルに直接自分の好きなライブラリのpodspecを書くこともできるみたいです。

まず最初のステップはpodspecを書くことです。今回は例としてMKNetworkKitというライブラリのバージョンv0.8a用のpodspecを書いてみることにします。

# まずは対象のリポジトリをcloneしてくる
# ここでは相手のリポジトリを直接使ってますが、github上でforkして、そっちを使うようにしてもいいです。forkしたほうが自分で自由にコードに改変を加えたりtagを打ったりできますのでよいかも。
git clone https://github.com/MugunthKumar/MKNetworkKit.git
# 移動
cd MKNetworkKit
# 対象のバージョンにHEADを移動します
git reset --hard v0.8a
# podspecファイルのひな形を出力します
pod spec create MKNetworkKit
これでMKNetworkKit.podspecファイルが出力されますので、今度はこのファイルを書き換えます。先ほどcloneしてきたライブラリのソースコードとプロジェクト設定を見ながら、必要なソースコード、必要なリソース、不要なファイル、ライブラリやフレームワークなどのビルド設定を考えて、適切な設定を用意しなければなりません。
今回はこんな感じで書きました:
Pod::Spec.new do |s|
  s.name     = 'MKNetworkKit'
  s.version  = '0.8a'
  s.license  = 'MIT'
  s.summary  = 'Full ARC based Networking Kit for iOS 4+ devices'
  s.homepage = 'https://github.com/MugunthKumar/MKNetworkKit'
  s.author   = { 'MugunthKumar' => 'mknetworkkit@mk.sg' }
  s.source   = { :git => 'https://github.com/MugunthKumar/MKNetworkKit.git', :tag => 'v0.8a' }

  s.source_files = 'MKNetworkKit/*.{h,m}', 'MKNetworkKit/Categories/*.{h,m}'
  s.clean_paths  = 'MKNetworkKitDemo', '*.xcodeproj', 'sample.JPG'
  s.frameworks   = 'CFNetwork'
  s.requires_arc = true

  s.dependency 'Reachability', '~> 2.0'
end
大事なのはsource, source_files, frameworks, requires_arc, dependencyぐらいです。あとは自分しか使わないならでたらめでかまいません。
sourceは:tagの指定の代わりに:commitでコミットのハッシュ値を指定することもできるみたいです。
このpodspecファイルの記法、やたらとたくさんある上にドキュメントがあまりないので、私は結局公式リポジトリのpodspecを探して見よう見まねで書きました。以下、参考にした物を列挙します。
https://github.com/CocoaPods/Specs
https://github.com/CocoaPods/Specs/blob/master/ASIHTTPRequest/1.8.1/ASIHTTPRequest.podspec
https://github.com/CocoaPods/Specs/blob/master/ASIWebPageRequest/1.8.1/ASIWebPageRequest.podspec
https://github.com/CocoaPods/Specs/blob/master/BlocksKit/0.5.0/BlocksKit.podspec
https://github.com/CocoaPods/Specs/blob/master/BlocksKit/0.9.0/BlocksKit.podspec
https://github.com/CocoaPods/Specs/blob/master/SSToolkit/0.1.2/SSToolkit.podspec
https://github.com/CocoaPods/Specs/blob/master/Kiwi/1.0.0/Kiwi.podspec
https://github.com/CocoaPods/Specs/blob/master/MGSplitViewController/1.0.0/MGSplitViewController.podspec
使用するリソースファイルも指定出来るみたいです。
https://github.com/CocoaPods/Specs/blob/master/SVProgressHUD/0.5/SVProgressHUD.podspec
巨大なのになるとこんなのも書けるみたいです。
https://github.com/CocoaPods/Specs/blob/master/RestKit/0.9.3/RestKit.podspec
https://github.com/CocoaPods/Specs/blob/master/Nimbus/0.9.0/Nimbus.podspec

書き終わったら、書いたpodspecファイルに問題がないかをチェックします。
pod spec lint MKNetowrkKit.podspec
何か問題があれば何かエラーが出ます。修正しましょう。何も無ければ何も出ません。
問題が無くなったらひとまずpodspecファイルについては完成です。次はこのpodspecファイルを置くリポジトリをgithubを使って用意します。github上に適当な名前でリポジトリを作りましょう。私は今回 https://github.com/akisute/Specs というリポジトリを作りました。
リポジトリを作ったら、先ほど作ったpodspecファイルを、以下の命名規則に従ってリポジトリの中に配置します:
/podspecのs.name/podspecのs.version/先ほど作ったpodspecファイル
たとえば今回の例では:
/MKNetworkKit/0.8a/MKNetworkKit.podspec
という名前で配置する必要があります。私が試した際は、間違ってると正しくpodspecファイルを認識してくれませんでした。ファイルを配置したらこのリポジトリをgithubにpushします。

さてこれでpodspec用のリポジトリが出来ましたので、今度はCocoaPods側の設定を行います。以下のコマンドを実行します:

pod repo add myrepo リポジトリのURL
これでmyrepoという名前でリポジトリが登録されます。 ~/.cocoapods/ 以下を覗いてみると、確かに myrepo という名前のリポジトリが追加されているはずです。

あとは普通にCocoaPodsを使うのと同じ要領で、Podfileを書いて、pod installすればうまくいくはずです。・・・といいたいところなのですが、一発でうまくいくことはまれで、たいていpodspecファイルの書き方に問題があったりとか、pod化したい対象のライブラリのコードに問題があってビルドが通らないのが普通です。そこで以下のようなワークフローになります。
  1. コードに問題があるなら、コードをforkして自分の思うように書き換えてpush
  2. podspecファイルを修正して自分のpodspec用リポジトリにpush
  3. 組み込みたいプロジェクトのPodsディレクトリ、Podfile.lockファイル、生成されたxcworkspaceを削除。
  4. 再度 pod install MyProject.xcodeproj を実行。
  5. ビルド。
  6. 問題があれば1. に戻る。
うん、これは素人にはお勧めできない。

しかしながらこのCocoaPodに対応するライブラリが増えていけば、iOSの開発はずいぶんと楽になるはずです。ということで積極的に使っていきたいと思います!

2011年11月27日日曜日

Cocoa Framework に用意されていないロックを Objective-C で実装する


Cocoaフレームワークは非同期処理時のロックを取るために、NSLockingというプロトコルと、NSLock, NSRecurrsiveLock, そしてNSConditionalLockという3種類のロックの実装を提供しています。が、残念ながらちょっとまともな非同期コードを書こうと思うとこれでは全然足りません。っていうか、NSConditionalLockがロック抜けるときにしか条件値を書き換えられない実装なのが正直いけてないと思います。これじゃCounting Lock(最初に決めた数だけ同時にロックできるロック。Counting Semaphoreともいう)にもRead/Write Lock(書き込みロックと読み込みロックの二種類が用意され、書き込みロックが取られていない限りは、何個でも同時に読み込みロックが取れる、効率のいいロック)にも使えません。というわけで、Objective-Cで書かれたCounting LockとRead/Write Lockを見つけましたのでご紹介いたします。

http://cocoaheads.byu.edu/wiki/locks

中身はpthread.hのpthread_mutexを使って実装しているようです。一見危なそうですが、Cocoaフレームワークが使用するスレッドは全て内部実装がpthreadなので全く問題ありません。

2011年11月19日土曜日

UIWebView.scrollView に対して KVO を使うと色々面白い

iOS 5より、UIWebViewにscrollViewプロパティが追加され、たとえばスクロールを無効にしたりステータスバーをタップしても一番上に戻らないようにしたりなど、UIWebViewのスクロール周りの処理を外から自由に触れるようになりました。ですが便利なのはこれだけではありません。KVOの仕組みを使うことで、さらにUIWebViewを便利に使うことができます。ここでは私が使っている中で一番のおすすめをご紹介します。

■UIWebViewの描画しているHTMLのcontentSizeを非同期的に、リアルタイムで取得する
UIWebview.scrollViewのcontentSizeプロパティは、UIWebViewの描画しているHTMLの大きさ(contentSize)と同じ値になります。この性質を利用して、contentSizeプロパティにKVOを貼ると、UIWebViewの描画しているHTMLの大きさ(contentSize)が変わったタイミングで通知を受け取ることができます。こんなコードになります。
- (void)viewWillAppear:(BOOL)animated
{
  [self.webView.scrollView addObserver:self forKeyPath:@"contentSize" options:0 context:NULL];
  [self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://akisute.com"]]];
}
- (void)viewWillDisappear:(BOOL)animated
{
  [self.webView.scrollView removeObserver:self forKeyPath:@"contentSize"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
  NSLog(@"%s", __func__);
  NSLog(@"  * contentSize = %@", NSStringFromCGRect(self.webView.scrollView.contentSize));
}
このテクを使うことで、例えばUIWebView自体をスクロールさせないようにした上で通知を受け取ってUIWebViewのframeをどんどん更新するようにし、UIScrollViewの中に埋め込んでしまってUIImageViewやUILabelのようにして使うみたいなUIを作ることが簡単にできます。HTMLの高さに依存するコードも作れますし、なかなか面白いですよ。iOS 5はもちろん、iOS 4でも動作します。(余談で解説)

■余談
UIWebViewのscrollViewプロパティは実はiOS 4のころからPrivate APIとして存在します。ですがiOS 5よりパブリック扱いになったおかげか、iOS 4向けのアプリでscrollViewプロパティを触っていてもリジェクトされたりクラッシュすることなく普通に使えるので非常に嬉しいです。iOS 3では使えませんのであしからず。

iOS 5でのSSL/TLS通信時にエラーが発生した場合のエラーコードの調べ方

iOS 5より新たにSecurity.frameworkというフレームワークが追加されました。このフレームワークはAppleが実装したSSL/TLS用のライブラリで、iOS 5よりCFNetwork系のクラス(NSURLConnectionなどの内部実装にも使われています)のSSL/TLS通信時に使われるようになったみたいです。

http://developer.apple.com/library/ios/#releasenotes/General/WhatsNewIniPhoneOS/Articles/iOS5.html の一番下にちょこっとだけ説明があるので、見てみると、
Security
The Security framework (Security.framework) now includes the Secure Transport interfaces, which are Apple’s implementation of the SSL/TLS protocols. You can use these interfaces to configure and manage SSL sessions, manage ciphers, and manage certificates.

For information about the Secure Transport interfaces, see the SecureTransport.h header file of the Security framework.
うむ、ドキュメント無いからヘッダ読めとはなかなか適当ですね。

普通にアプリを作っている最中にこのSecurity.frameworkを直接触る必要は全くないのですが、問題はこのSecurity.frameworkを使っているAppleのframeworkが通信時にSSL/TLS絡みのエラーを吐いたときに発生します。このとき、それらのフレームワークは以下のようなNSErrorを返してきます。
Error Domain=NSOSStatusErrorDomain Code=-9830 "The operation couldn’t be completed. (OSStatus error -9830.)
NSErrorの読み方はErrorDomainに指定されている文字列に応じてエラー番号が定義されているドキュメント/ヘッダファイルを見るというものなのですが、このNSErrorはNSOSStatusErrorDomainのErrorDomainを指定しているにも関わらず<CarbonCore/MacErrors.h>にエラー番号の定義が全く書いていないのです!

色々原因を調べた結果、以下のQ&Aが見当たりました。
http://developer.apple.com/library/mac/#qa/qa1499/_index.html
どうやらSecurity.framework絡みのエラーはNSOSStatusErrorDomainを使っているがMacErrors.hではなく<Security/SecureTransport.h>などに定義が書いてあるみたいなのです。見てみると・・・
/*************************************************
 *** OSStatus values unique to SecureTransport ***
 *************************************************/

/*
    Note: the comments that appear after these errors are used to create SecErrorMessages.strings.
    The comments must not be multi-line, and should be in a form meaningful to an end user. If
    a different or additional comment is needed, it can be put in the header doc format, or on a
    line that does not start with errZZZ.
*/

enum {
 errSSLProtocol    = -9800, /* SSL protocol error */
 errSSLNegotiation   = -9801, /* Cipher Suite negotiation failure */
 errSSLFatalAlert   = -9802, /* Fatal alert */
 errSSLWouldBlock   = -9803, /* I/O would block (not fatal) */
    errSSLSessionNotFound   = -9804, /* attempt to restore an unknown session */
    errSSLClosedGraceful   = -9805, /* connection closed gracefully */
    errSSLClosedAbort    = -9806, /* connection closed via error */
    errSSLXCertChainInvalid  = -9807, /* invalid certificate chain */
    errSSLBadCert    = -9808, /* bad certificate format */
 errSSLCrypto    = -9809, /* underlying cryptographic error */
 errSSLInternal    = -9810, /* Internal error */
 errSSLModuleAttach   = -9811, /* module attach failure */
    errSSLUnknownRootCert  = -9812, /* valid cert chain, untrusted root */
    errSSLNoRootCert   = -9813, /* cert chain not verified by root */
 errSSLCertExpired   = -9814, /* chain had an expired cert */
 errSSLCertNotYetValid  = -9815, /* chain had a cert not yet valid */
 errSSLClosedNoNotify  = -9816, /* server closed session with no notification */
 errSSLBufferOverflow  = -9817, /* insufficient buffer provided */
 errSSLBadCipherSuite  = -9818, /* bad SSLCipherSuite */
 
 /* fatal errors detected by peer */
 errSSLPeerUnexpectedMsg  = -9819, /* unexpected message received */
 errSSLPeerBadRecordMac  = -9820, /* bad MAC */
 errSSLPeerDecryptionFail = -9821, /* decryption failed */
 errSSLPeerRecordOverflow = -9822, /* record overflow */
 errSSLPeerDecompressFail = -9823, /* decompression failure */
 errSSLPeerHandshakeFail  = -9824, /* handshake failure */
 errSSLPeerBadCert   = -9825, /* misc. bad certificate */
 errSSLPeerUnsupportedCert = -9826, /* bad unsupported cert format */
 errSSLPeerCertRevoked  = -9827, /* certificate revoked */
 errSSLPeerCertExpired  = -9828, /* certificate expired */
 errSSLPeerCertUnknown  = -9829, /* unknown certificate */
 errSSLIllegalParam   = -9830, /* illegal parameter */
 errSSLPeerUnknownCA   = -9831, /* unknown Cert Authority */
 errSSLPeerAccessDenied  = -9832, /* access denied */
 errSSLPeerDecodeError  = -9833, /* decoding error */
 errSSLPeerDecryptError  = -9834, /* decryption error */
 errSSLPeerExportRestriction = -9835, /* export restriction */
 errSSLPeerProtocolVersion = -9836, /* bad protocol version */
 errSSLPeerInsufficientSecurity = -9837, /* insufficient security */
 errSSLPeerInternalError  = -9838, /* internal error */
 errSSLPeerUserCancelled  = -9839, /* user canceled */
 errSSLPeerNoRenegotiation = -9840, /* no renegotiation allowed */

 /* non-fatal result codes */
 errSSLPeerAuthCompleted     = -9841,    /* peer cert is valid, or was ignored if verification disabled*/
 errSSLClientCertRequested = -9842, /* server has requested a client cert */

 /* more errors detected by us */
 errSSLHostNameMismatch  = -9843, /* peer host name mismatch */
 errSSLConnectionRefused  = -9844, /* peer dropped connection before responding */
 errSSLDecryptionFail  = -9845, /* decryption failure */
 errSSLBadRecordMac   = -9846, /* bad MAC */
 errSSLRecordOverflow  = -9847, /* record overflow */
 errSSLBadConfiguration  = -9848, /* configuration error */
 errSSLLast     = -9849, /* end of range, to be deleted */

    /* DEPRECATED aliases for errSSLPeerAuthCompleted */
    errSSLServerAuthCompleted   = -9841, /* server cert is valid, or was ignored if verification disabled DEPRECATED */
 errSSLClientAuthCompleted   = -9841,    /* client cert is valid, or was ignored if verification disabled; reusing error as you can only be client or server - DEPRECATED */

};
ばっちり書いてますね。これでエラーの原因も特定できます。今回のケースはerrSSLIllegalParamだったみたいですね。

2011年9月18日日曜日

iOS, Android, Windows Phoneのメモリ管理とかメッセージングの仕方を調べてみた

ぼちぼちTwitterにつぶやいていたらTogetterにまとめてくださった方がいらっしゃるので、せっかくなのでこちらでも紹介したいと思います。

http://togetter.com/li/189550 (githubみたいにembedできればいいのになーなんて><)

結構気合い入れて調べたのでおすすめです。

2011年9月12日月曜日

UIWebView の Private API を使って BASIC認証のあるページにアクセスする

元ネタはこちら: http://d.hatena.ne.jp/KishikawaKatsumi/20090603/1243968707

仕事でどうしても以下の要件を満たすUIWebViewが必要になったので作りました。
  • 開発環境にBASIC認証がかかっており、そこにUIWebViewでアクセスしたい。
  • アクセス先のHTMLにリンクが埋め込まれているため、URLをhttp://username:password@example.comのように変換することができない。webView:shouldStartLoadWithRequest:navigationType:で頑張ればいける気がしなくともないのですが結局断念しました。
  • 諸事情によりNSURLConnectionが使えない(当然ASIもダメ。あくまでUIWebViewでアクセスする必要がある)。
普通につくるとどうにもうまくいかなかったので、結局UIWebViewをオーバーライドしてPrivate APIを叩く作戦を取ることにしました。

というわけで出来上がったソースコードはこちら。MITライセンスです。
https://gist.github.com/1210372
iOS 4.3.5で動作確認しています。iOS 5でも多分動作します。 iOS 3以下はわかりません><

注意: このコードにはPrivate APIが多分に含まれています。このコードが含まれたアプリをApp Storeに提出しても十中八九審査に通りません。あくまで開発環境での検証用または自分のiPhoneに入れてニヤニヤするコードにのみお使いください。


■しくみ

UIKitをclass-dumpしてたら使えそうなシグネチャを見つけたので、継承してオーバーライドしてゴニョゴニョしてたらできちゃった!って感じです。それにしてもWebKitはPrivate APIにしておくにはもったいない出来の良さですね。このAPIへのフルアクセスがあればAndroidのWebViewと同等かそれ以上に自由に使えるのですが。

2011年8月22日月曜日

UDIDが使えなくなりそうなので、UIIDを使えるようにしました

■2012/11/11追記
iOS 6より[[UIDevice currentDevice] identifierForVendor]というAPIがAppleより提供され、よりプライバシーに配慮した上により安全な方法で自分の開発したアプリケーションを利用するユーザーを個別に認証することが可能になりました。それに伴い拙作のライブラリもidentifierForVendorが利用可能であればこちらを利用するように修正いたしました。今後はこのidentifierForVendor(または広告APIなどを作る場合であれば[[UIDevice sharedManager] advertisingIdentifier])が個体認識の主流になっていくと思われます。identifierForVendorとadvertisingIdentifierの仕様まとめは http://stackoverflow.com/questions/11836225/ios6-udid-what-advantages-does-identifierforvendor-have-over-identifierforadve が一番詳細かなと思います。
追記終


つい先日TechCrunchがiOS5よりUDIDの使用が非推奨になると報道し、巷はiOSでのUDIDの使用についての話題で俄然盛り上がっています。セキュリティ的によろしくないから良い変更だという声もあれば、すでに認証用として使っていてシステム改修が必要という悲観の声もあります。しかし私はどちらかというとUDIDをバリバリ使っちゃってる方陣営の人なので、セキュリティの問題については知っていても、やはりUDID相当の物が無いと不便だなぁと思ってしまうのです。そこでプログラマらしくコードで解決することにしました。

UDIDにはセキュリティの懸念があるし、もう使えなくなる。だったらもっといいIDを超簡単に生成して使えるようにすればいいじゃない。ということでUnique Installation Identifier (インストール毎ID、UIID) を生成するライブラリを早速書いてみました。
ソースコードはこちら。MITライセンスです。
https://github.com/akisute/UIApplication-UIID
昔のgistはこちら

使い方は超簡単で、
  1. アプリが一度削除されたとしても同一のUIIDを返すようにしたいのであれば、#define UIID_PERSISTENT=1して、Security.frameworkをプロジェクトに追加する
  2. UIApplication+UIID.hをimport
  3. [[UIApplication sharedApplication] uniqueInstallationIdentifier]で取れます
はいこれだけです。


■そもそもどうしてUDIDを使うのか

たいていの場合は以下のような理由です。
  • ログイン機構なしに簡易にユーザーの識別をしたいときに、毎回同じ値を返し、かつユーザー事に異なる値が取得出来る何かが必要になるので、UDIDを使う
  • UDID値の取得がいつでもどこでも可能、さらに超簡単で外部ライブラリのインストールなど面倒なことが一切無い
  • しかもAppleのドキュメントにそう使えって書いてある。・・・補足すると使ってもいいよ、ぐらいのニュアンスで、使えという風に推奨はしてないみたいです>< またUDIDのみをキーとしてユーザー情報を保存するな、とも書いてますね。


■UDIDは何がまずいの

主にまずい理由はセキュリティです。特にガラケーのかんたんログインの問題が参考になるのですが、iOSの場合は以下のようなセキュリティ問題が発生します。
  • 相手のUDIDがわかってしまえば、簡単になりすましが可能
  • UDIDの値を返すメソッドの実装を差し替えて、任意の値を返すように出来る。JailBreakしているユーザーであれば誰でも簡単に実行可能、そのためのアプリもCydiaで検索すれば転がっている
  • UDID自体、様々な方法で取得可能。アプリをインストールさせてそこ経由で集めてみたりはもちろん(その際値を取得することに対する警告すら出ない)、iTunesからでも値を確認できる
  • 上記三つの理由のため、その気になればたやすく任意のユーザーになりすませる
  • さらに恐ろしいことにUDIDは端末事に一意になるため、一度値が特定されてしまうと端末を買い換えない限りずっとなりすまされてしまう、リセットできない


■UIIDだとどうなる

Unique Installation Identifier, UIIDはUDIDと異なり「アプリのインストール基盤毎」に一意な値を返すような実装になっています。簡単にまとめると、あるアプリAが、デバイス1, 2, 3にインストールされた場合、UIIDは1, 2, 3の全てで異なる値になります。ここまではUDIDと同じですが、UDIDと違うのはデバイス1に別々のアプリA, B, Cがインストールされた場合、それぞれ異なる値になります。中身は単にCFUUIDというiOSに元からある良くできたID生成ルーチンなんですが、これにより以下のようなメリットが得られます。
  • 外部からIDの値を取得するのが困難。推測も十分に長い上に独立な値なため困難、取得も(UIID_PERSISTENT=1でビルドすればKeychainを使うので)困難です。
  • 万が一何かの間違いで流出したとしても、アプリごとに異なる値になるため他のアプリのセキュリティが犯されたりはしない。また同様の理由で他の悪意のあるアプリからUIIDを取得して攻撃することもできない。
  • プログラム的にUIIDをリセット可能。UUID_PERSISTENT=0であればアプリを消せばユーザーが任意にリセット可能。
  • Keychainを使ったメリットとして、ユーザーがデバイスを乗り換えてもiTunesのバックアップに値が保持されるため、それから復元を行った場合UIIDの値が引き継がれる。
  • 通常のアプリであればUDIDを使っていたケースのほとんどはこのUIIDでそのまま代用可能、あとは過去のUDIDとUIIDのヒモ付だけサーバー側でやってしまえば完全に乗り換えられる
  • それでもどうしてもデバイスごとに一意な値が欲しいならhttps://github.com/gekitz/UIDevice-with-UniqueIdentifier-for-iOS-5/blob/master/Classes/UIDevice+IdentifierAddition.mなど使えばよいのかなと。ただしこの実装はMACアドレスの値を使っているため、MACアドレスの値がわかってしまえばIDをたやすく生成可能で、UDIDの持つセキュリティ上の問題は残ります。


■使用上の注意

UDIDよりは問題が少ないですが、それでもこのUIIDだけで認証を行うような作りのアプリには間違ってもしないように!以下のような問題があります。
  • そもそもこの値は完全にユーザーと一意にヒモ付いた値ではありません。UIIDはあくまでアプリのインストール単位とヒモ付いているだけです。複数のデバイスを一人のユーザーが所有していたりすると完全にアウトですし、インストールされた端末が譲与された場合も対応できません。
  • プログラム的にUIIDはリセット可能なので最悪の場合でもずっとなりすまされるのは回避できるのですが、その際にヒモ付けられていたユーザー固有の情報が消えてしまいます。ログインIDとパスワードで認証をしているのであれば、ログインIDはそのままにしてパスワードだけをリセットすることで、なりすましの問題を解決しつつ、ユーザーの一意性は保ったままにできるため、万が一の際はログインIDとパスワードを使っている方が圧倒的に利便性が高まります。
  • 以上の理由により、UIIDはあくまで簡易的・一時的にユーザー認証をする時に使用し(たとえばユーザーが購入したアプリ内課金の商品をアプリが消されるまでの間だけ履歴として持っておいたり、ゲームのランキング等で匿名だけれど点数ランキングに参加できるようにしたり)、正式で完全なサービスはログインIDとパスワードによって提供するべきです。そうすることで一人のユーザーが複数のデバイスでサービスを使えるようになります。さらにはAndroidとも連携できたりしますしお得です。


■っていうか

全部高木先生が一年前に言っている通りになっちゃってるじゃないすか!っていうか私が作った物もこの高木先生がおっしゃってる「アプリ専用の(セキュアな)独自IDを生成してそれを保存して使う」というものの実装にすぎません。しかしまったく、せっかく警告してもらっても、人間実際に問題に直面しなければなかなか手をつけないものですね><

しかし、しかしですね、あえてここで一言、エンジニアとして申させていただきたい。

私、エンジニアが欲しいのはセキュリティ上正しい実装の方法だとか、概念だとか、ましてやどこそこのログイン方法はいけてないから直せや、などという文章でもないのです。我々が欲しいのは、「すでに実装されている、セキュリティ上正しくて、猿でも理解できて、1分で組み込めて、どのような環境でも動き、ユーザーが会員登録なんて面倒極まりないことをしなくても済むユーザー認証の手段」なんです。要するに、
// 何か良くわからんけどこのトークンをHTTPS経由でPOSTして認証しておけば超スーパー確実かつセキュアで猿でも実装できてハッピーになる
[[NSAuthentication sharedUser] authenticationToken]
↑コレが今すぐ欲しいんですよ、我々エンジニアというのは。そうすれば誰だってUDIDを使って認証するみたいな面倒くさいことするわけ無いじゃないですか。頼まれたってやりませんよ。

私はユーザー認証をしたいだけなんです、それも可能な限り楽に。口で何と批判しようが、正しい方法を教えようが、世の中は決して変わらないと思います。みんな楽をしたいから。なのでAppleには是非UDIDを廃止するこの機会にぜひ上記のような何かをUIKitなりFoundationなりに組み込んで欲しいですね。こういうところも、良いプラットフォーマーの責務の一部じゃないかなと。

2011年8月12日金曜日

iOS で ImageIO を使ってアニメーションGIFファイルを生成してみる

参考にしたのはこちら。
http://pojos-devlog.blogspot.com/2005/08/saving-animated-gif-using-coregraphics.html

iOS 4以降でよければImageIOフレームワークが使えるためむちゃくちゃ簡単です。任意のUIImage / CGImageRefから好きなようにアニメーションGIFを生成できます。



iOS 3以前の場合は・・・頑張れとしか・・・

メモ: CoreDataで更新処理をするときは、lockをわすれずに

単なるメモ書きです><

http://twitter.com/#!/akisutesama/status/83521489382555650
http://twitter.com/#!/akisutesama/status/83521729380626433
ある一つのCore Dataのモデルを非同期的に複数箇所から更新するときは、たとえどんなに軽微な、プロパティ一つだけの、他からは触られない様な変更ですら、きちんとlockを取らないと危険ということがわかった。API実行クラスだけでは不十分であった。非同期であればロック必須。
変更を行うコードブロックを渡して、内部で安全にロックして実行、必要に応じてロールバックや失敗通知も行える様にする仕組みを作ろうと思った。
CoreDataのモデルオブジェクトの更新はただのsetterプロパティの使用だけで発生してしまうのでついつい忘れがちになるのですが、これが原因で実際にクラッシュしたアプリもあるので油断禁物。

UIPanGestureRecognizerはiOS4.0ではtranslationプロパティを正しく返さない

UIPanGestureRecognizerのtranslationプロパティは、iOS 4.0でかつUIScrollViewの配下になっているviewに対して取り付けた場合、常にCGPointZeroを返してしまうようです。iOS 4.1のシミュレータで確認したら直ってましたので、iOS 4.0限定だと思われます。というわけでvelocityプロパティをかわりに使っておくことをお進めします。

検証結果はこちら

[NSObject load] と [NSObject initialize] の違い

クラスがObjective-Cのランタイムにロードされ利用可能になったタイミングで、そのクラス全体の初期化を行いたいということはよくあると思います。Objective-CではNSObjectクラスの以下のメソッドを用いてクラス全体の初期化を行うことができます。
  • + load
  • + initialize
この2つですが、結構挙動が異なります。詳細については以下のとおり。
http://cocoawithlove.com/2008/03/cocoa-application-startup.html
  • loadメソッドはクラスがロードされて利用可能になったら即座に呼び出される。
    • このとき、自分以外の他のクラスはまだロードされていない可能性があるので、自分以外のクラスを利用するような初期化はできない。
    • main関数の内部のNSAutoReleasePoolが用意されるよりも先に呼び出されるので、autoreleaseを使うような初期化を行う場合には自分でNSAutoReleasePoolを生成して管理する必要がある
  • initializeメソッドはそのクラスに実際のアクセスが最初に発生したタイミングで呼び出される。
    • 要するに一度も使われないクラスでは呼び出されない。
    • 自分以外のクラスもロードが完了しているので、自由に他のクラスを利用できる。
    • autoreleaseについても特に気にしなくて良い。
基本はinitializeメソッドを使うほうがより安全で確実なうえに、使われないなら初期化されないので経済的でいい感じです。こちらを使うことをお勧めします。

またloadメソッドについては、iOS実機で自家製frameworkを使っているを使っているとき、framework内部にビルドされているクラスのloadメソッドが呼び出されないという問題があります(静的ライブラリ.aについては未検証)iOSシミュレータおよびMacではきちんとframeworkに含まれているクラスについてもloadメソッドが呼び出されるのですが・・・ともかく地雷が大きいので避けたほうが懸命です。

[UIView willMoveToSuperview:] が便利です

UIKitやFoundationには、iOS 2.0のころから存在するのに、意外と知られていない便利なメソッドやプロパティがたくさんあります。今回はUIViewのメソッドをご紹介します。

UIViewはUIViewControllerと違ってライフサイクルが単純で、どのタイミングで自分自身が画面上に追加されたのか、どのタイミングで自分自身が画面から外されたのか、などを把握しづらいとお嘆きの方がいらっしゃると思います。事実その用途のためだけにUIViewControllerを使ってプログラミングをしている人も見かけます。そこで以下のメソッドをご紹介です。
  • willMoveToSuperview:
    • 自分自身が新しいSuperview以下に移動しようとしたとき(新しいSuperviewに対してaddSubview:されようとしたとき)に呼び出されます。
  • didMoveToSuperview
    • 自分自身が新しいSuperview以下に移動したとき(新しいSuperviewにaddSubview:されたとき)に呼び出されます。
  • willMoveToWindow:
    • 自分自身が新しいWindow以下に移動しようとしたとき(新しいWindowに対してaddSubview:されようとしたとき)に呼び出されます。
  • didMoveToWindow
    • 自分自身が新しいWindow以下に移動したとき(新しいWindowに対してaddSubview:されたとき)に呼び出されます。
  • didAddSubview:
    • 自分自身に他のviewがsubviewとして追加されたときに呼び出されます。
  • willRemoveSubview:
    • 自分自身のsubviewsから他のviewが取り除かれようとしているときに呼び出されます。
これらのメソッドをUIViewのサブクラスでオーバーライドすることにより、かなりの自由度でviewの動きをコントロールすることができます。
たとえば自作のUIViewで、画面にviewが追加されたタイミングで何かしたい・・・というときなどは以下のようにできます:
- (void)willMoveToSuperview:(UIView *)newSuperview

{
NSLog(@" * superview = %@", newSuperview);
NSLog(@" * superview's window = %@", newSuperview.window);
// UIViewControllerでいうところの loadView 兼 viewDidLoad 兼 viewWillAppear 兼 viewWillDisappearみたいなタイミング
}

- (void)didMoveToSuperview
{
// UIViewControllerでいうところの viewDidAppear 兼 viewDidDisappear みたいなタイミング
// ここで、もしsuperviewがあり(画面に表示される可能性があり)、まだ自分自身のデータが初期化されていない場合には
// reloadDataして初期表示データを読み込む
// superviewがない場合には画面から外されたのですべてのビューまわりをリセットして、次の表示に備えるようにしておく
if (self.superview) {
if (!self.someData) {
[self reloadData];
}
} else {
self.someData = nil;
[self __resetOutlets];
}
}

- (void)willMoveToWindow:(UIWindow *)newWindow
{
NSLog(@" * window = %@", newWindow);
// いまいち使いづらいのでwillMoveToSuperviewとかを使うようにしてます
}

- (void)didMoveToWindow
{
// いまいち使いづらいのでdidMoveToSuperviewを使うようにしてます
}
これでUIViewの使い勝手もアップ!ですね。

2011年8月9日火曜日

[UITableViewController scrollToRowAtIndexPath:atScrollPosition:animated:] の挙動まとめ

UITableViewController の scrollToRowAtIndexPath:atScrollPosition:animated: メソッドは、対象のテーブルビューのセクションにヘッダ・フッタが付いている場合挙動が変化する事がわかったので、ちょっと調査してまとめてみました。具体的には以下のような動きをするようです。

  • このメソッドは自分で呼び出すか、またはテーブルビューのセルの中に UITextField のようなフォーカスを取るコントロールを配置して、それが選択されたときに呼び出される
  • このメソッドで指定した indexPath の section に Header View or Header Text / Footer View or Footer Text が指定されているとき、このメソッドは選択された indexPath の row だけではなく、それらのヘッダやフッタも同時に表示される位置にスクロールしようとする
  • ということであんまり長い Section Header / Section Footer を作ると scrollToRowAtIndexPath:atScrollPosition:animated: の挙動がおかしくなる
  • Table View Header / Table View Footer については全然無関係なので長くしても大丈夫
画像にすると以下のような感じになります。

初期状態

section3つ、row3つ、合計9行のtable viewを作って、それぞれにsection header / section footerを追加しました。このテーブルビューを使って実験を行います。

UITableViewScrollPositionTopを指定してスクロール
section0, row0section0, row1
section0, row2
section1, row0

UITableViewScrollPositionTopを指定すると、sectionの一番上のrowが指定された場合のみ、そのsectionのsection headerの高さを考慮してスクロールするようになります。

UITableViewScrollPositionMiddleを指定してスクロール
section0, row0section0, row1section0, row2
section1, row0section1, row1section1, row2

UITableViewScrollPositionMiddleの場合は特にsection header / section footer関係なく、中央に選択された行が来るようにスクロールするようです。

UITableViewScrollPositionBottomを指定してスクロール
section0, row0section0, row1section0, row2
section1, row0section1, row1
section1, row2

UITableViewScrollPositionBottomを指定すると、sectionの一番下のrowが指定された場合のみ、そのsectionのsection footerの高さを考慮してスクロールするようになります。

2011年8月5日金曜日

自分流 View Controllerの作り方 その2



その1はこちら

ぼくのかんがえたさいきょうのせっけいです
主に以下の書籍に影響受けまくりであります
0321127420Patterns of Enterprise Application Architecture (Addison-Wesley Signature Series (Fowler))
Martin Fowler
Addison-Wesley Professional 2002-11-05

by G-Tools
4798116831レガシーコード改善ガイド (Object Oriented SELECTION)
マイケル・C・フェザーズ ウルシステムズ株式会社
翔泳社 2009-07-14

by G-Tools


図を適当に補足
ViewWrapperは既存のすでにあるどうしようもない設計のViewを何とか救いたいときに非常に便利、Wraper / Decoratorパターンを適用してボタンのタップを奪い取ってViewHelperに流すみたいな役目をする
ViewHelperは簡単に言うならUITableViewControllerのdelegate, datasourceだけを担うオブジェクトみたいな感じ。要するにView専用のドメインロジックを書くオブジェクト
Viewの表示を制御するドメインロジックが途方もなく大きくてViewControllerに納めるのが不可能になってしまったときに超便利

Serviceは準ドメインロジックだと思っている、たいていの場合セミシングルトンみたいにする(通信が絡むので複数画面をまたいで使うことがほとんど)
Androidの場合はここ、普通にServiceクラスでいいんじゃないでしょうかね

Managerはドメインロジックというよりもプロセス外へのリソースアクセスを行うためのクラスというイメージ、個人的にはゲートウェイみたいな感じ
  • NSFileManager
  • NSUserDefaults
  • KeychainAccessManager
  • InAppPurchaseManager
  • APIConnectionManager
Modelはドメインモデルです、ほとんどの場合はCoreDataのNSManagedObject。用がないときでもCoreData使っておけ(超便利)
と思ってましたがCoreDataのモデルは特にN:N関係を正しく扱わないと簡単に問題が発生してしまいますので、安易に採用すると危険かも。

Task, Operationってのは非同期で実行されていく特別なドメインロジックのイメージ。要するにAPI通信みたいなもんです
API通信を複数束ねて使ったりとか並列実行したりとかの制御が絶対必要になるのでそういうときに使う via @monjudoh
// MyTaskが終わったらMySuperTaskを実行して、それが終わったらさらにMySuperDuperTaskを連続して実行したい
// 終わったらselfに通知させたい
Task *root = [[[MyTask alloc] init] autorelease];
root.nextTask = [[[MySuperTask alloc] init] autorelease];
root.nextTask.nextTask = [[[MySuperDuperTask alloc] initWithId:100] autorelease];
root.delegate = self;
[root start];

自分流 View Controllerの作り方 その1



その2はこちら

以前勉強会の際に発表した View Controller の作り方のメモをまとめてみました。あくまでメモなので中身はうまくまとまっていませんが、何かのご参考になればと思います。




通信が絡んでくると、たいていの人がやりがちな問題(実例)
  • API通信のレスポンスを処理するコードがViewControllerの中に入っている
  • API通信が3種類必要で、Aを実行したあとにBとCを実行しなければならないとか
  • ABCのレスポンスJSONのパースまでViewControllerでやっている
  • というかAPIの呼び出しの組み立てだとかURLの指定だとか自体がIBActionの中に入っていたりする
API通信だけじゃなくてIn App Purchaseなどでも同様の事例が見られる

それに対する対応策。そもそもなぜこのような問題が発生するのか?
  1. Outletの生成・更新・レイアウトが分離されていない
    • そのため複数回画面が更新されるタイミングが発生するととたんに破綻する
    • 大変よく見かける初心者コードが"drawXXX"という名前のOutletを生成してデータをセットしてframeまでセットして画面に配置するコード
    • Outletを描画コードと勘違いしている。Outletはペンやブラシに相当するものであって、実際に線を書くコードではない
    • この初心者コードでも動く唯一の理由は画面が一回(viewWillAppear時)しか更新されないから
  2. 通信という(比較的大きくなりがちな)ドメインロジックがViewControllerに混入している
そこで問題1.に対応するためにViewControllerの中でやる作業を以下のように分割する
  • Outletを生成する
    • preload(一度に生成する方法)
    • lazy load(必要になったら生成し、必要でなくなったら捨てる方法)
  • Outletのプロパティを更新する
  • Outletをレイアウトする
これらはそれぞれ(基本的に)以下のようなUIViewControllerのメソッドが対応する
  • loadView
    • Outletをpreloadする場合はコレで全く問題ない。このとき、self.viewとここで作られたOutletの生存期間は等しくなる
    • Outletをlazy loadする場合はOutletを生成するコードと削除するコードを用意しておいて、必要なタイミングで呼び出すとかする
  • なし
    • プロパティを更新するために、たとえばupdateOutletsみたいなメソッドを自分で用意してやる
  • 各種willRotate...系メソッド
    • willRotateほげほげの中にレイアウトコードを入れておくと自動的に画面の回転にも対応できて超便利
    • 自動的に必要なタイミングで適切に呼び出ししてくれて超便利
    • そういうのが嫌いな人はlayoutOutletsForInterfaceOrientation:みたいなメソッドでも作ればいいんじゃないでしょうか
次に問題2.に対応するためにAPIの呼び出しやファイルアクセスなどはService, Managerなどの層を作ってそちらに任せる
決してViewControllerの中にドメインロジックを混入させないのが大事
→混入するとドメインロジックとビューナビゲーションロジックが混ざって大爆発する
→さらにドメインロジック自身も複雑な通信が必要になると大爆発する

それとは別にイベントを受け取ってViewの状態を制御する大事なお仕事をする必要がある
  • ボタンタップしたり
  • 画面をタップしたりパンしたり
  • スクロールが発生したり
  • APIコールが完了したり
  • In App Purchaseが完了したり
ここまでがView Controllerのお仕事。決してドメインロジックを混ぜないのがポイント

2011年7月18日月曜日

BPStudy #46 での発表資料 ViewController の作り方を公開してます

かなり旧聞になってしまいますが、 BPStudy #46 で話したときの資料をpreziにアップしてますので、ご覧いただけます。
http://prezi.com/7b_joy2lcfil/bpstudy-46/

発表時にお見せしたソースコードはちょっと公開出来なかったのでこの資料だけだとイマイチよくわからないのですが、要するに
  • View Controllerにサービスを書かないようにしてサービスレイヤを分離しろ
  • View Controllerの描画コードは、アウトレットの生成、アウトレットのデータ更新、アウトレットのレイアウトの3つに分離しろ
って言う感じのお話をしました。

すげー当たり前のことしか書いてなくて恐縮なのですが><
実際、人様のコードをメンテしたりバージョンアップしたりするお仕事をいただいていると、ほとんど全てのコードがこの問題に引っかかっていて非同期通信化すると動かないとかちょっとView Controllerの呼び出され方を変えると動かないとかあって、iOS開発者の皆さんがこういうところに気を配ってコードを書いていただければなぁそして私が楽できればなぁという思いで書いてみた次第であります。

2011年4月23日土曜日

KeychainItemWrapper を改造して、複数の Keychain Item に同時にアクセス出来るようにしてみた

※2011/04/23現在での情報です。以下の問題は全て Apple に対して報告いたしましたので、ひょっとしたら将来的に修正されるかもしれません。


現在の仕事にて課金情報を安全にアプリケーション内に保存したいということになり、それならばということで iOS に最初から用意されている Security.framework を使って Keychain 領域に情報を保存するアプリを作ることにしました。この Security.framework は全てC言語にて実装されており、そのまま使うと非常に取っつきづらいです。そこで、 Apple がサンプルコードとして提供している GenericKeychain プロジェクトKeychainItemWrapper クラスを使って Keychain 領域にアクセスすることにしました。

最初は問題ないように見えたのですが、実はこの KeychainItemWrapper クラス、Version 1.2には深刻なバグがあります。それは、複数の KeychainItemWrapper クラスのインスタンスを使って複数の識別子を持った情報を保存しようとすると、同時に一種類しか保存できなくなる(二つめを保存しようとするとステータスコード25299が返却されて保存できなくなる)というものです。

原因を調査した結果、以下のページに記載されている内容が問題だとわかりました。
http://useyourloaf.com/blog/2010/3/29/simple-iphone-keychain-access.html
The combination of the final two attributes kSecAttrAccount and kSecAttrService should be set to something unique for this keychain. In this example I set the service name to a static string and reuse the identifier as the account name.
サンプルで提供されている KeychainItemWrapper クラスにはこれらの一意となるキーが指定されておらず、単に検索用の kSecAttrGeneric だけが指定されていたため、一意キーの指定がおかしくなって二つ目の Keychain アイテムが保存できなくなっていた、というわけです。

そこでこちらの問題を修正した KeychainItemWrapper クラスをご用意しました。
https://gist.github.com/938375

ライセンスは元となるAppleのオープンソースライセンスに準拠します。改変・再配布・利用すべて自由ですが、その際ソースコードの最上部にあるAppleのライセンス条項コードは絶対に改変しないでください。Apple先生に何か言われても私は知りませんよ><


■修正版 KeychainItemWrapper の使い方

基本的には通常の KeychainItemWrapper と何ら変わりません。
// インスタンスを作る
KeychainItemWrapper *wrapper = [[KeychainItemWrapper alloc] initWithIdentifier:@"com.akisute.keychain.KeychainTestApp.item1"
serviceName:@"com.akisute.KeychainTestApp"
accessGroup:nil];

// 値を保存する
// いったんresetKeychainItemしているのはmyValueがnilのとき保存されているデータを削除したいから
// 基本的に値(kSecValueData)として保存できるのはUTF-8のNSStringのみです。内部的にNSDataに変換されてKeychainに保存されます。
// それ以外の情報を保存したい場合はすみませんが未対応です>< Base64文字列にするとかJSON文字列にするとか工夫して回避してください><
NSString *myValue = @"abesi";
[wrapper resetKeychainItem];
[wrapper setObject:myValue forKey:(id)kSecValueData];

// 値を取り出す
NSString *resultValue = [wrapper objectForKey:(id)kSecValueData];
NSLog(@"resultvalue = %@", resultValue);

2011年4月17日日曜日

UINavigationController に管理されている UIViewController の view のサイズを変更したい

たとえばアプリ内の固定の位置に広告を突っ込みたいときなど、 UINavigationController に管理されている UIViewController の view 構造を操作したい場合があると思いますので、調べてみました。


■UINavigationController.viewの中身

iOS 4 ですと以下のようになっています。



最上位の UINavigationController.view に相当するのが UILayoutContainerView で、その下に UINavigationTransitionView があり、そのまた下に UIViewControllerWrapperView というのがあって、こいつが表示される UIViewController.view をラップして表示しているという仕組みになっているようです。

おそらく iOS 3.2 でも同じだと思います。 iOS 3.1.3 以前についてはわかりません。


■実際にやってみる

いろいろ試してみた結果、もっともうまくいきそうなのは以下のように、 viewDidAppear のタイミングで UIViewController.view.superview に対して操作を行う方法であることがわかりました。
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
self.headerView.frame = CGRectMake(0, 0, 320, 100);
self.view.frame = CGRectMake(0, 100, 320, self.view.superview.frame.size.height - 100);
[self.view.superview addSubview:self.headerView];
}

■問題発生

ところがこの方法では画面遷移時のアニメーションが発生すると上記の例で追加した headerView の表示が無茶苦茶に壊れてしまいます。またタイミングを誤ると headerView のサイズが self.view.superview.bounds の大きさに補正されてしまいます。恐らくは UINavigationTransitionView とかいう private な view がなにやらやっているのでしょう。アニメーションが発生しない範囲であれば問題なさそうですが、いまいちです。


■結論

横着しないで表示される UIViewController の view で何とかするしかないみたいですね><

2011年2月28日月曜日

UIWebView のスクロールを制御するためのプロパティを書いてみた

UIWebView にどうして scrollEnabled プロパティがついてねえんだ Apple のチンパンジー野郎!とお嘆きの全国1000万の iOS 開発者の皆様、こんばんわ。もちろん私もその一人であります。

嘆いていてもしょうがないので何とかスクロールを制御する方法を・・・と思って探していたら、すでに2009年の地点で @nakamura001 さんがこんなブログを書いてらっしゃいました。

http://d.hatena.ne.jp/nakamura001/20090520/1242837408

が、遷移先で詳解されている

http://praveenmatanam.wordpress.com/2009/04/03/how-to-disable-uiwebview-from-user-scrolling/

のコードが正直いまいちなのです。何がいまいちって、せっかくのCocoa環境であるにも関わらず、わざわざobjc/runtime.hなんていう低レベルなC言語の関数を使っています。別にパフォーマンスがタイトな場所でもないですし、かっこよくCocoaっぽく書き直してみました。ということで書き直したコードがこちら↓

https://gist.github.com/846258

で、このAdditionを導入するとですね、
UIWebView *webView = [[[UIWebView alloc] initWithFrame:frame] autorelease];
webView.webViewScrollEnabled = NO;
みたいな書き方ができてハッピーになれます。

内部的には NSInvocation を使っています。 Mac OS X 10.5 (iOS 2.0) から存在するこのクラス、本当に便利で、ぶっちゃけ呼び出し対象のシグネチャさえわかれば何でも呼び出せるスグレモノです。フレームワークを作るときなど、呼び出し先のシグネチャしかわからない状況下で対象のメソッドを呼び出す時などに便利な感じかもです。その上 NSInvocationOperation を使ってそのまま並列化もできたりして。
NSMethodSignature *sig = [subview methodSignatureForSelector:selector];
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature:sig];
[invocation setTarget:subview];
[invocation setSelector:selector];
[invocation invoke];
BOOL result = YES;
[invocation getReturnValue:&result];
return result;