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月23日水曜日

静的ライブラリ中のシグネチャが衝突してビルドできないときに再ビルドしないでシグネチャを書き換える

皆さんも以下のようなビルドエラーを見たことが一度はあると思います。

これはビルド時に同一プロジェクト内に同じ名前のシグネチャの関数やクラスが存在するためリンクができなくて失敗しているというエラーです。特に以下のようなケースでよく発生します。
  • 自分が作ったクラスや関数の名前と、外部から持ってきたライブラリが使っているクラスや関数の名前が衝突している
  • 外部から持ってきたライブラリ同士でクラスや関数の名前が衝突している
  • 外部ライブラリをインストールする際に、-all_loadしたり-ObjCしたりている
そういうわけで、外部からライブラリをたくさん導入すると、base64やMD5など、プログラム上でよく使われるのに標準で用意されていないライブラリがよく衝突してしまうわけです。大抵の場合はぶつかっているシグネチャの名前をソースコード上でちょっと書き換えて再度ビルドすることで回避ができるのですが、極稀にソースコードを書き換えることができないケースが存在します。以下にそんな場合の対処方法をまとめます。


■具体例
AdMobのSDK(libGoogleAdMob.a)とopenssl(libcrypto.a)を同時に一つのプロジェクトにインストールした時、冒頭の画像のようにMD5というシグネチャがビルド時に衝突してしまうのです。AdMobのはプロプロエタリなので当然書き換えられませんし、opensslのように巨大で複雑なコードに手を入れて再ビルドするのも非常に危険です。そもそもopensslはビルド自体が難しいのです。

このような場合は、ソースコード自体を書き換えるのではなくビルド済みの静的ライブラリ側のオブジェクトファイルを書き換えることで対処を行うことが可能です。
コンパイル時のリンカの設定を変更すれば対処できそうな気もするんですが、GNU ldにはそのようなオプションが見当たりませんでした。なんかSun Solarisのldだと対処できるみたいです。
参考: http://stackoverflow.com/questions/6940384/how-to-deal-with-symbol-collisions-between-statically-linked-libraries
参考: http://stackoverflow.com/questions/393980/restricting-symbols-in-a-linux-static-library

注意: 以下の手順は失敗すると静的ライブラリ自体が完全に破損したり、実行時に深刻な問題が発生する可能性があります!! 以下の解説を見てもちんぷんかんぷんな人は適用しないことを強くおすすめいたします。この手順を適用したことによって発生したいかなる損害についても私は責任をとりかねます。


■静的ライブラリ内部のシグネチャを書き換える方法
静的ライブラリ内部のシグネチャを書き換えるには、以下のようなツールを使用します。

lipo
以前にもご紹介した、Apple純正のユニバーサルバイナリ/ユニバーサルライブラリ(fat binaryともいいます)を作ったりバラしたりするツールです。iOSのライブラリはほぼすべてがi386, armv6, armv7の三種類に対応するfat binaryになっており、基本的にApple純正でないツールはそういったfat binaryに対して歯が立たないので、まずこのツールを使って普通のライブラリに戻した上で、以下のツールを使うわけです。

nm
こいつもApple純正です。バイナリの中に入っているシグネチャの名前を一覧表示することができます。Apple純正なのでfat binaryに対しても使えて超便利です。

objdump
GNU objdumpというツールがありまして、こいつを使うとバイナリの詳細な中身を覗き見ることができます。nmよりも表示される情報が詳細です。Apple純正ではないので、以下で紹介されているようにしてMacPorts経由でインストールするのをお勧めします。
http://d.hatena.ne.jp/amachang/20080401/1207027290

objcopy
GNU objcopyです。ライブラリ内部のシグネチャを書き換える事ができるツールで、objdumpとセットでついてくるのですが、残念ながらiOS向けのバイナリに対しては全く使えません。話すと長くなるのですがバージョンを変えようがarm向けのセットをインストールしようがarでライブラリからオブジェクトファイルを取り出して試みてみようが何やっても一切無駄です。使えませんので諦めてください。
参考: http://www.mail-archive.com/bug-binutils@gnu.org/msg02829.html
参考: http://stackoverflow.com/questions/2231698/how-can-i-easily-install-arm-elf-gcc-on-os-x

objconv
で、使えないobjcopyに代わって、今回の英雄です。こいつを使って実際にライブラリ内部のシグネチャを自由自在に書き換えることができます。メンテもしっかり行われているようで動作も安定しています。以下のサイトからダウンロードできます。
サイト: http://www.agner.org/optimize/#objconv
マニュアル: http://www.agner.org/optimize/objconv-instructions.pdf
残念ながらsource配布のみしか無いため、自分でビルドしてやる必要があります。と言ってもそこそこ簡単で、以下のようにするだけです。
  • ソースコードをダウンロードする
  • zipを解凍する
  • source.zipを解凍する
  • build.shを実行する。ただしこのシェルスクリプトは1行目だけがひどくバグってるので、自分で中を見てビルドコマンドを叩いてやるようにしてください。ね?簡単でしょう?
