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

2009年7月12日日曜日

EditControlとAccessoryViewの背景は透明



  • EditControlの背景はデフォルトで透明
  • AccessoryViewの背景もデフォルトで透明
  • UILabelの背景はデフォルトで白塗りつぶし(透明にはならない)

OS 2.2.1での実験結果なので、3.0では違うかも(たぶん同じ)

それだけです。お粗末様でした・・・

2009年7月7日火曜日

YourTurnをApp Storeにサブミットしてみた



2ヶ月ぐらい前からちょこちょこ作っていた物をレビューに出してみました。まぁ、どうなるかは分かりません。現在一週間経過、特に何も変化無し。今回の目的であるApp Store童貞を捨てることは見事達成できそうです><


公開されるのを気長に待ちながら、現在1.1アップデートを作成中。セルがかっこよくなりました。

2009年6月8日月曜日

時間を入力するために、カスタムUIPickerViewを作ってみた



時間を入力するためのUIが欲しかったので、こんな感じのカスタムUIPickerViewを作ってみました。ソースコードはこちら。
http://github.com/akisute/YourTurn/blob/8119bf028acf4908edb602d277544bc2cf6a5848/Classes/YTTimePickerView.h
http://github.com/akisute/YourTurn/blob/8119bf028acf4908edb602d277544bc2cf6a5848/Classes/YTTimePickerView.m


使い方はソースを見ていただければきっとご理解いただけると信じておりますので、UIPickerViewを使っていて気づいた点などをいくつか晒してみたいと思います。


■参考にした記事
id:paellaさんのダイアリーを参考にさせていただきました。ありがとうございます。
http://iphone-dev.g.hatena.ne.jp/paella/20090521#20090521fn1

基本的にはこちらの記事にまとめられている内容に従って、
// 2) ビューを返したい場合
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view {}
このデリゲートメソッドを使いカスタムViewを利用した実装を行ったのですが、一部私の実装とpaellaさんの記事で異なっている点があるので、ご紹介したいと思います。


■UILabelを直接デリゲートメソッドから返す
元記事では、
UIViewを返すデリゲート、必ずUIViewを返さないとうまく動いてくれません。このサイトやこのサイトで紹介されているようなUILabelを直接渡してあげる方法は、少なくとも私の環境(2.2.1)では何も表示されませんでした。
と、UIViewを返さなくてはダメとご指摘がありますが、どうやらUILabelでも大丈夫みたいです。ビルドターゲット = iPhone OS 2.2.1の環境で確認できました。

UILabelをそのままデリゲートから返却したときに画面に表示されなかった原因は、どうやらUILabelのframe.size.heightが0になってしまっていた(要するにつぶれてしまっていた)のが原因みたいです。そこで、以下のようなコーディングを行いました。
- (UIView *)pickerView:(UIPickerView *)picker viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view
{
UILabel *label = (UILabel *)view;
if (!label)
{
label = [[[UILabel alloc] init] autorelease];
label.frame = CGRectMake(label.frame.origin.x, label.frame.origin.y, 20.0, 23.0);
label.backgroundColor = [UIColor clearColor];
// 以下初期化コードなど・・・
}

return label;
}
このように、label.frameの高さを無理矢理指定してやれば、きちんと画面に出せるみたいです。また、この方法を使えば、元記事で
ポイントはビューの座標。どうも各要素での{0,0}の位置はPicker内各Rowのど真ん中にあるらしく、サンプルのように負数を指定してあげないとどこかに寄った状態になってしまうみたいです。
とご指摘があったビューの座標も、特に気にせず普通に指定することが出来るようになりました。


■Pickerを、UIScrollViewのサブクラス(UITableViewなど)にaddSubviewする場合の注意点
この記事の見出しに貼り付けた画像では、作成したカスタムPickerをGroupedスタイルなUITableViewのfooterViewに貼り付けています。
    // Initialize time picker with a previously selected value
timePicker = [[YTTimePickerView alloc] initWithFrame:CGRectZero];
NSInteger initialValue = [[NSUserDefaults standardUserDefaults] integerForKey:_USERDEFAULTS_TIMEPICKER_INITIALVALUE];
timePicker.time = (initialValue == 0) ? 300 : initialValue;
timePicker.timePickerViewDelegate = self;
[timePicker selectRowWithCurrentTime];
self.tableView.tableFooterView = timePicker;
このように、UIScrollViewのサブクラスのサブビューとしてUIPickerViewを貼り付けると、そのままの状態ではうまくPickerのドラムが回転してくれないという症状が発生してしまいます。
これはどうやらUIPickerViewのスクロールとUIScrollViewのスクロールがけんかしてしまっているみたいです。そこで、スクロールする必要のないTableView側の設定をOFFにします。具体的には、Interface Builderを利用する場合、以下の赤枠でくくった箇所を設定すると良いようです。



こうすることで、綺麗にドラムが回転してくれるようになります。

2009年5月19日火曜日

UINavigationの片方のサイドに複数個のボタンを持たせたい



こんな感じでUINavigationBarの片方のサイドに複数個のボタンをおく方法を探してみました。UIToolBarと異なり、UINavigationBarではUINavigationItem.rightBarButtonItem, UINavigationItem.leftBarButtonItem, それからタイトル部分と、最大でも3個しかアイテムを配置することが出来ません。そのため、複数のボタンを一つの配置箇所にまとめて配置したい場合には、カスタムビューを作成する必要があります。
Appleの配布しているデモアプリケーションにもありますが、こういう場合にはUISegmentedControlのmomentaryプロパティをYESに指定して、ボタンみたいに利用するのがいちばん良いようです。
NSArray *items = [NSArray arrayWithObjects:@"Add", @"YourTurn", nil];
UISegmentedControl *segmentedControl = [[[UISegmentedControl alloc] initWithItems:items] autorelease];
segmentedControl.selectedSegmentIndex = UISegmentedControlNoSegment;
segmentedControl.segmentedControlStyle = UISegmentedControlStyleBar;
segmentedControl.momentary = YES;
[segmentedControl addTarget:self action:@selector(segmentedControlClicked:) forControlEvents:UIControlEventValueChanged];
self.navigationItem.rightBarButtonItem = [[[UIBarButtonItem alloc] initWithCustomView:segmentedControl] autorelease];

この方法でひとまずはUINavigationItemの右側に複数のボタンを作ることが出来ました。しかしまだまだ完璧ではなくて、現地点で分かっている限りでも二つの問題があります。
  1. ボタンの背景色が通常のボタンとは異なる色になる
  2. 個々のボタンひとつひとつを個別にDisableすることができない

Appigo Todoなんかは普通にUINavigationBarの右側に3つもまとめてボタンを綺麗に配置してあったりするのですが、一体全体どうやってるんでしょうね?・・・うーん。

2009年3月19日木曜日

Your mobile device has encountered an unexpected error (0xE800003A)

いつになってもこのエラーの対処法が覚えられなくてGoogle先生にお尋ねすることになっているので、ここに備忘録を記しておきます。

1.info.plistのBundle identifierとProvisioning ProfileのApp Identifierを一致させる


これはネットのどこを見ても書いてある情報なので大丈夫だと思いますが一応。
面倒ならば、裏技的手法として、Provisioning ProfileのApp Identifierを*だけにするという手段があります。こんな風に。
ABCDEFGHIJK123.*
この方法だとオープンソースのプロジェクトを拾ってきたときでもすぐに実機インストールすることができるので、いちばんお勧めかもしれません。


2.クリーニングする(Shift+Command+K)

で、1.は毎回試すのですが、それでもうまくいかないので次はこれ。クリーニングを行います。
もしくはプロジェクトフォルダ以下のbuildディレクトリの中身をすべて消してしまっても同様の効果が得られます。


今のところこの二つを試したら毎回うまくビルド出来ているので、とりあえずはこれで大丈夫かと思います。

2009年1月24日土曜日

route-meでタッチイベントを扱いたい

  • タッチイベントを扱うときはRMMapViewDelegateプロトコルを採用する
  • シングルタッチ、ダブルタッチを感知したり、マーカー上のタッチやドラッグを感知したり、地図の移動およびズームを感知したりできる
  • 現状、マップのドラッグやズームを使用不可能にするための手段は用意されていない。Delegateの返り値による操作もできない。
  • UIMapViewにenableDraggingおよびenableZoomというインスタンス変数が用意されているが、mapView->enableDraggingのようにしてアクセスしようとするとコンパイルエラーになってしまう。

