iOS の UDID 廃止問題は誰にとっての問題なのか

ごぶさたしております。もはや境界どころかデザインともプログラムとも距離が出てきている今日この頃、すっかりこのブログの存在を忘れていました。

すでにネタの旬は過ぎていますが、Apple の UDID 廃止問題について触れておこうかと。事実関係や最もらしい憶測は、以下の記事を参照。

アップル、iOS端末ユーザーのプライバシー保護で方針変更 - 「iOS 5」ではUDIDの利用を推奨せず

あくまで UIDevice クラスの uniqueIdentifier メソッドが deprecated でマークされただけであって、iOS 5.0 で今すぐ廃止されるという話ではない点に注意。各所予想を取りまとめると、iOS 5.0 では現状どおり使用できる(コンパイル時に Warning が出る)が、iOS 5.1 で廃止されるのではないかという話。ちなみに iOS 4.0 から 4.1 のアップデートまでの期間は3ヶ月ほどだったことを鑑みると、本件 UDID の廃止も年内目処でしょうか。

今日明日の話ではないですが、可及的速やかに対策が必要な話ではあります。

影響範囲

個体識別番号というのは、ガラケー時代からもセキュリティ問題を指摘されてきたポイントであって、今回の Apple による UDID 廃止アナウンスを歓迎する向きもあるようです。しかし大半にとってはネガティブ・インパクトであって、特にアプリ・デベロッパーにとっては痛い仕打ちでした。影響範囲は以下のブログが網羅的です。

なぜiOSでUDIDが必要とされていたのか、メモ

利用ポイントを抜粋させていただくと、

  • アプリケーションのサーバとのセッション保持
  • 行動トラッキングによるターゲティング広告
  • リワード広告

目的は異なるにせよ、デベロッパーにとって容易にユーザを特定するための手段としての UDID というのは、便利な代物です。UDID の万能なところは、ターゲティング広告やリワード広告で使われているように、アプリを横断しても同一ユーザを特定できるところです。その良し悪しは別として、iOS 関連のエコシステムの拡張において、UDIDの果たした役割は小さくなかったと思います。

対応策

各所それぞれと対応策をディスカッションした結果、MACアドレスを使うのだとか、Cookie を絡めてごにょごにょするのだとか、みんな考えていることは同じなのだなーという印象。

自分も ioctl をごにょごにょして MAC アドレスを取得し、UDIDの代わりに使うということを試してみました。そして、これが上手く機能しちゃうわけです。今まで無頓着に UDID を使ってきたのなら、同様に MAC アドレスを使って終わりというスタンスも、正直ありなんじゃないかと思います。

ただ、iOS ビジネス圏で頑張っておられます法人各位においては、『UDIDがダメならMACアドレスだよね』というのは禁断のアプローチであって、話の背景を考えるに、妙なリスクを抱えないように細心の注意を払って、いま一度対応策は検討すべきです。結局 UDID の代わりに UUID を利用するという、Apple が提示している手法に落ち着くでしょう。

じゃあ、アプリ横断のリワード広告とかシングルサインオンとかどうするのよ、という話になります。Apple からしたら『リワードすんじゃねー』とか『GameCenter使えよー』という話なんだと思うので、間違いなくケアはされません。Cookieを組み合わせて頑張るか、scheme のパラメータを使って頑張るとか、諦めるとか、自分で汗かかないといけないんでしょうね。

UDID廃止でワリを食うのは誰か

確実に UDID を使ってきたデベロッパーはワリを食いますが、もう一歩踏み込んで考えてみましょう。

アプリ界隈も最近はプラットフォーム乱立状態ですが、本件は、UDIDの代わりに使えるユーザIDを持っているプラットフォーマーにとっては追い風になりえます。こういう記事もあります。OpenFeint offers its service to replace UDID

広告会社はどうか。そもそも今年4月時点でリワード広告は Apple エコシステムから追い出しを食らっており、すでに業界大手の TapJoy などもビジネスを変えてきています。AdMob などのネットワーク広告への影響も限定的と思われます。一部の属性ターゲティングに対応されているだけで、まだ UDID を効果的に利用したオーディエンス・ターゲティングを武器にできているネットワークはいません。なんだかんだ変わらず。