さて、これで実際にシグネチャの書き換えを行う準備が整いました。


■実践:全く同一のライブラリのシグネチャだった場合
base64なんかでよく発生します。この場合は片方をweakシンボルにします。
weakシンボルとは: http://d.hatena.ne.jp/syohex/20100610/1276180481 がわかりやすいです。

早速やってみましょう。以下の例ではlibTest.a中のbase64_encodeシグネチャを書き換えます。
まずは以下のコマンドで対象のライブラリのfat binaryを通常のバイナリに戻して:
lipo -thin armv6 libTest.a -output libTest_armv6.a
lipo -thin armv7 libTest.a -output libTest_armv7.a
lipo -thin i386 libTest.a -output libTest_i386.a
objconvを実行:
objconv -fmacho -nw:base64_encode libTest_armv6.a
objconv -fmacho -nw:base64_encode libTest_armv7.a
objconv -fmacho -nw:base64_encode libTest_i386.a
lipoで元通りに戻します:
lipo -create libTest_i386.a libTest_armv6.a libTest_armv7.a -output libTest.a
これでビルドが通るはずです。


■実践:同じ名前の違うライブラリのシグネチャだった場合
冒頭のMD5のケースがこれです。名前が同じなのに実装がまるで違うので、weakシンボルにすると深刻なバグが発生します。こういう時は慎重に見定めた上で、使われていないと思われる方のシグネチャをhiddenシンボル(ローカルシンボル)にして、外部ファイルからリンクできないようにしてしまいます。これなら実装は存在しますがリンクされないようになるだけなので、対象のシンボルが外部から使われていないのであればこれだけでいけます。

今度はlibcrypto.a中の_MD5シグネチャを書き換えてみましょう。
こちらもまずはlipoを使って通常のバイナリに戻して:
lipo -thin armv6 libcrypto.a -output libcrypto_armv6.a
lipo -thin armv7 libcrypto.a -output libcrypto_armv7.a
lipo -thin i386 libcrypto.a -output libcrypto_i386.a
objconvを実行:
objconv -fmacho -nl:_MD5 libcrypto_armv6.a
objconv -fmacho -nl:_MD5 libcrypto_armv7.a
objconv -fmacho -nl:_MD5 libcrypto_i386.a
lipoで元通りにして完成:
lipo -create libcrypto_i386.a libcrypto_armv6.a libcrypto_armv7.a -output libcrypto.a
これで無事ビルドが通りました。


■実践:同じ名前の違うライブラリのシグネチャで、かつ外からバリバリ呼ばれていた場合

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年11月18日金曜日

iOS 5の日本語キーボードの高さに対応する (iOS 3, 4, 5全対応)

iOS 5より日本語キーボードの高さが変わっているので、今まで決め打ちで高さ216pxとかやってレイアウトしていたビューが軒並み使えなくなってしまいました。今後はキーボードが出たり引っ込んだり種類が切り替わったりのタイミングできちんとキーボードの大きさを調べて適切にビューをレイアウトしてやる必要があります。ということでその対応をしたのでメモ。

前提条件として、以下の要件を満たすように作りました。
  • iOS 3, 4, 5全てで正常に動作すること。iOS 3.0でも動作しなければならない。
  • キーボードのframeを適切に取得できること
  • キーボードが出てくるタイミング、消えるタイミング、キーボードの種類が変わるタイミング、全て取れること

■まずはログを見てみる
キーボードの動作のタイミング、およびキーボードのframeは、NSNotificationを使って取得することができます。使用するNotification名はUIWindowのドキュメントに以下のように定義されています。
http://developer.apple.com/library/ios/#documentation/UIKit/Reference/UIWindow_Class/UIWindowClassReference/UIWindowClassReference.html
// Available from iOS 2
UIKeyboardWillShowNotification
UIKeyboardDidShowNotification
UIKeyboardWillHideNotification
UIKeyboardDidHideNotification
// Available from iOS 5
UIKeyboardWillChangeFrameNotification
UIKeyboardDidChangeFrameNotification
で、これらのNotification名でNSNotificationCenterにobserverを追加すると、通知が飛んできます。飛んできたNSNotificationオブジェクトのuserInfoプロパティに特定のキーでキーボードのframeが格納されているというしくみです。使えるキーは以下のとおり。
// Available from iOS 3.2
UIKeyboardFrameBeginUserInfoKey
UIKeyboardFrameEndUserInfoKey
// Available from iOS 3.0
UIKeyboardAnimationCurveUserInfoKey
UIKeyboardAnimationDurationUserInfoKey
// Available from iOS 2.0 ~ 3.2 (Deprecated in newer versions)
UIKeyboardCenterBeginUserInfoKey
UIKeyboardCenterEndUserInfoKey
UIKeyboardBoundsUserInfoKey
iOS 3, 4, 5すべてできちんと動作しなければならないので、これらをふまえて、以下のように実装します。
  • Notification名にはUIKeyboardWillShowNotification, UIKeyboardDidShowNotification, UIKeyboardWillHideNotification, UIKeyboardDidHideNotificationを使う。
  • userInfoのキーには、iOS 3.0/3.1のみUIKeyboardBoundsUserInfoKeyを使い、それ以外のバージョンではUIKeyboardFrameEndUserInfoKeyを使う。