route-me上でタッチイベントを取得してみました。

こんな感じで、画面上のタップした点を取得することができます。

タッチイベントの取得方法は、
まずRMMapViewDelegateプロトコルを任意のクラスに適合させて、
@interface MapViewController : UIViewController  {
/* 中略 */
- (void) singleTapOnMap: (RMMapView*) map At: (CGPoint) point
{
NSString *message = [NSString stringWithFormat:@"(%f, %f)", point.x, point.y];
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"singleTapOnMap"
message:message
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil];
[alertView show];
}
/* 後略 */

適合させたクラスのオブジェクトをRMMapViewのdelegateプロパティにセットします。
RMMapView *mapView = [[[RMMapView alloc]
initWithFrame:[UIScreen mainScreen].applicationFrame WithLocation:initialLocation.coordinate]
autorelease];
mapView.delegate = self;

普通のUIViewとほとんど同じですね。

しかしこの方法では画面上の座標がとれるだけで、緯度経度を取ることができませんので、
RMMapViewのメソッドを用いて、緯度経度に変換します。
@interface MapViewController : UIViewController  {
/* 中略 */
- (void) singleTapOnMap: (RMMapView*) map At: (CGPoint) point
{
//ポイントをLatLongに変換して表示する
CLLocationCoordinate2D coordinate = [map pixelToLatLong:point];
NSString *message = [NSString stringWithFormat:@"(%f, %f)", coordinate.latitude, coordinate.longitude];
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"singleTapOnMap"
message:message
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil];
[alertView show];
}
/* 後略 */

これで緯度経度になりました。

ところで、地図のスクロールやズームを制限して利用したいというときがあると思います。
たとえば日本向けのアプリなのにアルゼンチンの地図をスクロールして表示されたら困るという場合です。

早速地図のスクロールを制限する方法を調べてみたところ、
RMMapViewにいかにもそれらしいメンバー変数を発見。
@interface RMMapView : UIView
{
RMMapContents *contents;
id delegate;
BOOL enableDragging;
BOOL enableZoom;
RMGestureDetails lastGesture;
float decelerationFactor;
}

しかしながらこのメンバー変数、プロパティも操作するためのメソッドもなく、外部から操作することができません。
ためしに無理矢理以下のようなコードを書いてアクセスしてみたところ、コンパイルではねられました。
RMMapView *mapView = [[[RMMapView alloc]
initWithFrame:[UIScreen mainScreen].applicationFrame WithLocation:initialLocation.coordinate]
autorelease];
//以下の行でエラー。アクセスできません
mapView->enableDragging = NO;

残念ながらまだこの機能は現在のバージョンのroute-meでは利用できないみたいです。

その他、delegateメソッドの返り値をNOにしたらスクロールしなくなるとかそういう機能がないか調べてみましたがやはりなさそうで、今のところ地図のスクロールおよびズームを制限するのは難しそうです。今後のバージョンアップに期待ですね。

2009年1月11日日曜日

iPhoneやiPhoneシミュレータ上でNSURLCacheクラスを使う

  • NSURLConnectionやNSURLDownloadを利用すると自動的にNSURLCacheにキャッシュデータを蓄える
  • iPhoneシミュレータは/private/var/folders/XX/XXXXXXXXXXXXXXXXXXXXXXX/-Caches-/iPhoneのアプリ名/Cache.dbの中にキャッシュデータを蓄えている
  • iPhone実機では、メモリ上へのキャッシュは働くがファイル上へのキャッシュは行われない。したがってアプリを終了するとキャッシュはすべて消える。
  • NSURLCacheクラスについて参考 http://episteme.arstechnica.com/eve/forums/a/tpc/f/8300945231/m/863005881931/p/5
  • [NSURLRequest setCachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData];を使って、意図的にNSURLCacheクラスへのキャッシュを止めることもできる
  • キャッシュを停止する方法の例 http://github.com/takuma104/ntlniph/tree/master/Classes/models/NTLNHttpClient.m 76行目

HTTP通信を行う際にキャッシュを使いたい場合があると思います。
特に通信状況の良くないiPhoneプログラムでは、キャッシュを利用したいと思う機会が多いはずです。
Cocoaフレームワーク上でHTTP通信を行う場合には、NSURLConnectionクラスやNSURLDownloadクラスを利用するのが一般的だと思いますが、
これらの通信クラスを利用すると、自動的にNSURLCacheクラスのShared Instanceに通信結果がキャッシュされていくようなしくみになっています。

キャッシュされた結果は以下のようなコードで取り出せます。
  NSURL *url = [NSURL URLWithString: urlStr];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
//キャッシュされたURLレスポンスを、NSURLCacheのshared instanceから取得します
NSCachedURLResponse *cachedData = [[NSURLCache sharedURLCache] cachedResponseForRequest:request];

こうして取得されたNSCachedURLResponseは通常のNSURLResponseと同じように扱うことができます。

で、問題になってくるのはここからです。
このNSURLCacheクラスは2種類のキャッシュを内部的に持っています。メモリキャッシュとファイルキャッシュです。
メモリキャッシュの方はもはや説明不要だと思うのですが、やっかいなのはファイルキャッシュのほうです。再起動しても結果が消えないため、以前の結果が表示されると言うことが起こりえます。
iPhoneシミュレータ上で実行された場合、どこにこのURLレスポンスのキャッシュが保存されているかを調べてみたところ、
/private/var/folders/XX/XXXXXXXXXXXXXXXXXXXXXXX/-Caches-/iPhoneのアプリ名/Cache.db

の中にキャッシュが生成されていることがわかりました。(XXの部分は実行するマシンによって異なります。)
従って、キャッシュが不要になった場合はこのファイルを消してください。

2009年1月5日月曜日