アナリティクスはどうか。最近、国内でもチラホラ出てきていますが、インストールを計測して分析するような効果測定ツールを提供している企業です。AdMob も提供しているので、広告会社とも重なる領域です。もともと UDID でトラッキングができたのは、アプリからアプリへ遷移するものだけで、すでに業界の興味はブラウザ(Safari)からアプリへの遷移のトラッキングに移っています。そして、ブラウザからアプリへの計測を行う技術が確立されれば、UDIDは不要になります。なので、中長期の影響はありません。

重要なポイントなので繰り返しますが、興味をブラウザに移すとUDIDは不要になります。折しも Facebook のプロジェクト・スパルタンのニュースが増えつつある昨今、UDID問題などから完全フリーなブラウザへの大波が起こる可能性はありえます。

そう考えていくと、実はワリを食うのは Apple なんではないかと。

UDIDが使えないくらいで、アプリを捨ててHTML5にシフトするということは無いとは思いますが、後から振り返って、UDID廃止がターニングポイントだったという話はなくもないと思います。というか、今ユーザはアプリとブラウザを区別せずに使っているという現実は軽視できないです。気付いたら、Apple を中心としていた周辺エコシステムが、丸ごとブラウザ方面へ横シフトしてたりするんでしょうか。怖いです。

結論

UDIDの代わりにMACアドレスを使う前に、各法人担当者のみなさまは韓国の訴訟とか見てから決断をしていただきたく。あと、Android は関係ないから良いやーと言って、IMEI などの端末識別コードを使ってしまうのも問題ありですので、ご注意を。

ちなみに、iOS で言えば脱獄しちゃえばMACアドレスも書き換えられるし、Androidで言えば root でデバイスIDあたりは変えられてしまうわけでして、担当者各位におきましては、不正を見越した仕組みづくりをどうかよろしくお願いします。

iPhoneアプリ開発時にハマる色んなエラー

iPhoneアプリを開発して AppStore に上げるたびに、その過程で様々なエラーが出てハマるので解決策をメモしておきます。

Couldn't register xxxxx.debug with the bootstrap server. Error: unknown error code. This generally means that another instance of this process was already running or is hung in the debugger.Program received signal: "SIGABRT".

実機でテストしようと思ったら、main.m でこんなエラーを吐いてコケる。原因不明ですが、電源 OFF-ON で実機を再起動したらエラーが出なくなりました。なんなんでしょうか。

Code Sign error: The default keychain doesn't have an identity matching the profile 'xxxxx profile' and identity 'iPhone Developer'

AppStore 申請時にアーカイブのバリデーションを行ったら、このエラーが発生。きちんと証明書もインストールしているし、配布用のプロビジョニングファイルもオーガナイザにインストールしています。

これは読んで字のごとく、ビルド設定のコード書名IDの設定が誤っていました。Debug は iPhone Developer を、Release は iPhone Distribution を指定しないとダメですね。アーカイブ作成時点ではビルドが成功するので、分かりにくいです。