■実装してみる
ということでまずはサンプルアプリを作って動かしてみて、実際に動作を見てみることにしました。大体こんな感じのコードです。
- (void)viewWillAppear:(BOOL)animated
{
  // Notification observerを追加する
  [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onUIKeyboardWillShowNotification:) name:UIKeyboardWillShowNotification object:nil];
  [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onUIKeyboardDidShowNotification:) name:UIKeyboardDidShowNotification object:nil];
  [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onUIKeyboardWillHideNotification:) name:UIKeyboardWillHideNotification object:nil];
  [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onUIKeyboardDidHideNotification:) name:UIKeyboardDidHideNotification object:nil];
}
- (void)viewWillDisappear:(BOOL)animated
{
  // Notification observerを削除する
  [[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)onUIKeyboardWillShowNotification:(NSNotification *)notification
{
  NSLog(@"%s", __func__);
  NSLog(@"  * userInfo = %@", notification.userInfo);
  // UIKeyboardFrameEndUserInfoKeyが使える時と使えない時で処理を分ける
  CGRect bounds;
  if (&UIKeyboardFrameEndUserInfoKey == NULL) {
    // iOS 3.0 or 3.1
    // bounds
    bounds = [[notification.userInfo objectForKey:UIKeyboardBoundsUserInfoKey] CGRectValue];
  } else {
    // それ以外
    // frameだがoriginを使わないのでbounds扱いで良い
    bounds = [[notification.userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
  }
  // boundsを使った処理をここに書く
}

ログはこんな感じに。

iOS 5.0, iPhone 4S
2011-10-19 10:35:15.007 SampleApp[5675:707] -[SampleViewController viewWillAppear:]
// 前の画面の英字キーボードが一旦引っ込んで出てきている
2011-10-19 10:35:15.502 SampleApp[5675:707] -[SampleViewController onUIKeyboardDidHideNotification:]
2011-10-19 10:35:15.506 SampleApp[5675:707]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = "0.25";
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 216}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 588}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 480}, {320, 216}}";
}
2011-10-19 10:35:15.509 SampleApp[5675:707] -[SampleViewController onUIKeyboardWillShowNotification:]
2011-10-19 10:35:15.510 SampleApp[5675:707]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = "0.35";
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 216}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
}
2011-10-19 10:35:15.513 SampleApp[5675:707] -[SampleViewController onUIKeyboardDidShowNotification:]
2011-10-19 10:35:15.514 SampleApp[5675:707]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = "0.35";
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 216}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
}
2011-10-19 10:35:16.162 SampleApp[5675:707] -[SampleViewController viewDidAppear:]
// キーボード引っ込める
2011-10-19 10:35:44.246 SampleApp[5675:707] -[SampleViewController onUIKeyboardWillHideNotification:]
2011-10-19 10:35:44.247 SampleApp[5675:707]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = "0.25";
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 216}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 588}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 480}, {320, 216}}";
}
2011-10-19 10:35:44.509 SampleApp[5675:707] -[SampleViewController onUIKeyboardDidHideNotification:]
2011-10-19 10:35:44.511 SampleApp[5675:707]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = "0.25";
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 216}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 588}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 480}, {320, 216}}";
}
// キーボード出す
2011-10-19 10:35:55.135 SampleApp[5675:707] -[SampleViewController onUIKeyboardWillShowNotification:]
2011-10-19 10:35:55.136 SampleApp[5675:707]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = "0.25";
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 216}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 588}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 480}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
}
2011-10-19 10:35:55.397 SampleApp[5675:707] -[SampleViewController onUIKeyboardDidShowNotification:]
2011-10-19 10:35:55.398 SampleApp[5675:707]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = "0.25";
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 216}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 588}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 480}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
}
// 日本語キーボードに変更
2011-10-19 10:35:58.167 SampleApp[5675:707] -[SampleViewController onUIKeyboardWillShowNotification:]
2011-10-19 10:35:58.168 SampleApp[5675:707]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = 0;
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 252}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 390}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 354}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 228}, {320, 252}}";
}
2011-10-19 10:35:58.170 SampleApp[5675:707] -[SampleViewController onUIKeyboardDidShowNotification:]
2011-10-19 10:35:58.171 SampleApp[5675:707]   * userInfo = {
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 252}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 390}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 354}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 228}, {320, 252}}";
}
// 英語キーボードに変更
2011-10-19 10:36:00.483 SampleApp[5675:707] -[SampleViewController onUIKeyboardWillShowNotification:]
2011-10-19 10:36:00.484 SampleApp[5675:707]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = 0;
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 216}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 336}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 228}, {320, 252}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
}
2011-10-19 10:36:00.485 SampleApp[5675:707] -[SampleViewController onUIKeyboardDidShowNotification:]
2011-10-19 10:36:00.486 SampleApp[5675:707]   * userInfo = {
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 216}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 336}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 228}, {320, 252}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
}
// 画面を抜ける、キーボードが隠れる通知より先にviewWillDissappearされるので通知が来ない
2011-10-19 10:36:03.570 SampleApp[5675:707] -[SampleViewController viewWillDisappear:]
2011-10-19 10:36:03.986 SampleApp[5675:707] -[SampleViewController viewDidDisappear:]
iOS 3.1.3, iPhone 3G
2011-10-19 10:43:46.548 SampleApp[352:207] -[SampleViewController viewWillAppear:]
// 前の画面の英字キーボードが一旦引っ込んで出てきているのだが、iOS 3.1.3では引っ込む側の挙動が見られない。出てくるだけになっているようだ。
// さらにDidShowの通知がキーボードがviewDidAppearの呼び出しのあとに行われるようになっている。
// どうやらキーボードがどこに属しているのかが違うみたいだな。
2011-10-19 10:43:47.202 SampleApp[352:207] -[SampleViewController onUIKeyboardWillShowNotification:]
2011-10-19 10:43:47.210 SampleApp[352:207]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = 0.35;
    UIKeyboardBoundsUserInfoKey = NSRect: {{0, 0}, {320, 216}};
    UIKeyboardCenterBeginUserInfoKey = NSPoint: {480, 372};
    UIKeyboardCenterEndUserInfoKey = NSPoint: {160, 372};
}
2011-10-19 10:43:48.253 SampleApp[352:207] -[SampleViewController viewDidAppear:]
2011-10-19 10:43:48.315 SampleApp[352:207] -[SampleViewController onUIKeyboardDidShowNotification:]
2011-10-19 10:43:48.336 SampleApp[352:207]   * userInfo = {
    UIKeyboardBoundsUserInfoKey = NSRect: {{0, 0}, {320, 216}};
    UIKeyboardCenterBeginUserInfoKey = NSPoint: {160, 588};
    UIKeyboardCenterEndUserInfoKey = NSPoint: {160, 372};
}
// キーボード隠す
2011-10-19 10:43:52.854 SampleApp[352:207] -[SampleViewController onUIKeyboardWillHideNotification:]
2011-10-19 10:43:52.862 SampleApp[352:207]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = 0.300000011920929;
    UIKeyboardBoundsUserInfoKey = NSRect: {{0, 0}, {320, 216}};
    UIKeyboardCenterBeginUserInfoKey = NSPoint: {160, 372};
    UIKeyboardCenterEndUserInfoKey = NSPoint: {160, 588};
}
2011-10-19 10:43:53.183 SampleApp[352:207] -[SampleViewController onUIKeyboardDidHideNotification:]
2011-10-19 10:43:53.188 SampleApp[352:207]   * userInfo = {
    UIKeyboardBoundsUserInfoKey = NSRect: {{0, 0}, {320, 216}};
    UIKeyboardCenterBeginUserInfoKey = NSPoint: {160, 372};
    UIKeyboardCenterEndUserInfoKey = NSPoint: {160, 588};
}
// キーボード出す
2011-10-19 10:43:54.621 SampleApp[352:207] -[SampleViewController onUIKeyboardWillShowNotification:]
2011-10-19 10:43:54.629 SampleApp[352:207]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = 0.300000011920929;
    UIKeyboardBoundsUserInfoKey = NSRect: {{0, 0}, {320, 216}};
    UIKeyboardCenterBeginUserInfoKey = NSPoint: {160, 588};
    UIKeyboardCenterEndUserInfoKey = NSPoint: {160, 372};
}
2011-10-19 10:43:54.972 SampleApp[352:207] -[SampleViewController onUIKeyboardDidShowNotification:]
2011-10-19 10:43:54.977 SampleApp[352:207]   * userInfo = {
    UIKeyboardBoundsUserInfoKey = NSRect: {{0, 0}, {320, 216}};
    UIKeyboardCenterBeginUserInfoKey = NSPoint: {160, 588};
    UIKeyboardCenterEndUserInfoKey = NSPoint: {160, 372};
}
// キーボードの種類をここで英語→日本語、日本語→英語に変えているのだが、通知が来ない
// だがiOS 3ではキーボードの種類によって高さが違うということが(基本)ないので気にしなくて良い
[Switching to process 11779 thread 0x2e03]
warning: No copy of  found locally, reading from memory on remote device.  This may slow down the debug session.
// 画面抜ける、viewWillDisappearより先にKeyboardが隠れる通知が来る
2011-10-19 10:44:07.442 SampleApp[352:207] -[SampleViewController onUIKeyboardWillHideNotification:]
2011-10-19 10:44:07.454 SampleApp[352:207]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = 0.35;
    UIKeyboardBoundsUserInfoKey = NSRect: {{0, 0}, {320, 216}};
    UIKeyboardCenterBeginUserInfoKey = NSPoint: {160, 372};
    UIKeyboardCenterEndUserInfoKey = NSPoint: {480, 372};
}
2011-10-19 10:44:07.472 SampleApp[352:207] -[SampleViewController viewWillDisappear:]
2011-10-19 10:44:08.374 SampleApp[352:207] -[SampleViewController viewDidDisappear:]
問題なさそうですね。キーボードの種類が切り替わったタイミングにもUIKeyboardWillShowNotificationとUIKeyboardWillHideNotificationがきちんと呼ばれているようです。これならUIKeyboardWillChangeFrameNotificationを使う必要はあんまり無いように思えます。実際、UIKeyboardWillChangeFrameNotificationを使わないでUIKeyboardWillShowNotificationとUIKeyboardWillHideNotificationだけを使ったアプリをリリースしていますが、特に問題なさそうです。

