2008年12月21日日曜日

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月18日木曜日

ようやくiPhoneアプリのパッケージ構成/クラス設計のコツがわかってきました

  • 良いクラス構造を学ぶためには、複数の偉い人のソースを読むのが一番良い

オブジェクト指向言語に限らず、どのようなプログラムを組むにしても、
クラス分割やモジュールの分割は再利用性・保守性を高める上で重要な点だと思います。
もちろん、iPhoneのアプリ開発だってそうです。

Objective-CでCocoa Touchフレームワークを使い始めておよそ1ヶ月、
ようやくこのフレームワークの作法やクラス分割のコツなどがつかめてきました。
まだまだ間違っているところが多数あるような気がしますが、現段階での自分のクラス分割法を晒してみます。

まずはグループ(Javaで言うパッケージ)の分け方から。
私はこんな感じで分けることにしました。ほとんど偉い人のパクりです。
  • Classes
    • Libraries
      各種ライブラリを配置する
      • JSON
      • ImageStore
      • その他自分が作ったアプリのロジックもここに置く
    • Controllers
      View Controllerの類を配置する
      • UIApplicationDelegate
      • UIViewController
    • Views
      カスタムビューを配置する
      • UITableViewCellのサブクラス
      • UIViewのサブクラス

悩ましいのはカスタムビューのライブラリを利用するときなのですが、
一応、Librariesに追加しようと考えています。

自分の書いたロジックはLogicsのようなグループを作成してLibrariesと明確に分けたほうが良いかもしれませんが、
後々自分の書いたロジックも可能な限り使いまわせるようにしたいので、Librariesに入れるようにしています。



クラスの種類を大別すると以下のような感じに。

●View
ビュー。画面。そのまんまですね。

●Controller
UIApplicationDelegateとUIViewControllerがここに属します。データ受け取って画面を書き換えるのがお仕事です。

●Logic
以前はControllerとDataSourceを直接つないでいたのですが、どうしてもうまくいかないので、ControllerとDataSourceの間に1つ層を設けるようにしました。
こいつのお仕事はDataSourceから非同期に受け取ったデータを貯蓄しておき、Controllerにデータを取得するためのインターフェースを提供することです。
データを貯蓄しておけるので、必要なときだけDataSourceからデータを取ってくることができます。
ん?ここまで書いていて思ったけど、LogicというよりLightWeightパターンやProxyパターンに近い?もっといい名前を付けてあげようかな。

●DataSource (Data Access Object, DAO)
実際にデータを取ってくるクラス。HTTPアクセスやらファイルアクセス、SQLiteへのアクセスなどいろいろ手段はありますけど、
いずれの場合でも必ず非同期でデータを取ってくるというところがキモだと思います。

クラス間の結合方法ですが、ControllerとLogic、LogicとDataSourceの間は、Delegateおよびプロパティを使って結んでいます。
Controller -> Logic, Logic -> DataSourceの呼び出しはプロパティからたどってアクセス。
DataSourceは非同期処理を行うので、DataSourceからの返り値はDelegateをたどってControllerまで伝播される仕組みです。
この仕組みは本当に良くできていると思います。非同期処理なのにとても簡単。

現在の課題としては、出来る限り複数のControllerから同一のLogicに対するアクセスを避けるようにしたいのですが、
そのための具体的な方法が良く思いつきません。
Tab barとか作って大規模なアプリになってくると難しそうですねー・・・
delegateだけではなくてCocoaに標準で備わっている通知機能も使っていく必要があるかもしれないと思ってます。