com.apple.transporter.util.StreamUtil.readBytes(Ljava/io/InputStream;)[B

アーカイブしたファイルを、オーガナイザから AppStore に転送しようと思ったら、こんなエラーが出ました。意味が分かりません。どうも Xcode4 のバグが原因のようです。Xcode3 にダウングレードして、そのオーガナイザから転送したら上手くいきました。

ちなみに、Xcode3 と Xcode4 は共存可能です。私は、/Xcode3 というディレクトリを作ってそこにインストールしました。Xcode4 が枯れてきたらそちらに移るつもりなので、一時的にこのような構成にしました。

Objective-C コードのビルド時にエラーが起こるものは、プログラムのバグ、もしくはビルド設定の誤りが原因なので、解決のアプローチが簡単です。Google 先生に聞いても、多くのケースで答えが返ってきます。それ以外の、上記のようなエラーは、なかなか答えが見当たらないのが厄介です。

今後、他にもハマったら更新します。

UIWebView で history.back が使えないケース

このたびの東北太平洋沖地震で被災されているみなさん、ご家族、ご友人のみなさんに、心からお見舞い申し上げます。

非常に月並みな言葉ではありますが、私も、被災地で生まれ育った関係者のひとりとして、今はもう感情に訴えるべきでないという思いがあるので、このくらいにさせてください。ただ、ひとりのエンジニアとして、ひとりのビジネスマンとして、ひとりの日本人として、被災地の社会・経済の復興に役立つような、実利ある行動を取っていきたいと思っています。

さて。

『実利ある行動』とか銘打ったあとにやりづらいですが、小さい話をします。iPhone の UIWebView で表示したサイトで、 javascript:history.back() が動かないケースがあってハマりました。

結論から言うと、UIWebView クラスに loadData:MIMEType:textEncodingName:baseURL: や loadHTMLString:baseURL: でロードした HTML ページは、javascript の履歴スタックに載らないようです。

これらのメソッドでロードしたページから次のページに遷移したあと、そこで history.back() を動かしても、うんともすんとも言いません。UIWebView には、canGoBack というブラウザの戻る機能が使えるかどうかを判別するプロパティや、goBack というブラウザの戻る機能そのものとなるメソッドもあるのですが、これらも効きませんでした。とにかく、UIWebView が直接 HTTP 経由で受信したデータ以外は、履歴をたどれません。

細かい通信制御がしたかったため、NSURLConnection でデータを取得して、それを UIWebView の loadData:MIMEType:textEncodingName:baseURL: に食わせるようにしていたのですが、この設計だと history.back() が使えないわけです。分かるか、そんなもん。

ということで、UIWebView で history.back() を使いたい場合は、loadRequest: メソッドで HTML を読み出しましょう。細かい制御は、UIWebViewDelegate でごにょりましょう。

1時間以上ハマりました。相変わらず Cocoa に慣れない。

View+Navigation based iOS アプリの作り方

Xcodeで iOS アプリの新規プロジェクトを作成する際、Navigation-based ApplicationやView-based Applicationといったテンプレートを選択することができますが、その両方の機能性を持たせたいということがあると思います。というか、さっきそういう境遇に陥って困ったので、メモです。

今回は、アプリのトップ画面は View-based Application だけれども、そこからメニューを選んだときには NavigationController のスタックに積んで、Navigation Bar 付きのページに遷移していくというもの。うむ、説明が難しい。平たく言うと、トップ画面は Navigation Bar がなくて、2階層目の画面は Navigation Bar があってトップに戻れる感じです。

さっそく作ります。ここでは、Sampleというアプリを考えます。

まず、View-based Application テンプレートを使ってプロジェクトを作成します。これに NavigationController の機能を追加していきます。

はじめに SampleAppDelegate.h を開いて、UINavigationController を追加します。1行追加するだけ。

@interface SampleAppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow *window;
    UINavigationController *navigationController;
    SampleViewController *viewController;
}

次に SampleAppDelegate.m を開いて、application:didFinishLaunchingWithOptions: と dealloc を以下のように修正します。NavigationController の rootViewController に、デフォルトの UIViewController をセットして、その view を window のサブビューに追加して表示させるようにしているのがポイントでしょうか。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    
    
	navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];
    
    // Add the view controller's view to the window and display.
    [window addSubview:navigationController.view];
    [window makeKeyAndVisible];
    
return YES; }
- (void)dealloc { [viewController release]; [navigationController release]; [window release]; [super dealloc]; }

続いて、トップ画面では NavigationBar を隠し、2階層目以降は表示するようにします。ここでは next: が、2階層目を開く処理として、ボタンか何かに紐付けた IBAction 的なメソッドということにします。

SampleViewController.m を開いて、以下のような感じにします。

#import "SampleViewController.h"
#import "NextViewController.h"
    
@implementation SampleViewController
    
- (void)viewWillAppear:(BOOL)animated {
	self.navigationController.navigationBarHidden = YES;
}
    
- (void)hoge:(id)sender {
	NextViewController *next = [[NextViewController alloc] init];
	[self.navigationController pushViewController:next animated:YES];
 	[next release];
	self.navigationController.navigationBarHidden = NO;
}

// 以下略