2011年10月10日月曜日

iOS で Private API を使って Bluetooth Keyboard の状態を取ったりしたいメモ

iOS で Private API を使って Bluetooth Keyboard の状態を取ったりしたいメモです。言うまでもありませんが以下の参考資料に使われているコードとかを使ったアプリが App Store の審査に通ることは絶対にありません。真似しないでね!
あと以下のコードを使って実機で試したコードがまだないので、Jailbrake していない iOS 4.3.5 で動くかどうかすらわかりません。すみません><

GSEventを使ってKeyboardの修飾キーの状態を得る
https://gist.github.com/1242475

iPhoneのイヤフォンマイクやBluetoothキーボードでシャッターを切れるようにするiRemoteShutter
http://hitoriblog.com/?p=1747

iSSHのBluetoothキーボード対応を強化するiSSHPatcher
http://hitoriblog.com/?p=1798

BluetoothキーボードからiPhone/iPadのタスクスイッチを可能にするAltTab
http://hitoriblog.com/?p=3958

BluetoothManager
http://stackoverflow.com/questions/1743610/programmatically-turn-on-bluetooth-in-the-iphone-sdk
BluetoothDeviceを使えばデバイスの電池残量とかも見られそう

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月31日日曜日

Objective-C で文字列リテラルに \0 を含めたいときの作戦