route-meの、地図画像 (Tile) のキャッシュを操作したい

  • 公式のドキュメントはこちら http://code.google.com/p/route-me/wiki/CacheConfiguration
  • キャッシュにはメモリに対するキャッシュ、ファイルを利用したキャッシュ、sqliteを利用したキャッシュの3通りがある。デフォルトではメモリに32枚、sqliteに無制限に地図画像をキャッシュするようになっている。
  • キャッシュの設定を変更するにはrouteme.plistファイルを用いる
  • route-meは内部的にsqliteを処理するためにFMDBライブラリ (http://gusmueller.com/blog/archives/2008/03/fmdb_for_iphone.html) を利用しているため、route-meを利用したプロジェクトでは何もしなくてもFMDBが使える
  • たとえば、一度OpenStreetMapで開いた地図画像のキャッシュは、地図提供元をVirtualEarthにしてもOpenStreetMapにしても残ってしまうので、一度キャッシュクリアする必要がある
  • キャッシュをまとめて削除したいときはrouteme.plist中でdb-cacheのcapacityとminimalPurgeを1にする
  • またはSQLを直接発行してZCACHEテーブルをDELETEすればよい
  • またはiPhone Simulatorのdbファイルを直接Finderから削除する (~/Library/Application Support/iPhone Simulator/User/Applications/英字の長ったらしい識別子/Documents/*.sqlite) 参考はhttp://ameblo.jp/xcc/entry-10171488747.html

route-meを使っているうちに、困った事態に遭遇しました。
最初の頃に使っていたOpenStreetMapの地図画像が、いつまでたってもVirtualEarthの地図画像に入れ替わらないんです。

ごらんの通りOpenStreetMapの地図が出ます。tileSourceはVirtualEarthになっているのに!

iPhoneシミュレータをリセットしたりアプリケーションを削除したりしましたが効果なしです。
そこで原因を調査したところ、route-meは内部的にsqliteを用いた画像データのキャッシュを持っており、このキャッシュが自動的に最新のデータと入れ替わらないのが原因であることがわかりました。
(通常キャッシュはある程度保持期間が決まっていて、保持期間を過ぎたデータは破棄するようになっていると思うのですが、route-meについては少なくとも1日2日程度の期間ではキャッシュを破棄してくれないようです)
(というかsqliteのデータベースってアプリケーションごとに/var/root/Library/アプリケーション名/以下に配置されているからアプリケーションを削除したら消えるんじゃないんですか・・・?それとも全部ゴミになって残るの?単にiPhoneシミュレータが悪い?)

そこでキャッシュを破棄する方法を調べるがてら、route-meの地図画像キャッシュを設定する方法について調べてみました。
http://code.google.com/p/route-me/wiki/CacheConfiguration
公式のドキュメントをみると、"routeme.plist"というplistファイルを作成してビルドターゲットに加えることで、地図画像キャッシュの動作を変更することができるらしいです。

こんな感じで。


こうすることで、sqliteを用いたキャッシュの中に保存されるデータ量を制限することができます。
ただし、データベースの構造をみても、キャッシュ古くなったデータを自動的に削除するような機能はないようです。そのため、毎日決まった箇所の地図データを見るようなアプリの場合は、何年たっても地図の画像データが更新されません。
このように地図の画像データが更新されない恐れがある場合には、sqliteを用いたキャッシュを利用しないように設定するのがよいと思います。


さて、これで今後のキャッシュの利用方法は設定することができましたので、いますでにキャッシュされてしまっているデータを削除しましょう。
route-meではZCACHEというテーブルの中に地図画像キャッシュデータを保存していますので、このテーブルのデータを削除すればよいです。
sqliteにはTRUNCATEが無いので、DELETE文を使ってすべてのデータを削除します。
DELETE FROM ZCACHE

参考:http://www.mail-archive.com/sqlite-users@sqlite.org/msg09144.html

また、iPhoneシミュレータ上でキャッシュを消したい場合には、以下のパスにsqliteのデータベースファイルが存在しますので、そちらを丸ごと削除すればより簡単です。
(~/Library/Application Support/iPhone Simulator/User/Applications/英字の長ったらしい識別子/Documents/*.sqlite)

さぁ削除していざチャレンジ!


・・・あの、変わってないんですけど。何で?どうして?
ログをコンソールから調べてみましたが、routeme.plistが正しく読み込まれていて、間違いなくDBへのアクセスは発生していません。
となると怪しいのは地図提供元URLへのアクセスです。ここで何らかの仕組みが働いてキャッシュされているのではないかと思って調査してみました。
すると、RMWebTileImageクラスの中にこんなソースを発見。
 NSURL *url = [NSURL URLWithString: urlStr];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSCachedURLResponse *cachedData = [[NSURLCache sharedURLCache] cachedResponseForRequest:request];

怪しいです。怪しさ大爆発です。次回はちょっと脇道にそれて、このNSURLCacheについて調査してみようと思います。

2009年1月3日土曜日

iPhoneでMapView route-meを使ってみよう! RMMarkerManager編

  • マーカーを配置するには、まずRMMarkerクラスのインスタンスを適当に生成する
  • 生成には[RMMarker markerWithNamedStyle:]を使うと便利。styleNameはRMMarker.hで2種類ほど定数として定義されている
  • マーカーを生成したら、[RMMarkerManager addMarker: AtLatLong:]で地図上に配置する
  • RMMarkerのlocation初期値はおそらくnilなので、生成したマーカーを何も考えずに[RMMarkerManager addMarker]とするとエラーになるはず(未確認)
  • マーカーにラベルをつけるときには[RMMarker setLabel:]または[RMMarker setTextLabel:]でよい。setTextLabelは正直いまいち

地図のズーム設定に初期位置設定もできるようになり、さらには地図の提供元の設定もできるようになりました。
しかしこれだけではただの地図が存在するだけです。実用的なアプリケーションにするためには、地図上に情報を配置して、アプリケーションからユーザーに何かを提示してあげなければなりません。
ということでお次は地図上にマーカーを配置してみようと思います。

route-meには標準でマーカーを管理するための機能があります。
マーカーそのものを表すRMMarkerクラスと、マーカーを管理するRMMarkerManagerクラスです。
まずはRMMarkerManagerクラスからみていきましょう。RMMarkerManager.hから、重要そうなプロパティおよびメソッドだけを切り出してみました。
//マーカーの追加・削除・表示非表示
- (void) addMarker: (RMMarker*)marker;
- (void) addMarker: (RMMarker*)marker AtLatLong:(CLLocationCoordinate2D)point;
- (void) addDefaultMarkerAt: (CLLocationCoordinate2D)point;
- (void) removeMarkers;
- (void) hideAllMarkers;
- (void) unhideAllMarkers;

//マーカーの取得
- (NSArray *)getMarkers;
- (void) removeMarker:(RMMarker *)marker;
- (CGPoint) getMarkerScreenCoordinate: (RMMarker *)marker;
- (CLLocationCoordinate2D) getMarkerCoordinate2D: (RMMarker *) marker;
- (NSArray *) getMarkersForScreenBounds;

//状態を取得
- (BOOL) isMarkerWithinScreenBounds:(RMMarker*)marker;
- (BOOL) isMarker:(RMMarker*)marker withinBounds:(CGRect)rect;
- (BOOL) managingMarker:(RMMarker*)marker;

//マーカーの移動
- (void) moveMarker:(RMMarker *)marker AtLatLon:(RMLatLong)point;
- (void) moveMarker:(RMMarker *)marker AtXY:(CGPoint)point;

一通り必要そうなメソッドはそろっており、「使い方等はもう宣言をみればわかるだろ!」といわんばかりの丁寧な作りになっています。
マーカーを配置したければ、- (void) addMarker: (RMMarker*)markerあたりを呼び出せばよさそうですね。

次にRMMarker.hをみてみます。重要な物だけ抜粋してみました。
//マーカーインスタンスの生成
+ (RMMarker*) markerWithNamedStyle: (NSString*) styleName;
- (id) initWithCGImage: (CGImageRef) image anchorPoint: (CGPoint) anchorPoint;
- (id) initWithCGImage: (CGImageRef) image;
- (id) initWithKey: (NSString*) key;
- (id) initWithUIImage: (UIImage*) image;
- (id) initWithStyle: (RMMarkerStyle*) style;
- (id) initWithNamedStyle: (NSString*) styleName;

//ラベルの設定および表示&非表示
- (void) setLabel: (UIView*)aView;
- (void) setTextLabel: (NSString*)text;
- (void) setTextLabel: (NSString*)text toPosition:(CGPoint)position;
- (void) toggleLabel;
- (void) showLabel;
- (void) hideLabel;
- (void) removeLabel;

//マーカーの画像変更および表示&非表示
- (void) replaceImage:(CGImageRef)image anchorPoint:(CGPoint)_anchorPoint;
- (void) hide;
- (void) unhide;

//各種プロパティ(おそらくここから直接操作することはない)
@property (assign, nonatomic) RMXYPoint location;
@property (retain) NSObject* data;
@property (nonatomic, retain) UIView* labelView;

デフォルトで用意されているマーカーを利用する場合は、+ (RMMarker*) markerWithNamedStyle: (NSString*) styleNameを使えばよさそうです。
自分でマーカーの画像を指定する場合は- (id) initWithUIImage: (UIImage*) image;などを用いればうまくいきそうです。

では実際に地図上にマーカーを配置してみます。
例によってRMMapViewの初期表示時に、以下のようなソースを追加します。
    CLLocation *markerLocation = [[[CLLocation alloc]
initWithLatitude:35.689613731585375 longitude:139.7616720199585]
autorelease];
RMMarker *marker = [RMMarker markerWithNamedStyle:RMMarkerRedKey];
[mapView.markerManager addMarker:marker AtLatLong:markerLocation.coordinate];

ソースの先頭で"RMMarker.h"と"RMMarkerManager.h"を#importするのをお忘れなく!
では実行してみましょう。


ばっちりです!マーカーが表示されました。

今度はマーカーにラベルをつけてみます。
ラベルとはマーカーの上に表示される説明書き領域です。RMMarkerには標準でラベルを扱う機能があります。
ラベルとしてシンプルに文字列を指定する方法と、凝ったUIViewを指定する方法が用意されていますが、まずはシンプルに文字列を出してみようと思います。
    [marker setTextLabel:@"Japan Meteorological Agency"];

これを実行すると・・・


出ましたね。
しかし正直言ってこれではわかりづらすぎです。とても実用に耐えません。
ラベルを使いたい場合には、setTextLabel:を用いず、きちんとしたUIViewを設計してから、setLabel:を利用して指定する方がよいと思います。

2009年1月1日木曜日

iPhoneでMapView route-meを使ってみよう! RMMapContents詳細編

  • 利用する地図の提供元を変更したい場合には、tileSourceプロパティを変更する。利用できるtileSourceはMapViewプロジェクト内の/Map/Tile Sourceグループの中にある
  • 地図画像(Tile)が更新されたときに呼び出すためのdelegate(RMTilesUpdateDelegate)を持っている
  • 投影方法(Projection)をメルカトル図法以外に変更できるようなプロパティがあるが、実際に変更できるかどうかは未調査

前回の記事ではroute-meライブラリのRMMapViewクラスを調査して、地図の初期表示座標やズーム倍率を設定する方法を調べてみました。
今回はroute-meライブラリのうち、地図の実体そのものを表現するRMMapContentsクラスを調査してみようと思います。

さっそくRMMapContents.hを読んでみます。利用できるプロパティとメソッドの一覧はこんな感じです。
@property (readwrite) CLLocationCoordinate2D mapCenter;
@property (readwrite) RMXYRect XYBounds;
@property (readonly) RMTileRect tileBounds;
@property (readonly) CGRect screenBounds;
@property (readwrite) float scale;
@property (readwrite) float zoom;

@property (readwrite) float minZoom, maxZoom;

@property (readonly) RMTileImageSet *imagesOnScreen;

@property (readonly) RMProjection *projection;
@property (readonly) id mercatorToTileProjection;
@property (readonly) RMMercatorToScreenProjection *mercatorToScreenProjection;

@property (retain, readwrite) id tileSource;
@property (retain, readwrite) RMMapRenderer *renderer;

@property (readonly) CALayer *layer;

@property (retain, readwrite) RMMapLayer *background;
@property (retain, readwrite) RMLayerSet *overlay;
@property (retain, readonly) RMMarkerManager *markerManager;
@property (nonatomic, retain) id tilesUpdateDelegate;
@property (readwrite) NSUInteger boundingMask;

- (id) initForView: (UIView*) view;
- (id) initForView: (UIView*) view WithLocation:(CLLocationCoordinate2D)latlong;

// Designated initialiser
- (id)initForView:(UIView*)view WithTileSource:(id)tileSource WithRenderer:(RMMapRenderer*)renderer LookingAt:(CLLocationCoordinate2D)latlong;

- (void) didReceiveMemoryWarning;

- (void)moveToLatLong: (CLLocationCoordinate2D)latlong;
- (void)moveToXYPoint: (RMXYPoint)aPoint;

- (void)moveBy: (CGSize) delta;
- (void)zoomByFactor: (float) zoomFactor near:(CGPoint) center;
- (void)zoomInToNextNativeZoomAt:(CGPoint) pivot animated:(BOOL) animated;
- (void)zoomByFactor: (float) zoomFactor near:(CGPoint) center animated:(BOOL) animated;

- (void)zoomInToNextNativeZoomAt:(CGPoint) pivot;
- (float)adjustZoomForBoundingMask:(float)zoomFactor;
- (void)adjustMapPlacementWithScale:(float)aScale;
- (void)setZoomBounds:(float)aMinZoom maxZoom:(float)aMaxZoom;

- (void) drawRect: (CGRect) rect;

// During touch and move operations on the iphone its good practice to
// hold off on any particularly expensive operations so the user's
+ (BOOL) performExpensiveOperations;
+ (void) setPerformExpensiveOperations: (BOOL)p;

- (CGPoint)latLongToPixel:(CLLocationCoordinate2D)latlong;
- (CGPoint)latLongToPixel:(CLLocationCoordinate2D)latlong withScale:(float)aScale;
- (CLLocationCoordinate2D)pixelToLatLong:(CGPoint)aPixel;
- (CLLocationCoordinate2D)pixelToLatLong:(CGPoint)aPixel withScale:(float)aScale;

- (void)zoomWithLatLngBoundsNorthEast:(CLLocationCoordinate2D)ne SouthWest:(CLLocationCoordinate2D)se;
- (void)zoomWithRMMercatorRectBounds:(RMXYRect)bounds;

- (RMLatLongBounds) getScreenCoordinateBounds;
- (RMLatLongBounds) getCoordinateBounds:(CGRect) rect;

- (void) tilesUpdatedRegion:(CGRect)region;

長い!長いです!
RMMapContentsクラス独自のメソッドだけではなくて、RMMapViewクラスが持っている機能と同じ、地図の表示位置を動かしたり、拡大倍率を操作するメソッドも持っています。
どういうことかと調べてみると・・・
-(void) moveToXYPoint: (RMXYPoint) aPoint
{
if (delegateHasBeforeMapMove) [delegate beforeMapMove: self];
[contents moveToXYPoint:aPoint];
if (delegateHasAfterMapMove) [delegate afterMapMove: self];
}
-(void) moveToLatLong: (CLLocationCoordinate2D) point
{
if (delegateHasBeforeMapMove) [delegate beforeMapMove: self];
[contents moveToLatLong:point];
if (delegateHasAfterMapMove) [delegate afterMapMove: self];
}

どうやらRMMapViewクラスのメソッドの実装は、デリゲートのメソッドの呼び出しに加えて、RMMapContentsクラスのメソッドを呼び出しているだけみたいですね。

閑話休題。
たくさん利用できるプロパティとメソッドがありますが、今回はその中でも「利用する地図の提供元を制御する」方法を調べてみようと思います。

RMMapViewの標準の地図提供元はOpenStreetViewです。
http://www.openstreetmap.org/
OpenStreetViewとは一般のユーザーがGPSを利用して測定した道などの地理情報を自由にアップロードして作るオープンソースな地図プロジェクトです。
このOpenStreetView、ヨーロッパなどのプロジェクト参加者の多い地域では非常に精度がよいのですが、
日本ではまだそれほど定着していないようで、ちょっと田舎に入ると地図の精度が絶望的なことになってきます。

ということで、地図の提供元をOpenStreetViewから別の提供元に変更する必要があります。
route-meでは、OpenStreetView以外にも標準で、
Microsoft VirtualEarth(http://www.microsoft.com/japan/virtualearth/ http://maps.live.com/
またはCloudMade(http://www.cloudmade.com/ http://maps.cloudmade.com/
の地図を利用することができます。

やり方は簡単で、RMMapViewのcontentsプロパティのtileSourceプロパティに、
MapViewプロジェクト内の/Map/Tile Sourceグループ内のクラスを指定してやるだけです。
たとえばMicrosoft VirtualEarthを利用するときは、以下のようにします。
    RMMapView *mapView = [[[RMMapView alloc]
initWithFrame:[UIScreen mainScreen].applicationFrame WithLocation:initialLocation.coordinate]
autorelease];
mapView.contents.tileSource = [[[RMVirtualEarthSource alloc] init] autorelease];

これを実行すると以下のようになります。


見事VirtualEarthになりました。これで日本地図を拡大しても安心です。

※2009/01/11追記
本記事で紹介している、RMMapViewクラスのcontentsプロパティのtileSourceプロパティを書き換える方法を用いると、
新しくサーバーからロードされてくるタイルに関しては変更後のtileSourceからロードされてくるのですが、
既存のロード済みのタイルに関しては変更前のtileSourceからロードされてキャッシュされている画像がそのまま表示されてしまいます。

従って、以下のような実装ができません。
  • 最初からOpenStreetMap以外の地図提供元を利用した実装ができない。RMMapViewのinitと同時に最初の地図情報がOpenStreetMapからロードされてしまうため。
  • あとからユーザーの操作に応じて地図の提供元を変更するような実装ができない。
この問題はroute-meプロジェクトのバグトラッカーにも掲載されています。
http://code.google.com/p/route-me/issues/detail?id=12#c5
現在のところは残念ながら改善されていないようです。とりあえず今のところは、以下のような対応を心がけましょう。
  • 途中で地図の提供元を変更する場合には、動的に画像キャッシュをクリアする。ただし、簡単にキャッシュをクリアできるような構造になってはいない。
  • 最初からOpenStreetMap以外の地図提供元を利用するときには、ダウンロードしてきたMapViewプロジェクトのソース自体を直接修正する。
具体的にはRMMapContents.mの56行目の、以下の箇所を書き換えることで実現できます。
- (id) initForView: (UIView*) view WithLocation:(CLLocationCoordinate2D)latlong
{
//この行のRMOpenStreetMapsSourceを変更する
id _tileSource = [[RMOpenStreetMapsSource alloc] init];
RMMapRenderer *_renderer = [[RMCoreAnimationRenderer alloc] initWithContent:self];

id mapContents = [self initForView:view WithTileSource:_tileSource WithRenderer:_renderer LookingAt:latlong];
[_tileSource release];
[_renderer release];

return mapContents;
}

バグトラッカーにも高い優先順位で掲載されているので、近い将来に対応されることが期待できますので、それまではちょっと様子見という感じでしょうか?

iPhoneでMapView route-meを使ってみよう! RMMapView詳細編

  • RMMapViewには主に「現在の表示位置」「現在のズーム」を操作するためのメソッドが用意されている
  • RMMapViewのプロパティとして、マップ自身を表すRMMapContents、マップ上のマーカーを操作するRMMarkerManager、そしてマップからの操作を受け取るRMMapViewDelegateが用意されている

前回の記事では実際にroute-meの地図を自分のアプリに組み込んでiPhoneシミュレーター上で動作させるところまでをやってみました。
今回はさらに一歩進んで、route-meの実体であるRMMapViewの使い方について調べてみようと思います。

○ソースを読んでみる
公式ページを見ても一切ドキュメントが用意されていない・・・ので、ココはソースを読んで解析するしかありません。
まずは前回、実際にViewとして自分のアプリに読み込ませた、RMMapView.hを調査してみます。
// Any other functions you need to manipulate the mapyou can access through this
// property. The contents structure holds the actual map bits.
@property (readonly) RMMapContents *contents;

@property (retain, readonly) RMMarkerManager *markerManager;

// do not retain the delegate so you can let the corresponding controller implement the
// delegate without circular references
@property (assign) id delegate;
@property (readwrite) float decelerationFactor;

- (id)initWithFrame:(CGRect)frame WithLocation:(CLLocationCoordinate2D)latlong;

- (void)moveToLatLong: (CLLocationCoordinate2D)latlong;
- (void)moveToXYPoint: (RMXYPoint)aPoint;

- (void)moveBy: (CGSize) delta;
- (void)zoomByFactor: (float) zoomFactor near:(CGPoint) aPoint;
- (CGPoint)latLongToPixel:(CLLocationCoordinate2D)latlong;
- (CLLocationCoordinate2D)pixelToLatLong:(CGPoint)aPixel;
- (void)zoomInToNextNativeZoomAt:(CGPoint) pivot;
- (void)setZoom:(int)zoomInt;
- (void)zoomWithLatLngBoundsNorthEast:(CLLocationCoordinate2D)ne SouthWest:(CLLocationCoordinate2D)se;
- (void)setZoomBounds:(float)aMinZoom maxZoom:(float)aMaxZoom;

- (RMLatLongBounds) getScreenCoordinateBounds;

うーん読みやすい。すばらしい。
ヘッダのソースコードを読めば、リファレンスがなくても普通に使えそうな感じですね。

では実際に使ってみます。
手始めに、前回までは初期表示としてオーストラリアの片田舎が表示されるようになっていたのですが、
これを日本全体が見えるような初期位置とズーム倍率に設定してみようと思います。

MapViewを初期生成するところで、以下のように初期位置を設定します。
- (void)loadView
{
CLLocation *initialLocation = [[[CLLocation alloc]
initWithLatitude:35.0 longitude:135.0]
autorelease];
self.view = [[[RMMapView alloc]
initWithFrame:[UIScreen mainScreen].applicationFrame WithLocation:initialLocation.coordinate]
autorelease]
}

initWithFrame:(CGRect)frame WithLocation:(CLLocationCoordinate2D)latlongの第2引数の値を生成するために、CLLocationを利用します。
CLLocationはCocoa Touchに標準で付属されている、Core Locationライブラリに含まれるクラスです。
GPSを使ったアプリを作られたことのある方でしたら馴染み深いクラスだと思います。GPSを使う場合には、上記の例のように直接initすることはなく、CLLocationManagerからCLLocationの値を生成します。
CoreLocationを利用するためだと思いますが、ビルド時にCLLocationの実体がないのでリンクができないと怒られてしまいます。以下の図のようにCoreLocation.Frameworkをプロジェクトに追加してください。もちろん追加したらチェックボックスを入れてターゲットに追加するのも忘れずに。


これでビルドが通るはずです。やってみましょう。


初期位置が日本になりました!でもズームがちょっと近すぎる感じがします。もう少し日本全体が入るようにしてみましょう。
[self.view setZoom:5];


これでも何となくうまくいきましたが、ズーム率をint型で指定するのはいまいちわかりづらいです。
表示したい地理的位置がわかっている場合には、緯度経度の値を用いてズームを設定するための便利なメソッドが用意されています。
CLLocation *northEast = [[[CLLocation alloc]
initWithLatitude:40.0 longitude:145.0]
autorelease];
CLLocation *southWest = [[[CLLocation alloc]
initWithLatitude:30.0 longitude:125.0]
autorelease];
[self.view zoomWithLatLngBoundsNorthEast:northEast.coordinate SouthWest:southWest.coordinate];


いい感じですね。日本全体が見えるようになりました。

その他、地図の中心位置を移動するためのメソッド(画面上のXY座標指定、緯度経度指定、偏差指定の3タイプあります)や、
Mapアプリ上で特定地点をダブルタップしたときのように、地図上の特定の位置に向かってズームするためのメソッドが用意されています。

2008年12月28日日曜日

自分のiPhoneアプリにroute-meを組み込んでみる

  • http://code.google.com/p/route-me/wiki/EmbeddingGuidev2 ただし一部間違いなど不正確なところがある
  • route-meはフレームワークになっていない(ただのXcodeプロジェクト)なので、自分のアプリに組み込むためにはXcodeプロジェクトを参照する設定を行わなければならない
  • MapViewを組み込む際にはInterface Builderを用いるよりもソースコードから直接Viewを作る方が楽

前回に引き続きMap Viewが使いたいということで、今回はいよいよ実際にroute-meを利用して自分のアプリ上でMap Viewを表示してみたいと思います。
参考にしたのは以下のページ。
EmbeddingGuidev2
http://code.google.com/p/route-me/wiki/EmbeddingGuidev2

○まずは何はなくともプロジェクトをダウンロードする
一週間に何度も更新が入るようなプロジェクトですので、常に最新のソースを利用したい方は、パッケージを落としてくるよりSubversionを利用してチェックアウトするのがおすすめです。
ということでコマンドプロンプトから以下のコマンドを実行。
$ svn checkout http://route-me.googlecode.com/svn/trunk/Proj4
$ svn checkout http://route-me.googlecode.com/svn/trunk/MapView

パッケージからダウンロードする場合でも、Subversionからチェックアウトする場合でも重要なことは、このProj4というプロジェクトディレクトリと、MapViewというプロジェクトディレクトリを同じディレクトリに配置する必要があるということです。例えばこんな感じです。
$ ls -l
total 0
drwxr-xr-x 12 akisute staff 408 12 25 22:06 MapView/
drwxr-xr-x 168 akisute staff 5712 12 25 22:04 Proj4/
drwxr-xr-x 13 akisute staff 442 12 25 22:07 ThisisMyProject/

これはMapViewがProj4プロジェクトを参照しているからのようです。
自分のプロジェクトについては何処に置いても大丈夫ですが、同じ場所においておいた方が後からプロジェクトの参照がしやすくていいかもしれません。

ちなみに、私たちユーザーが使うのはMapViewプロジェクトだけです。Proj4は内部的に利用されるだけですので、私たちが直接呼び出すことはありません。

○プロジェクトの参照を自分のプロジェクトに追加する
次は配置したMapViewプロジェクトへの参照を自分のプロジェクトに追加します。


プロジェクトに追加を選択して、先ほど配置したMapViewプロジェクトの中の、MapView.xcodeprojを選択します。
このとき、プロジェクトのコピーは作成してはいけません。「ディスティネーショングループのフォルダに項目をコピーする」というチェックボックスを選択しないようにしてください。


追加が成功するとこんな感じになります。
"Xcode Project Management Guide"の37ページ目(Referencing Other Projects)というところに書いてある方法なんだそうです。

・ビルドターゲットにファイルを追加
libMapView.aというファイルの横にある小さなチェックボックスをチェックします。


こんな風に。MapView.appはチェックしなくてよいです。
これでlibMapView.aがビルドターゲットに追加されます。

・ビルドターゲットの設定
ビルドターゲットの設定を開きます。プロジェクトメニューから選択するか、ターゲットを直接ダブルクリックします。


では直接依存関係というところに、先ほどのMapViewを追加します。+ボタンを押して追加してください。


このMapViewというのを追加します。
それから、リンク済みライブラリというやつをいくつか追加する必要があります。
QuartzCore.frameworkと、
libsqlite3.dylibというライブラリを追加してください。
原文では4つ追加するように指示されていますが、この2つだけで問題ないようです。

・ヘッダ検索パスを変更する
引き続きターゲットの設定ウィンドウから、「ビルド」タブを選択し、
「ヘッダ検索パス」という項目を探します。


このように、MapViewプロジェクトへのパスを設定します。
ここでは早退パスで指定していますが絶対パスでも問題ないと思います。
再起的チェックボックスを忘れずに。

・プロパティタブの識別子を設定
プロパティタブの中の、識別子という項目を設定します。
これはMapViewに限らずiPhoneアプリを実機にインストールする際に必ず必要になる設定なのですが、一応忘れずにということで。

・ここらで一度ビルドしてみる
Command + Bを押下して一度ビルドしてみます。ここまでの設定が間違いなければビルドに成功します。
「警告がいくつか出るかもしれないが無視してくれ」と原文には書いてましたけど、私の場合には警告は一つも出ませんでした。ラッキーです。


ビルドにどうしても成功しない場合には、こちらの画像を参考にしてみてください。ライブラリがきちんと追加されていなかったりしませんか?

・リソースの追加
MapViewプロジェクトの中にある画像ファイルを、
自分のプロジェクトにコピーしてきます。そうしないとマーカーの画像がでないんだとか。


こんな風に加えてみました。別に自分のプロジェクトの中なら何処に加えておいても問題ないとは思います。小さなチェックボックスをチェックして、ビルドターゲットに追加するのも忘れずに。

○いよいよマップを自分のプロジェクトに配置
マップを自分のプロジェクトに配置する方法には、
1:
2:
この二つがあります。1の場合は、Interface Builderを起動して、Viewを配置し、Viewのクラス名をRMMapViewに変更すれば基本的にはOKですが、Interface Builder上で追加しただけでは動かない(gccがコンパイル時にリファレンスを削除してしまうらしいです)のでちょっとしたハックをコード上で行う必要があります。
ViewControllerに以下のようなコードを追加してください。
- (void)viewDidLoad {
[super viewDidLoad];
[RMMapView class]; //この行がハック
}
これでInterface Builderから追加したRMMapViewが動作します。

2の場合は、以下のようなコードを書きます。


画像ですみません。

○そしていよいよアプリケーションを実行


無事に出ました!
(地図の一番下に空白があるように見えるのは私の設定ミスで、普通に先ほどまでの記述に従って作ればきちんと全面が地図になると思います)
デフォルトの設定だと、オーストラリアのキャンベラ近郊のど田舎が最大倍率で表示されるようです。

○次回予告
一応地図は出ましたが、このままでは役に立たないので、
次回は初期表示位置の設定、初期倍率の設定、スクロール範囲の制御(日本の外は見れなくする)、マーカーの配置、クリックイベントの取得のやり方などを調べてみようと思います。

2008年12月27日土曜日

iPhoneでMap Viewを使いたいので、ライブラリを探してみました

  • iPhone Google Maps Component
  • route-me
  • TouchMap
  • 個人的にはroute-meがおすすめ、ただしMicrosoft Virtual Earthを使うことになる
  • ストリートビューが欲しい、またはどうしてもGoogle Mapで実装したいという人はAppleがCocoa Touchに組み込んでくれるのを期待しつつ待つしかない

iPhoneで開発をしている人なら、誰しも一度はこう思うでしょう。
「標準のGoogle Mapアプリみたいに、地図を使ったアプリが作りたい」と!
GPS・加速度センサー・タッチ操作に強力な通信機能、おまけに3D描画もできると、
こんなにすてきな機能と地図がくみ合わさったら、その可能性は無限大に違いありません。

が。しかし、なんということでしょう。
皆様ご存知の通り、Cocoa Touch Frameworkに地図機能は存在しないのです!
ライバルのAndroidにはあんなにすてきな地図機能があって、自由に使えるというのに!

さてさて前置きが長くなってしまいましたが、要するに、

「iPhoneでMap Viewが使いたいんだけどどうすりゃいいの」

ということです。
調べてみたところ、以下の3つのオープンソースライブラリが見つかりました。

○iPhone Google Maps Component
http://code.google.com/p/iphone-google-maps-component/
その名の通り、Google Mapを利用したMap Viewライブラリです。
実装にはiPhoneのUIWebViewを利用しており、JavaScriptを用いてGoogle Mapにアクセスし、
描画を行っているようです。
画面上に描画点(プロットとか画像とか)を自由に配置することができます。

○route-me
http://code.google.com/p/route-me/
こちらはObjective-Cネイティブ実装のMap Viewライブラリです。
ネイティブ実装であるため、きわめて軽快で高速な動作が特徴です。
ただし地図の提供元がOpenStreetMapか、Microsoft Virtual Earthに限られてしまいます。

○TouchMap
http://toxicsoftware.com/touchmap/
全く未知数です。
Objective-CまたはC言語による実装で、
地図の提供元がMicrosoft Virtual Earthであるということ以外何もわかりません。

いずれもGoogle Map StreetViewには対応していません。

さて、この中のどれを選ぼうか・・・というところですが
まずiPhone Google Maps Componentは真っ先に除外されます。
JavaScriptで実装されているため実機では重すぎて使い物になりません。唯一のGoogle Mapsを用いた実装だけに非常に残念です。
残る二つからroute-meをとるか、それともTouchMapをとるか悩みましたが、
TouchMapのほうがドキュメントが少なく、また開発頻度が悪い(10月から一度も更新されていない)ため、
現在最も勢いのありそうなroute-meを今回採用することにしました。


次回はroute-meのページに用意されているインストールの手引きを見ながら、
実際に自分のアプリの中で地図を動かしてみようと思います。
http://code.google.com/p/route-me/wiki/EmbeddingGuidev2

2008年12月25日木曜日

CS193P Cocoa Programming - AssignmentPresence3まで完了


最近ご無沙汰気味でしたが、
なんとかPresenceアプリ作成の宿題3まで完了させることができました。

リロードボタンと投稿ボタンがつきました。



詳細画面を見るとこんな感じです。ちょっと寂しい。




投稿ボタンを押すとこうなります。
UITextFieldではなくてUITextViewを使った方がよかった気がします。まぁいいや。


ソースコードなどは以下に公開しております。
http://github.com/akisute/akisute_cs193p/tree/master


残るはPresence4だけなのですが、Presence4の内容は正直それほど難しくない(TabBarを使うぐらい)ので、
ここらですっ飛ばして今興味があるroute-meという地図ライブラリの解析でもやってみようと思います。

2008年12月21日日曜日

Cocoa(iPhone)で、日本語を含むURLを開く方法

  • 基本的にはCore FoundationのC関数を利用する
    CFURLCreateStringByAddingPercentEscapes()
  • ただし、一部問題のあるケースがある
    URL中に&を含む場合などは正しく作れないので別の方法が必要

プログラマをやっていると、だんだんと日本語が嫌いになってきます。
いや、嫌いというのはおかしいのですが、とにかく英語以外の言語はトラブルが多いです。
コンピューターというのはつくづく英語を処理するためだけに作られているのだと思います。
(だからこそ、プログラムの「国際化」で飯を食える人がいる訳ですけど!)

すみません、前置きが愚痴っぽくなってしまいました。
iPhoneプログラミングにおいてももちろん、日本語を使う際に罠があります。
たとえばNSURLRequestなどがそうです。
日本語の含まれるURLを開こうとすると、うまく解釈してくれないんです。
(といいますか日本語が含まれるURLはURLエンコードしなくちゃいけないんです。・・・当たり前ですね)

例えばこんな感じです:
http://s3.amazonaws.com/twitter_production/profile_images/65140989/るーみゃ_normal.jpeg


自分の画像だけ出ないぞ!

そこでURLエンコーディングを行う関数を探してみたところ、ありました。
Cocoaで日本語のGETを飛ばそう!

早速真似してやってみました。
CFURLCreateStringByAddingPercentEscapes()関数を使うのがポイントらしいです。
            NSString *originalUrl = [userDict objectForKey:@"profile_image_url"];
           NSString *encodedUrl = (NSString *) CFURLCreateStringByAddingPercentEscapes
           (NULL, (CFStringRef) originalUrl, NULL, NULL, kCFStringEncodingUTF8);
           person.profile_image_url = encodedUrl;

たったのこれだけでうまくいきました!



ところが、有頂天になってTwitter上で騒いでたら、
@psychsさんから突っ込みが。
JavaScriptでいうencodeURI相当なんで、
日本を適当にエスケープしたいときにはいいんだけど、
たとえばパラメタに「&」を入れたい場合とかは、
ちゃんとencodeURIComponent 相当のやつを使って組み立てなきゃだめだよ

なんと具体的なアドバイス!(ありがとうございます!)ちょっと調べてみました。
javascript: escape(), encodeURI(), encodeURIComponent() 比較

細かいところはわかりませんが、とにかく一部の文字がエンコーディングされないみたいなのです。
深刻なのは?とか=とかでしょうか。気をつけないとトラブルに巻き込まれそうですね。

ちなみにiPhoneでencodeURIComponent相当のエンコーディングを行うためにはどうすればよいのかですが、
リファレンスを引いてみても特にそれらしきものは見当たりませんでした。
ひょっとしたら自分でやるしかないかもしれません。

■2008/12/29 22:00追記
Twitter上で、@norio_nomuraさんにencodeURIComponent相当のエンコードを行う方法を教えていただきました!ありがとうございました!
http://twitter.com/norio_nomura/status/1083641557
CFURLCreateStringByAddingPercentEscapes(NULL, string, NULL, CFSTR (";,/?:@&=+$#"), kCFStringEncodingUTF8);

最大のポイントは、第4引数のCFSTRです。ここで指定した文字列はエンコードされずにそのまま残るようです。
第4引数をNULLのまま使うと標準のencodeURL相当になり、
この例のように指定すると、encodeURLComponent相当のエンコードが可能になります!

iPhoneアプリをローカライズ(国際化対応)してみた

  • まずはNSLocalizedString()を利用してアプリを作る
  • 次にgenstringsコマンドを利用してLocalizable.stringファイルを自動生成する
  • Xcodeに取り込む
  • Xcodeからローカライズの設定を行う
  • 最後にかっこよく翻訳を行う
  • info.plistファイルの中身をローカライズしたい(たとえばアプリ名など)ときは、
  • InfoPlist.stringsファイルを手で作って、ローカライズ設定をして、翻訳すると良い
  • Localizable.stringは基本UTF-16に統一しておくこと
  • InfoPlist.stringsはUTF-16でないと動かない。Localizable.stringsはUTF-8でいいらしい(未確認)

iPhoneアプリを作ったならば、
「一人でも多くの人に使ってもらいたい」
「1本でも多く売りたい」
というのが心情というものです。
日本ではヒットしないアプリが海外では大ブレイクということも考えられますし、
iPhoneアプリの市場は日本国内よりも海外のほうが圧倒的に大きいです。
従って、iPhoneでアプリを作るなら、国際化対応は必須であるといえましょう!

ということで、今日はそんな大事な大事な国際化のやり方を勉強してみました。
Cocoa Frameworkは元々非常に国際化がしやすい作りになっていますので、
iPhoneアプリでもその仕組みの恩恵を受けることが出来ます!




国際化の第1歩はアプリ作成からです。まずはアプリを作らないと始まりません。
このとき、普段は画面に表示する文字列を
self.navigationItem.title = @"Title";

こんな感じで指定していると思いますが、
これをNSLocalizedString()関数を利用して
self.navigationItem.title = NSLocalizedString(@"Title",
@"Title message for the main view");

こんな風に書き直します。
第1引数はメッセージキー(普通は英語の文字列をそのまま使えばいいと思います)、
第2引数はコメントです。コメントは面倒でしたらnilでも大丈夫ですが、あると翻訳時に大変便利です。


アプリ側の準備が出来たら、次は翻訳文字列ファイル(Localizable.strings)を生成します。
手で作っても良いのですが、genstringsという大変便利なコマンドが最初から用意されているので、
こちらを利用するのが良いと思います。
使い方はこんな感じです。
genstrings [オプション] ファイル名

たとえば
genstrings *.m

コレを実行すると、カレントディレクトリにあるすべての拡張子がmのファイルから
自動的にLocalizable.stringsをつくってカレントディレクトリに保存してくれます。
生成されたLocalizable.stringsはUTF-16でエンコードされています。
(BOMのありなし、およびエンディアンはリトルかビッグかについては不明ですが、
とにかくUTF-16としてMac, Xcode, およびiPhoneが正しく認識してくれるのは間違いないです)

これだけだとプロジェクト内のすべてのソースをまとめてファイルにしてくれないので、
実際の開発時にはfindコマンドを併用して、以下のように実行するのがおすすめです。
genstrings -a $(find . -name "*.m") 

このコマンド入力によって!
カレントディレクトリ以下のすべてのの拡張子がmのファイルを対象にしてLocalizable.stringsを生成してくれる
ソースコードが更新されていた場合、以前に作成したLocalizable.stringsの内容に新しく追加された文字列が追加される
(-aオプションがないとファイルは上書き保存されてしまいます)


Localizable.stringsが生成されたら、Xcodeに取り込みます。
単純にファイルをXcodeプロジェクトに対してドラッグ&ドロップすれば大丈夫です。
このとき、必ず文字コードを「UTF-16」にしてください。
取り込み時にUTF-16にするのを忘れていたら、あとからファイルのエンコードをUTF-16にする必要があります。


取り込んだら、ローカライズの設定を行います。


まずは対象のファイルを選択して右クリック、「情報を見る」ウィンドウを開きます。
「ファイルをローカライズ可能にする」をクリック。


再度、「情報を見る」ウィンドウを開き直し、
「ローカリゼーションの追加」をクリック。プルダウンからJapaneseを選択。
ドイツ語とかフランス語もプルダウンから簡単に選択できます。
その他の言語の場合は自分で調べる必要がありそうです。


成功するとこんな感じになります。

解除するときは「情報を見る」ウィンドウの一般タブから、
「すべてのローカライズを取り除く」をクリックするだけです。

以上、とても簡単です。
ただしこのローカライズの設定時に文字コードの指定がUTF-16ではなくなることがあるので、
もう一度設定したファイルの文字コードをチェックしておくと安心です。


これで準備は出来たので、あとはLocalizable.stringsを翻訳するだけです。
翻訳したらビルドしてiPhoneにインストールすれば完成!


Localizable.stringsでは翻訳できない箇所、たとえばアプリ名を翻訳するときなど、
Info.plistの内容を翻訳したいときは、InfoPlist.stringsと言うファイルをXcode上で作って、
ローカライズ設定を行い、翻訳してビルドすればうまくいくと思います。


Xibファイルについてもローカライズ設定が可能なようなのですが、
Xib自体を翻訳するより、ソースコード中からラベルやタイトル名を変更するようにして、
ソースコード自体をLocalizable.stringsで翻訳するようにしたほうが変更に強くて楽かと思います。




トラブルシューティング
以上の内容に従っても翻訳されない!というときは、
まずはLocalizable.stringsが本当にUTF-16になっているか確認してみましょう。
InfoPlist.stringsはUTF-16でないと動きません。
Localizable.stringsについてはUTF-8でも動くそうなのですが、私の場合駄目だったので、
やはりUTF-16をオススメしておきます。
ただし、いずれの場合でも、UTF-8とUTF-16が混在すると確実に動きません。

それでも駄目な場合は、Finderでプロジェクトのディレクトリを開き、
buildディレクトリの中身を丸ごと消してから再度ビルドするとうまくいくと思います。
(以前のビルド結果が残って居るとうまくいかないみたいです)


それでは素敵な翻訳ライフを!

2008年12月20日土曜日

NSURLConnection使用中にbad server certificationエラーが出たときの対処法

  • 1203, NSURLErrorDomain, bad server certificate
  • このエラーはSSL使用時に証明書の内容が不正なときに発生する
  • 要するに自己認証証明書(オレオレ証明書)警告
  • 標準APIにはこのオレオレ証明書警告をオフにする機能がない
  • NSURLRequestのallowsAnyHTTPSCertificateForHost:をオーバーライドすることで回避可能

皆さんも、自宅に自分用のサーバーをお持ちだったり、会社のサーバーに業務でアクセスしたりということがあると思います。
こういうちょっとしたサーバーでは、たいていの場合正式な認証局が発行したものではない、自己認証による証明書、
いわゆる「オレオレ証明書」によるSSH認証が行われています。

便利だし、正式な認証局に頼むとお金が必要になったりでついついやってしまいますよね。
ブラウザからアクセスすると警告が出ますが、無視してしまえばなんてことはありません。

ところが。
iPhoneのアプリからこうした「オレオレ証明書」を利用しているサーバーに対して、
NSURLConnectクラスを用いてアクセスしようとすると
1203, NSURLErrorDomain, bad server certificate

というエラー(NSErrorのインスタンス)が発生し、処理が中断されてしまいます。
Appleさんちょっと厳しいですって。
(たとえば、Pythonでliburlを利用してアクセスしたときはたとえオレオレ証明書でも一切怒られません)
しかも困ったことに、このエラーを回避する方法が標準APIに用意されていません。
対策はただひとつ、「オレオレ証明書なんて使うな、Verisignにお金払え」ということらしいです。

オレオレ証明書なんて許さないぞという決意は大変良く分かりますが、
自宅のサーバーならともかく、会社のサーバーでは自分が勝手に証明書取るわけにもいかず。
困りました。

そこで先人たちがNSURLRequestクラスのprivateなメソッドを利用する回避手段を編み出してくださいました。
http://lists.apple.com/archives/Macnetworkprog/2006/Nov/msg00020.html
http://www.phapper.com/Default.aspx?g=posts&m=8


この方法に従って、NSURLConnectionクラス(およびNSURLRequestクラス)を利用する箇所で、以下のようなクラスカテゴリ実装を行います。
@implementation NSURLRequest(NSHTTPURLRequest)
+ (BOOL)allowsAnyHTTPSCertificateForHost:(NSString *)host
{
 return YES; // Or whatever logic
}
@end

これで全てのオレオレ証明書の認証を回避することが出来ます。
もし特定のホストのみを回避したいのであれば、以下のように適当なロジックを組んでやればいいと思います。
@implementation NSURLRequest(NSHTTPURLRequest)
+ (BOOL)allowsAnyHTTPSCertificateForHost:(NSString *)host
{
 return [@"ba-tyan.oreore.com" isEqual:host];
}
@end

iPhoneではAdHocによって100人までは自由にアプリを配布することが出来ますので、
App Storeを利用しない、自分専用・内輪向けのハックアプリなんかをこうして作っても面白いですね。
でも、せっかくiPhoneで開発するならApp Storeに挑戦しなければ損!もちろん、そのうち挑戦しますよ?

2008年12月17日水曜日

CS193P 11日目 非同期処理をやってみる

  • 非同期処理を行う方法はいくつかある
  • URLフェッチ処理ならば、NSURLConnectionクラスをつかっておけば一発
  • さらに簡単にURLフェッチ処理を行いたいのであればこのライブラリをおすすめ
  • URLフェッチ以外の処理を行うならば、NSThreadを使うか、NSOperationとNSOperationQueueを併用する
  • NSThreadは従来どおり、本当にスレッド処理を記述する必要があるため非常に大変
  • 対するNSOperationはインスタンスをつくってキューにぶち込んだら後は勝手にやってくれる、楽
  • UIViewやUIViewControllerに対する処理(要するに画面に対する処理)は、必ずメインスレッドから呼び出す必要がある
  • スレッドセーフではないため
  • 要するに[object performSelectorOnMainThread:withObject:waitUntilDone:modes:]を使えば解決する

いきなり日付が飛んで11日目です。
このあたりからは課題1つにつき3日分ぐらいのの講義内容が含まれていて、難易度がどんどん高くなってきました。
母さん、おいらスタンフォード大学の学生にはなれそうもないよ。



さて、今回の内容は非同期処理です。
現在の課題ではTwitterのタイムラインをJSON形式で取得して表示を行っているのですが、
メインスレッド(プログラムのメインループが走っているスレッド)の上から直接URLに対してHTTPアクセスを行っているため、
処理が返ってくるまでメインスレッドがブロックされ、結果フリーズしたように見えるという問題がありました。
これを非同期処理にしてブロックしないようにしましょうね、と言うのが今回の課題の内容。

NSURLConnectionと言うクラスを使えばURLのフェッチを自動的に非同期で行ってくれるのですが、
ご丁寧に「NSThreadかNSOperationで処理してね」とご忠告が。
Threadはどうにも使いこなせる気がしないので、ここはより簡単なNSOperationを使おうと思います。
(ゲームなどではおそらくNSThreadを使うことになるんだと思いますが)


NSOperationというクラスを継承して、
mainメソッドをオーバーライドして処理を記述し、
NSOperationQueueに追加すると自動的にThreadを裏で立ち上げて並列処理を行ってくれます。
処理が完了したらKVOという機能を使ってNSOperationから通知を受け取るらしいです。
しかしこのKVOと言う概念がイマイチ理解できないので後回しにして、
より簡単なNSInvocationOperationというクラスを使うことにしました。

使い方はこんな感じです。
NSInvocationOperation *op = [[NSInvocationOperation alloc]
     initWithTarget:self
     selector:@selector(reloadPerson:)
     object:person];
 [self.operationQueue addOperation:op];
 [op release];

これだけで自動的に並列処理をしてくれるんだから凄いと思います。
ということで、今回の課題では以下のように並列構成をしてみました。

スレッド1:メインスレッド
スレッド2:TwitterからTimelineを取得するためのスレッド(NSInvocationOperation + NSOperationQueue)
スレッド3:画像を取得するための並列処理(ImageStoreを利用、内部的にはNSURLConnection)

ところがこれがうまくいきません。
1と2だけを並列処理させたときはうまくいき、1と3だけのときもうまくいくのですが、
1と2と3と並列で動かすとエラーになります。
ああもう!だから並列処理なんて嫌いだ!

デバッガで調査してみるとSocketの取得のあたり?でとまっている感じがしたので、
スレッド2かスレッド3がソケットを捕まえてロックしているのではないかと考え、
使ったらすぐreleaseするようにソースを変えてみたのですが、効果なし。

Google先生にご相談したところ、それらしい回答が。
【iPhone】スレッド中で[UITableView reloadData]を使ってはいけない
なるほど!自分のソースを見直すと、確かにスレッド2の処理の中でUITableViewに対してreloadDataを呼び出しています。
さっそくご指摘のあったとおりにソースを書き直してみました。
if ([delegate respondsToSelector:@selector(mPersonDataSourceDidFinishLoadOfPerson:)])
{
 [delegate performSelectorOnMainThread:@selector(mPersonDataSourceDidFinishLoadOfPerson:) withObject:person waitUntilDone:YES];
}

今度は一発で成功!

2008年12月6日土曜日

CS193P Cocoa Programming - ソースコードをgithubに公開いたしました

gitの練習もかねてここまでの成果ソースコードを公開してみました。
http://github.com/akisute/akisute_cs193p/tree/master

特に見所はありませんが、一応自分の勉強用として。

2008年11月30日日曜日

CS193P Cocoa Programming - 7日目 UINavigationViewControllerを使う

  • UIViewControllerのawakeFromNibは動作しないときがある。UIViewControllerの初期化には 必ずviewDidLoadを使うこと
  • UINavigationViewControllerはxibファイルに含めずに、ソースコード中で初期化したり管理する方がうまくいく
  • UINavigationViewControllerの上に表示されるバーを編集したいとき(ボタンを追加したりするとき)は、UINavigationViewControllerにpushされるUIViewControllerのnavigationItemプロパティを編集する。Interface Builderから操作できるかどうかはわからない
  • UIAlertViewがポップアップ表示、UIActionSheetが下からにょきっと出てくるボタンのリストを表示
  • ボタンにソースコード中からアクションを追加することが可能(UIButton, UIBarButtonなどのドキュメントを参照のこと)


さて、ナビゲーションがついてようやくiPhoneアプリらしくなってきました。
画面はナビゲーション時にUIViewControllerのインスタンスがどのように管理されているかをログに吐いてみたところです。viewDidDisappearの後にdeallocが毎回流されているのがわかります。
要するに、一つのviewを毎回毎回alloc->initしないでインスタンスを再利用してやろうと思うときはUINavigtionViewControllerにpushするだけではなくて、どこか別のところでUIViewControllerのインスタンスを保持しておく必要があるようです。2tchの作者さん曰くalloc->initは相当重い動作らしいので、できるかぎり使い回しができるようにしたいです。



ポップアップも出るようになりました。


※いい加減開発ペースと学習ペースをあげようと思っているので、しばらくの間はブログの更新がこんな感じで適当になりそうです。