ここでのポイントは、 navigationBarHidden に BOOL オブジェクトをセットするタイミングです。pushViewController:animated: する前に NavigationBar を表示しようとすると、画面遷移前ににょきっと NavigationBar が現れてくるので、あんまり心地良くありません。いろいろ試してみていただければと思いますが、コードを書く位置で、目に見える違いがあるのが GUI 開発の面白いところですね。

これで目的達成です。おつかれさまでした。大したことないですが、そのうち github にでも上げときます。

UIModalTransitionStyleCoverVertical のようなアニメーション

UIViewController でモーダルビューを表示するメソッド presentModalViewController:animated: の画面遷移アニメーションのスタイルは、デフォルトだと画面下からビューがせせり出てくる UIModalTransitionStyleCoverVertical になっています。結構気持ち良いので、これと同じアニメーションを、自分の UIView でも実装してみるテスト。というか、大人の事情により途中まで書いていてボツになったので、もったいなので、ここで書き上げてみます。

UIViewController内で UIView をモーダルで表示させたいメソッド内で、次のようなアニメーションを組めばOKのはず。

UIView *myView = [[UIView alloc] init];
/*
  View 生成する処理むにゃむにゃ
 */

// 初期位置を決める CGPoint p; if (self.interfaceOrientation == UIInterfaceOrientationPortrait || self.interfaceOrientation == UIInterfaceOrientationPortraitUpsideDown) { p = CGPointMake(0.0, CGRectGetHeight([[UIScreen mainScreen] applicationFrame])); } else { p = CGPointMake(0.0, CGRectGetWidth([[UIScreen mainScreen] applicationFrame])); } CGRect r = myView.frame; r.origin = p; [myView setFrame:r];
// Animation CGContextRef context = UIGraphicsGetCurrentContext(); [UIView beginAnimations:nil context:context]; [UIView setAnimationDuration:0.5]; [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut]; CGRect newRect = myView.frame; newRect.origin = CGPointMake(0, 0); [myView setFrame:newRect]; [UIView commitAnimations];
[self.view addSubview:myView]; [myView release];

ちなみに、UIModalTransitionStyleCoverVertical の animationCurve は UIViewAnimationCurveLinear かもしれません。個人的には EaseInOut が一番気持ちいいのでそれにしているだけです。

iPhone でファイルの追記と上書き

Perl でいうところの ">" や ">>" っぽくファイルの入出力をする方法はないかといじくり回した結果、正攻法か分かりませんが、とりあえず目的は達成できたのでメモ。

既存ファイルへの追記

NSString *path = @"hoge.txt";
NSFileHandle *fh = [NSFileHandle fileHandleForWritingAtPath:path]; [fh seekToEndOfFile]; for (int i = 0; i < 10; i++) { NSString *str = [NSString stringWithFormat:@"test%d\n", i]; NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding]; [fh writeData:data]; } [fh closeFile];

上書き

NSString *path = @"hoge.txt";
NSFileManager *manager = [NSFileManager defaultManager]; if ([manager fileExistsAtPath:path]){ NSError *error = nil; [manager removeItemAtPath:path error:&error]; } [manager createFileAtPath:path contents:[NSData data] attributes:nil];
NSFileHandle *fh = [NSFileHandle fileHandleForWritingAtPath:path]; for (int i = 0; i < 10; i++) { NSString *str = [NSString stringWithFormat:@"test%d\n", i]; NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding]; [fh writeData:data]; } [fh closeFile];

別のより良い方法を見つけたら追記します。

HTTPステータスを判定して UIWebView を生成する

UIWebView はかんたんに HTTP 通信して Web ページを表示することができて便利なのだけれども、HTTP ステータスコードを見ていないため、iPhone 上に "500 Internal Server Error" とか出てくるわけです。これはカッコ悪い。

例えば、次のように UIWebView の loadRequest: を使った場合、通信エラー処理は webView:didFailLoadWithError: に委譲されるものの、HTTPサーバのエラー処理は委譲されず、処理するタイミングがありません。