Xcode 4.0 から LLVM が標準のコンパイラとなり、各種警告が非常に厳しくなっています。その中でも特に今回は文字列リテラルに \0 が含まれているときの警告について回避策を発見したのでご紹介したいと思います。

Objective-C では文字列リテラルは @"abesi" のように @"" で囲んで表現します。このリテラルは(あくまで推測で確定ではないのですが)コンパイラによってコンパイル時に CFSTR("abesi") に置換され、 CFStringRef 型としてプログラム中に定義されているようです。さて問題はここからで、 Xcode 4.0 が内部的に構文解析のために使っている LLVM がこのリテラル中に \0 、要するにNULL文字が含まれていると以下のような警告を出すようになってしまったのです
CFString literal contains NUL character
普通はNULL文字をリテラル中に含めたいということはまず無いのですが、SMTPの通信部分を書いたりしていると通信プロトコル自体が命令を \0 で区切れみたいな要件を求めてくるため、どうしてもリテラルの中に \0 を含めたいときが出てきます。 まぁ、具体的には以下のライブラリなんですけどね。

http://code.google.com/p/skpsmtpmessage/source/browse/trunk/SMTPSender/Classes/SKPSMTPMessage.m
sendState = kSKPSMTPWaitingAuthSuccess;
NSString *loginString = [NSString stringWithFormat:@"\000%@\000%@", login, pass];
NSString *authString = [NSString stringWithFormat:@"AUTH PLAIN %@\r\n", [[loginString dataUsingEncoding:NSUTF8StringEncoding] encodeBase64ForData]];
NSLog(@"C: %@", authString);
if (CFWriteStreamWriteFully((CFWriteStreamRef)outputStream, (const uint8_t *)[authString UTF8String], [authString lengthOfBytesUsingEncoding:NSUTF8StringEncoding]) < 0)
{
error = [outputStream streamError];
encounteredError = YES;
}
else
{
[self startShortWatchdog];
}
そして私と同じライブラリの同じ箇所でぶつかった人を発見。

http://stackoverflow.com/questions/5814520/how-do-i-create-a-login-string-that-contains-2-embedded-nulls-and-the-login-an

さて回避方法はないものかと探してみたところ、ちょうど良い回避策が発見できました。

http://stackoverflow.com/questions/6211908/nsstring-with-0