- (void)viewDidLoad {
    UIWebView *wv = [[UIWebView alloc] initWithFrame: view.frame];
    wv.delegate = self;
    wv.scalesPageToFit = NO;
    [self.view addSubview:wv];
    
    [wv loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.deftrash.com/"]]];
    [wv release];
    
    [super viewDidLoad];
}

#pragma mark UIWebViewDelegate - (void)webView:(UIWebView *)wv didFailLoadWithError:(NSError *)error { NSLog(@"UIWebView::didFailLoadWithError"); }

仕方ないので、 NSURLConnection の delegate で制御します。 _conn と _data はそれぞれ、NSURLConnection と NSMutableData のインスタンス変数です。

- (void)viewDidLoad {
    NSURLRequest *req = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.deftrash.com/"]];
    _conn = [[NSURLConnection alloc] initWithRequest:req delegate:self startImmediately:YES];
    if (_conn) {
        _data = [[NSMutableData data] retain];
    }
}

-(void) connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { NSInteger status = [(NSHTTPURLResponse *)response statusCode]; if (status != 200) { [_conn cancel]; // 本当はちゃんと NSError オブジェクトを作って渡すべき [self connection:connection didFailWithError:nil]; } else { [_data setLength:0]; } } -(void) connection:(NSURLConnection *) connection didReceiveData:(NSData *) data { [_data appendData:data]; } -(void) connection:(NSURLConnection *) connection didFailWithError:(NSError *) error { [_conn release]; _conn = nil; [_data release]; } -(void) connectionDidFinishLoading:(NSURLConnection *) connection { UIWebView *wv = [[UIWebView alloc] initWithFrame: self.view]; wv.scalesPageToFit = NO; [wv loadData:_data MIMEType:@"text/html" textEncodingName:nil baseURL:nil]; [_conn release]; _conn = nil; [_data release]; }

NSURLConnection の initWithRequest:delegate: を使うと TCP 通信のプロセスを委譲できるので、これを利用しています。なぜか delegate のプロトコルはありません。connection:didReceiveResponse: で最初のレスポンスを受けた段階で、ステータス判定して、正常(200) でなければ通信をキャンセルしています。 本来は switch 構文で、もっと細かくステータスごとの処理をするべきでしょう。せめて、400 や 500 番台だったらエラーというぐらいにはすべきでしょうね。

このあたりのコントロールは、実際にやってみると非常に面倒です。こうした非同期通信を複数スレッド走らせる場合は、きちんとキューイングしたり、インスタンスの使い回しをしないようにしたり、気にかけるポイントが多かったです。いやあ、途中でメモリの開放漏れなどで、10回くらいクラッシュしました。

こうして考えると ASIHTTPRequest は超絶便利です。ちょっとした HTTP 通信をするにはライブラリとして大きくなりすぎているのが気になっているものの、BSD ライセンスですし、選択肢として取れるのであれば積極的に使いたいものですね。

おつかれさまでした。

ローカルの html を UIWebView で表示する

iPhone アプリ上で使用許諾文書のような長文を表示させたいとき、View にベタ書きという選択はありませんが、かと言ってわざわざ Web と通信するまでもありません。ということで、プロジェクトに配置しておいた静的 html を読んで表示してあげるテスト。

Xcode を開いて Resources フォルダに、 html ファイルを置いておきます。今回は policy.html というのを置いてみました。あとは、それを表示する ViewController で以下のようにするだけでできました。

- (void)viewDidLoad {
    CGRect frame = [[UIScreen mainScreen] applicationFrame];
    CGRect rect = CGRectMake(0,0,frame.size.width, frame.size.height);
    UIWebView *wv = [[UIWebView alloc] initWithFrame:rect];
    wv.scalesPageToFit = YES;
    [self.view addSubview:wv];
	
    NSString *path = [[NSBundle mainBundle] pathForResource:@"policy" ofType:@"html"];
    [wv loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:path]]];
    [wv release];
    
    [super viewDidLoad];
}

よくよく考えてみれば、これで html5 + javascript を読み込んでしまえば、すぐに簡易 iPhone アプリが作れるんですね。その割には、Xcode の新規プロジェクトのなかには "HTML-based Application" という選択肢がないのが、なんとも考えさせられます。