NSStringのstringWithFormat:に %C を埋め込んで、そこに 0 を渡せばうまくいくみたいです!
int main() {
NSString *string = [NSString stringWithFormat: @"Hello%CWorld!", 0];
NSData *bytes = [string dataUsingEncoding: NSUTF8StringEncoding];
NSLog(@"string: %@", string);
NSLog(@"bytes: %@", bytes);
return 0;
}
さっきの例だと、
sendState = kSKPSMTPWaitingAuthSuccess;
NSString *loginString = [NSString stringWithFormat:@"%C%@%C%@", 0, login, 0, pass];
NSString *authString = [NSString stringWithFormat:@"AUTH PLAIN %@\r\n", [[loginString dataUsingEncoding:NSUTF8StringEncoding] encodeBase64ForData]];
NSLog(@"C: %@", authString);
if (CFWriteStreamWriteFully((CFWriteStreamRef)outputStream, (const uint8_t *)[authString UTF8String], [authString lengthOfBytesUsingEncoding:NSUTF8StringEncoding]) < 0)
{
error = [outputStream streamError];
encounteredError = YES;
}
else
{
[self startShortWatchdog];
}
このようにすればばっちり上手く回避できました!

2011年7月18日月曜日

iPad の UIWebView で twitter.com を表示したときに出てくるアプリの宣伝広告を出さない方法

iOS アプリ内で Twitter のタイムラインを表示したり、投稿させたり、 Tweet Button を置きたいみたいな要求は結構あると思うのですが、ここで問題になるのが iPad の UIWebViewで twitter.com を表示したときです。以下の画像のように、 Twitter for iPad をオススメする広告が最初に表示されてしまうのです。



一度 Continue on mobile.twitter.com ボタンを押せば次の画面に遷移して二度と表示されなくなるのですが、大抵のお客さんはこの画面を見た瞬間意味不明になってしまうと思うので、表示されないようにしなければなりません。

最初に思いついた方法は以下の



http://mobile.twitter.com/settings/dismiss?d=2

このリンクを NSHTTPConnection なんかで踏ませればいいんじゃないかと思っていたのですが、調査してみた結果もっと簡単に何とかできることがわかりました。このアプリの宣伝広告を消すのに一番簡単な方法は、以下のように Cookie をセットしてやることです。
// mobile.twitter.comにUIWebViewからアクセスしたときに、"Get Twitter for iPad NOW"とかなんとか表示されるのを防ぐため、
// UIWebViewが使うcookieにd=2をセットしておく
// NSHTTPCookieExpiresはセットしなくても大丈夫です(その場合起動ごとにCookieがけされてしまうので、起動時に毎回セットしてください)
NSHTTPCookie *twitterForIPadAdBlockCookie = [NSHTTPCookie cookieWithProperties:[NSDictionary dictionaryWithObjectsAndKeys:
                                                                                @"d", NSHTTPCookieName,
                                                                                @"2", NSHTTPCookieValue,
                                                                                @".twitter.com", NSHTTPCookieDomain,
                                                                                @"/", NSHTTPCookiePath,
                                                                                [NSDate distantFuture], NSHTTPCookieExpires,
                                                                                nil]];
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:twitterForIPadAdBlockCookie];
これだけでご覧の通り!嘘のように広告が出なくなります。




■おまけ: javascript:スキームを使ってみよう

今回のように UIWebView をアプリ内で使って特定のWebアプリを表示する場合にWebアプリの挙動が知りたくなったときは、iPadのSafariブラウザから javascript: スキームを使って JavaScript のコードを流し込んでデバッグするとなかなかはかどると言うことがわかりました。たとえばクッキーを表示させてやるだけなら、以下のように超簡単にできます:
javascript:alert(document.cookie);


クッキーを流し込むのも、流し込むだけならこれまた簡単:
javascript:document.cookie="foo=bar";
より詳細な調査はPC側のブラウザでゆっくりやって、最後にちょっとだけ試したいところを実機でやってみることができるのがいい感じです。多分この用途のためのブラウザアプリなんかも App Store をあされば出てくると思うので、そういうのを使えばさらにはかどると思います。

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

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

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

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

2011年6月21日火曜日

Netlogがハックしたgmailアカウントの情報を用いてspamを送っている?

やや旧聞になるのですが、お恥ずかしながらgmailのアカウントが何らかの手段によってハックされてしまったようで、私のメールアドレスから大量のspamが連絡先に飛んでしまう事件がありました。関係者の皆様には大変ご迷惑をおかけいたしまして誠にすみません><

パスワードは標準以上に強固だったはず(ハックされたアカウントが使っていたパスワードは10桁で大文字小文字数字記号全てランダムに混ざったもの)なのですが、マルウェアも見つからないし心当たりのあるフィッシング被害もないしPSNには登録してないし・・・ということでまったくどこからやられたのかわかりません。恐ろしいです。パスワードだけではもはや不十分ということかも知れません。

さてここからが本題。
Netlog (http://ja.netlog.com/) というSNSがあるのですが、今週の月曜日に知人から続々Netlogへの勧誘メールが私から飛んできたという報告が。

手口はどうも私のgmailの連絡先に入っていたメールアドレスに対して、私がin-reply-toになるようなspamを次々送りつけるというもののようです。他の経路からspamされているのではと思って調べてみたのですが、一番怪しいFacebookについてもセキュリティが破られた形跡はないし、そもそも一度もNetlogなるサービスを聞いたこともないしページを開いたことも友達に勧められたこともありません。さらに私のgmailの連絡先にしか入っていないメールアドレスにもspamが飛んでいるようで、どうもこの盗まれたアカウント情報を元にspamを送っているような気がしてなりません。

あくまで状況証拠なのですが、ともかく皆さんもgmailのセキュリティには万全を期してください><

2011年5月29日日曜日

iPhone iPad 向けスタイラスで書き味勝負をしてみた 2回戦


写真は左から順に、 Pogo SketchoStylus 初期限定生産型パワーサポート スマートペン PBJ-9XシリーズBamboo Stylus

iPhone/iPad向けのスタイラスがいくつも発売されていますが、実際にスタイラスを使ったらどんな具合なの?指でいいんじゃないの?と疑問をお持ちの方と、 私のようなスタイラスマニアの方 のために、iPhone/iPad向けのスタイラスの書き味を実際に書いてみて試してみるという企画です。

前回の記事はこちら: http://akisute.com/2011/02/iphone-ipad.html
おすすめスタイラスまとめ記事はこちら: http://akisute.com/2010/06/ipad.html
Bamboo Stylusのレビューはこちら: http://akisute.com/2011/05/ipad-bamboo-stylus.html
oStylusのレビューはこちら: http://akisute.com/2010/10/ipad-ostylus.html


■比較方法

iPad上で実際にスタイラスでいろいろ書いてみて、書き味を見て比較します。
  • 使用するiPadは iPad 2 Black 16GB (WiFi+3G GSM)
    • 表面に保護フィルムは貼っていません
    • 保護フィルムがある場合、保護フィルムのためにペン先の滑りおよび感度が変化するため、結果が異なってくる可能性があります。
    • 一般的に、保護フィルムがある方が感度が悪くなります。滑りはフィルムの材質により変化します。
  • 使用するアプリは Noteshelf
    • 無地ノート
    • 黒インク
    • ペン先10pt
    • 拡大は使用しない
  • すべて一発勝負
    • undoなし
    • 書き損じたり、ペン先の感度が落ちて線が飛んでもやり直さない。それは感度が実際に悪いという証拠になるため。

■結果

書いてみたノートをpdfにして出力してみました。以下のURLからご覧になれます。
https://docs.google.com/viewer?a=v&pid=sites&srcid=ZGVmYXVsdGRvbWFpbnxha2lzdXRlc2FtYXxneDo0OTI3MWU0MjRlMzI2ZjNm

複数同じ文言が並んでいるページは、全て上から順に、
pogo sketch, oStylus, スマートペン, Bamboo Stylus
の順に並んでいます。


■考察

以下、いくつかのページをピックアップして考察。


pogo sketchは第一世代のスポンジ式ペン先であるため、やはりペン先感度が悪く線が飛びまくります。線自体は自然に書けているのですが。


oStylusは感度はよいものの、いかんせん書きづらい。曲線が特に不自然です。しかも書くのに時間がかかります。多分一番時間がかかりました。


スマートペンはさすがの高性能。最高の感度と軽い軸の恩恵で、一番楽に書けます。ただし線自体は細かいところが弱いです。



今回新顔のBamboo Stylus。ペンが重く、スマートペンより筆圧が必要なため疲れますが、スピードのある細かい線が魅力。一番速く書けたのもこのペンです。


並べてみるとこんな感じです。上からpogo sketch, oStylus, スマートペン, Bamboo Stylus。良いと思ったところには緑○を、悪いと思ったところには赤×を引いています。
pogoは線が飛びさえしなければ・・・
oStylusはとにかく線をゆっくり引いてしまう。 Noteshelf アプリは Penultimate 同様、線を高速に引けば美しく細くなるように出来ているのですが、見てのとおり全ての線が太くてゆっくり引いてしまっている。Zとかへろへろですよ、もう。
スマートペンとBamboo Stylusは安定して良いですね。高速に美しく書けています。


■まとめ

やはり第二世代のシリコンペン先を装備したスタイラスが圧倒的に良いです。その中でも自分の好みや用途で選べる次代になってきたのがすごくいいですね。次回はプリンストンテクノロジーの専用タッチペンやAcase Stylusなんかも加えてレビューしてみたいです。

iPad 用スタイラス Bamboo Stylus を試してみた



つい先日いよいよ発売された Bamboo Stylus, 早速買って試してみました。


■Bamboo Stylus とは?

http://wacom.jp/jp/company/news_detail.php?id=355
http://japanese.engadget.com/2011/04/19/bamboo-stylus/

B004XF0FQWWacom iPad/IPad2/iPhone4対応 描画、ポインティングに最適なタッチペン Bamboo Stylus CS-100/K0
ワコム 2011-05-27

by G-Tools

PC向けペンタブレットではおそらく業界最大手の Wacom が、満を持して投入してきた静電誘導式タッチディスプレイ用のスタイラスペンです。ペンタブレットの開発でおそらくこの手のスタイラスペンに関しては相当なノウハウがあるのではないかと思われ、非常に期待が持てます。プレスリリースで、特に以下のような点が既存のスタイラスとは異なると強調されています。
  • 手に持った時の心地よさと書きやすさを両立する重さと重心のバランス。
  • 上質なサテン調のブラックとシルバー色を採用し、高い質感を実現。
  • ペン先は主な他社製品よりも直径が約25%小さく、より自然で直感的な描画が可能。
  • スムーズで快適な書き心地を実現するペン先の実現。(磨耗した際には交換が可能)
  • iPadカバーに留めたり、ポケットに留めたりするなど、携帯や保管にも便利なクリップ。(脱着可能)

■最初に結論

良い点
  • 第二世代のシリコンペン先を採用している他のスタイラスペンと比べ、細くて滑りが良くしっかりとしたペン先。
  • サイズの割に軸がやや太めで、しっかりと握ることができる。
  • 上記の理由により、実物のペンと非常に近い筆書感覚が得られる。
  • クリップが丈夫で脱着可能。取り外してストラップを代わりにつけることもできる。
悪い点
  • 同サイズのスタイラスペンの中では圧倒的に重く、書く際に手が疲れる。
  • 第二世代のシリコンペン先を採用している他のスタイラスペンと比べ、感度がやや悪い。
  • 3000円という、やや高価な値段設定。

■外見、持った感触

まずは外見から。値段設定がやや高めで、かつBambooブランドの名前を冠していることからもわかるとおり、iPad向けスタイラスとしてはハイエンドを狙っている物と思われます。そのためデザインにも力が入っており、表面がiPadやMacのように梨地加工されています。単なる金属の棒という体の パワーサポート スマートペンプリンストンテクノロジー 専用タッチペン に比べ断然高級感があり美しく感じられます。

長さは12cmということで、他社のスタイラスと比べても数ミリ長い程度で標準的だと思います。軸は他社の製品に比べてやや太く、良くコンビニなどで売っているボールペンよりわずかに細い程度になっています。そのため手に持った際の感覚が非常にペンらしいです。また、重さが非常に特徴的で、他のスタイラスと比べて明らかに密度のある重さがあります。体感だと パワーサポート スマートペン より二倍近く重いように感じます。

上部とペン先はねじになっていて、それぞれクリップとペン先が取り外せるようになっており、メンテナンスのことを考えた作りになっているのが心憎いです。消耗品ではなく、長期間使わせる物という印象を受けます。また、取り外した上部のクリップの部分にストラップをつけて持ち歩くことも出来そうです。


長さ比較。左から右に短い物順。pogo sketch, スマートペン, Bamboo Stylus, oStylus


太さ比較。先ほどと同じ順序。pogo sketch, スマートペン, Bamboo Stylus, oStylus


■動作、書き味

買って最初に一筆書いてみた時にすぐ気づいたのが、まず感度が悪いです。ほとんど載せるだけで反応する超感度の パワーサポート スマートペン に対して、ほんの少しだけ力を込めないと反応しません。しかしながらペン先が細くてしっかりしており、iPadの表面を非常になめらかに滑ります。滑りがよいので実際に紙で書いているよりちょっと違う感じがしますが、ペンタブレットに慣れている人にはちょうどいいぐらいだと思います。先端がふにゃふにゃしてやや抵抗がある滑りの パワーサポート スマートペン とは対照的な印象です。

個人的には長時間書いていると疲れます。感度が悪く抵抗が弱いのと重いペンが相まって、腕の力を使うためだと思います。


実際に Noteshelf アプリ上で書いてみました。実際のペン同様、非常になめらかに書けています。


■まとめ

WacomとしてはiPad向けスタイラスのハイエンドモデルとして位置づけているこの商品ですが、実際に使ってみるとむしろ パワーサポート スマートペン と対になるような感じの印象を受けました。すなわち、以下のように棲み分けられます。

Bamboo Stylus をオススメする人
  • ペンに近い持ち味、書き味が欲しい
  • 細くてよく滑るペン先が何より欲しい!
  • 値段は高くても気にしない
パワーサポート スマートペン をオススメする人
  • 軽くて、書いていて疲れないペンが欲しい
  • 感度の良くて、実際のペンみたいに少し抵抗がある滑りのペン先が何より欲しい!
  • 値段は安い方がいい、または初めてスタイラスを試してみるので失敗しても後悔しない値段の方がいい
私自身はどっちを選ぼうか悩んでますが、せっかく買ったし、しばらくはBamboo Stylusを使ってみようかと考えてます。