2013年06月25日

iOSアプリ配信(4)iOS Volume Purchase Program (VPP)について

iOS Developer Enterprise Program を使用するような案件だったのですが、2012年10月より Volume Purchase Program (VPP) が日本でも使えるようになったので、試しに申請してみることにしました。結果としてリジェクトされてしまったので途中で頓挫していますが、参考になればと思いアップしてみます。

VPP は Enterprise とくらべて下記のような違いがあります。
・AppStoreのバージョンアップの仕組みが使える
・Appleによる審査がある(普通に販売されるアプリと同レベルの審査です)
・ユーザー側がVPPに登録する必要がある(DUNSナンバーが必要)
・Appleがアプリ料金の30%を徴収する(無料アプリにすれば徴収されないらしい?)

(1)アプリを作る
アプリは AppStore にアップロードするのと同じように作成します。Enterprise と違って Apple による審査がありますので、ガイドラインに従う必要があります。

また、AppStoreのバージョンアップの仕組みが使えるので、自前で作り込む必要はありませんし、WWWサーバを用意する必要もありません。

VPP のアプリが完成したら iTunes Connect から AppStore にアップロードします。情報を入力する画面の中に「Custom B2B App」というチェック項目がありますので、ここをチェックすると VPP アプリとして扱われます。「Custom B2B App」をチェックした場合は、ユーザー企業の VPP アカウントのIDを入力する必要があります。

(2)ユーザーにVPPに登録してもらう

アップル - ビジネス - ビジネス向けのVolume Purchasing Program
http://www.apple.com/jp/business/vpp/

ユーザー企業に VPP に登録してもらいます。VPP自体の費用はかかりませんが、既存の AppleID とは別の AppleID を作成してから VPP に登録する必要があります。また、DUNS ナンバーが必要なので多少手間はかかりますが、難しいことではないと思います。

今回はユーザーさんの VPP 登録が間に合わなかったので、自社で VPP に登録し「Custom B2B App」のユーザーとして自社のIDを指定したら、それが理由でリジェクトされました。AppStore にアップロードするときには、ユーザーさんの VPP 登録が完了している必要があります。

(3)料金設定
上記(2)で、自社のIDを指定してまで VPP にアップロードしたかった理由は、VPP で無料アプリを配信して問題がないかを確認したかったからです。アプリを無料にしてしまうと Apple の取り分がなくなってしまうのに、それでも AppStore の仕組み等が使えるのなら Apple にしては珍しく太っ腹だと思ったからです。

今回の件は、納期が近づいてきたこともあり、iOS Developer Enterprise Program で進めることに決まりましたのでこれ以上は調査しておらず、これ以上のことは分かりません。


posted by はるこち at 10:47| Comment(0) | TrackBack(0) | iOSアプリ開発 | このブログの読者になる | 更新情報をチェックする

iOSアプリ配信(3)iOS Developer Enterprise の注意点

iOS Developer Enterprise はうまく使うと便利ですが、面倒だったり、注意しなければならないこともあります。

(1)アプリのバージョンアップの仕組みを自分で作らなければなりません

エンドユーザーのスキルが高くバージョンアップをメール等で連絡することで皆がSafariからダウンロードページにアクセスしてくれるような恵まれた環境なら良いのですが、アプリからワンタッチでアップデートできるようにしてほしいという要望が出ることも多いと思います。

アプリ起動時に配信サーバに問い合わせて、新しいバージョンがあったら確認メッセージを表示して、アップデートを実行するときはSafariに切り替えて…、という仕組みを作る必要があるかもしれません。端末側に加えて、サーバ側もある程度作り込みが必要になるかもしれません。

(2)1年で期限が切れるので期限管理が大切

iOS Developer Enterprise Program は1年ごとに更新する必要があります。こちらはAppleからのメールを見逃さないようにしていれば、まあ大丈夫かと思います。

見逃しがちなのは、アプリに組み込んだ InHouse プロビジョニングプロファイルも1年で切れるということです。最初のうちは Enterprise Program とほぼ同じ日付なので忘れることはないと思いますが、Enterprise Program とは異なる日付で管理されているのでアップデートのタイミングによっては日付がずれてきます。(早まります)

プロビジョニングプロファイルの期限日の1ヶ月前くらいになると、端末には「プロビジョニングプロファイルはあとXX日で有効期限が切れます」というメッセージが表示されるのですが、普通の人でこのメッセージの意味が分かる人はいないと思われますので、問い合わせが殺到することが予測されます。

また、プロビジョニングプロファイルの有効期限が切れるとアプリ自体が起動しなくなりますので、アプリ内にバージョンアップの仕組みが組み込まれている場合は、Safariからアクセスするインストールページを案内してインストールし直してもらわなければなりません。

プロビジョニングプロファイルを作り直す場合、デベロッパー証明書の期限も近づいていると思いますので、そちらの更新も行なう必要があると思います。デベロッパー証明書を更新するには、Mac のキーチェーンアクセスからCSRを作成してアップロードし、CERファイルをダウンロードしてキーチェーンアクセスに登録します。
※先日デベロッパー証明書を更新したら有効期限が3年後の日付になりましたが、プロビジョニングプロファイルは相変わらず1年後の日付でしたので、更新作業が必要なことは変わらないようです。

最初のうちは、Enterprise Program の支払いが済まないと証明書の更新が行なえないかと思っていたのですが、支払いを済ませる前に更新処理を行なうことができたので、1ヶ月前と言わずもっと早い時期にプロビジョニングプロファイルの更新を行なうことができると思います。

アプリのプロビジョニングプロファイルの更新は通常のアップデートと同じやり方で配信しますので、アプリの修正が無くてもバージョンを上げてアップデート版を配信するようにします。

iOS Developer Enterprise と似たようなもので Volume Purchase Program(VPP) というものが2012年10月より使えるようになりましたので、こちらについては次の記事で紹介します。

(3)エラーがちょっとわかりにくい

OTAインストールでエラーが発生すると端末側にエラーメッセージが表示されますが、何が原因なのか分からないので原因究明に時間がかかることがあります。
otatest-error.png

このメッセージが表示されているということは plist のダウンロードは成功しているということですので、その先で何かエラーが発生していることになります。例えば下記のような可能性があります。
・ipaファイルへのパスが正しくない
・ipaファイルがダウンロードできない
・Largeアイコンがダウンロードできない
・Smallアイコンがダウンロードできない

Largeアイコンは表示されないと思うのですが、ちゃんとダウンロードできないとエラーになるようです。

「再試行」をタップするとダウンロードから再度実行します。3G回線などネットワークが安定していないだけなら再試行でうまくインストールされる可能性はあります。

「再試行」を試してもエラーになってしまう場合は、エラーの原因を突き止める必要があります。「完了」をタップするとインストール処理が(そのまま)中断されますので、青いバーが途中で止まっているアイコンを長押しし、プルプルさせてから×アイコンをタップして削除します。

インストールテストを何度か繰り返していると、アイコンの削除ができなくなってしまうことが稀にあります。このような場合は、スリープボタン長押しで表示される赤い「電源オフ」をスライドさせて、電源を切ってから再起動すると、画面から消えます。

サーバ側のファイルがダウンロードできない場合は、WWWサーバのエラーログを見るとファイルの特定ができると思います。

(4)BASIC認証の入力が面倒

Enterprise Program の場合、第三者が勝手にインストールするのを防ぐ必要がありますが、BASIC 認証を設定した場合、ユーザーIDとパスワードの入力が複数回求められます。
otatest-auth.png

おそらく、ipa ファイルや plist ファイルをダウンロードするたびに認証する必要があるということだと思うのですが、インストールのためのリンクをタップした後、ユーザーIDとパスワードを何度も入力するのはちょっと面倒だと思います。とりあえず我慢して入力しています。
posted by はるこち at 10:23| Comment(11) | TrackBack(0) | iOSアプリ開発 | このブログの読者になる | 更新情報をチェックする

iOSアプリ配信(2)iOS Developer Enterprise の OTA 配信

iOS Developer Enterprise の OTA 配信について紹介します。基本的なやり方は、前の記事で紹介した iOS Developer の OTA 配信とほとんど同じです。使用するプロビジョニングとして InHouse を選択することで、UUID が登録されていない端末でもアプリをインストールすることができるようになります。

また、Enterprise 配信のメリットとして AppStore のような審査を受ける必要がありませんので、かなり自由にアプリを作成することができます。リジェクトされることはありませんが、逆にAppleが審査しない代わり、発生したトラブルは自己責任で解決するということですので、あまり無茶苦茶なことも考えものだと思います。

※注意
iOS Developer Enterprise は、あくまでも「企業内」でのアプリ配信のためのものですので、顧客(他社)など自社以外の端末にインストールできるような状態にした場合は、Apple との規約違反になります。

大まかな手順は以下の通りです。

(1)iOS Developer Enterprise に登録する
以前は従業員500名以上の企業のみという制限がありましたが、今は人数制限は撤廃されました。登録するためには DUNS ナンバーが必要なので個人で登録することはできません。また、既存の AppleID を持っている場合は、別の新しい AppleID を作ってから Enterprise を購入する必要があります。

※自社開発でなく、デベロッパー会社が開発したアプリをユーザー企業が使用するという、今までの請負のような形態で行なう場合は、ユーザー企業が iOS Developer Enterprise に登録し、デベロッパー会社は、ユーザー企業の開発部門という形で開発を行ないます。デベロッパー会社が Enterprise Program に登録していても、それをユーザー企業に提供することはできません。

(2)配布用の InHouse プロビジョニングを作成する
iOS Developer サイトにログインし、Provisioning Profiles → Distribution → InHouse を選択します。

(3)Xcodeでプロジェクトを開いてプロビジョニングを指定し、Product → Archive を実行。
ここから先は通常の iOS Developer の OTA 配信と同じです。

(4)Organizer で Distribution ボタンをクリックし、必要事項を入力。
入力項目については、前の記事「iOSアプリ配信(1)AdHocのOTAインストール」を参照してください。
ipa ファイルと plist ファイルが作られます。

(5)ipa ファイルと plist ファイルを WWW サーバへアップロード
a href のリンクでダウンロードする部分も通常の OTA 配信と同じですが、注意点があります。
iOS Developer Enterprise の配信先はあくまでも「企業内」に限られますので、社外の端末がダウンロードできないように設定しておく必要があります。例えば、VPN等のプライベートセグメントに設置したり、パスワード認証の仕組み等が必要です。

なお詳しい検証はしていないのですが、Enterprise 配信のドキュメントに記載されている内容によると、インストール時に Apple のサーバと通信しているとの記載がありましたので、Enterprise 配信の動向も Apple では把握しているかもしれません。

(6)ダウンロードページのリンクを配信し、インストールしてもらう
Safariからダウンロードページにアクセスしてもらい、リンクをタップしてもらえばインストールが完了します。

簡単ですね。。。。。。と、いいたいところですが、実は結構面倒なこともあります。
その面倒なことについては、次の記事で紹介したいと思います。
posted by はるこち at 09:08| Comment(0) | TrackBack(0) | iOSアプリ開発 | このブログの読者になる | 更新情報をチェックする

2013年06月24日

iOSアプリ配信(1)AdHocのOTAインストール

開発したiOSアプリを実機で動かすのに一番簡単なのは Mac と USB ケーブルで接続してデバッグモードで実行することですが、デバッグの担当者が違っていたり、実機が遠隔地だったりする場合は USB ケーブルでのインストールは不便です。

そこで、OTA(Over the Air)という方法でインストールする方法を紹介します。偉そうに説明していますが、理解が不十分なところも多く間違った説明になっている箇所があるかもしれませんので、その辺りはご承知置きください。

Apple のドキュメントでは WiFi が前提のようですが、3G(LTE)でも大丈夫みたいです。普通にiOSアプリが開発できてUSBケーブルで接続して実行することができる環境であることを前提としますが、手順は下記のような感じです。

必要なもの
・iOSアプリ開発環境
・iOS Developer アカウント
・配信用Webサーバ
・PNG形式のアイコン(512pxと72px)

(1)AdHoc用のプロビジョニングプロファイルを作成する
iOS Developer サイトにログインし、Provisioning Profile → Distribution のページに入って「+」ボタンをクリックすると新しいプロビジョニングプロファイルを作成する画面になりますので、AdHoc を選択します。選択画面はこんな感じです。
provisioning-adhoc.png
必要事項を入力してプロビジョニングプロファイルを作成し、さらにインストールするデバイスのUUIDをプロビジョニングに登録します。

(2)プロビジョニングプロファイルをダウンロードし、登録します。
プロビジョニングプロファイルを手元のMacにダウンロードし、ダブルクリックすると登録されます。

(3)Xcodeで開いているプロジェクトにプロビジョニングプロファイルを設定します。
PROJECT の Build Settings と、TARGETS の Build Settings にそれぞれプロビジョニングプロファイルを選択する箇所がありますので、Release のほうに先ほどダウンロードした AdHoc のプロビジョニングプロファイルを指定します。

(4)スキーマのところで「iOS Device」を選択してから、Product → Archive を実行します。
otatest-schema.png
ここでシミュレータが選択されていると Archive がグレーアウトされます。

(5)Archive が完了すると Organizer が起動し、アプリのアイコンが表示されます。
表示されるまで少々時間がかかることがあります。
otatest-organizer.png

(6)Distribute をクリックします。
ディストリビューションの種類を選択する画面が表示されますので、「Save for Enterprise or Ad-Hoc Deployment」を選択します。
otatest-distribution.png

(7)配布用プロビジョニングプロファイルを選択します
通常はデフォルトで表示されるものをそのまま選択しておけば良いと思いますが、必要であれば選択し直します。
otatest-provisioning.png

(8)配布ファイルの情報を入力します。
otatest-plist.png
入力項目は下記の通りです。
Save As: ファイル名。
 履歴を保存するためにバージョン番号や日付を入れるのも良いと思います。
・保存先フォルダ: ipa と plist が保存されます。
 前回保存時の設定が記憶されるのですが意外に見失うかもしれません。
Save for Enterprise Distribution: にチェックを入れます。Enterpriseの契約がなくても大丈夫です。
 これ以下の入力項目は、ファイル名.plist というファイルに保存されます。
Application URL: ダウンロードURLです。
 iOSデバイスからアクセスできるアドレスを入力します。
Title: アプリのタイトル
 ダウンロード確認画面に表示されます。また、ダウンロード時に一瞬ホーム画面に表示されますが、アプリに設定されているアプリ名に変わります。
Subtitle: サブタイトル
 表示されないようです。バージョン番号等を入れたい場合は Title の方に入れた方が良いかもしれません。
Large Image URL: 512ピクセルアイコンのPNG
 サーバのログを見るとダウンロードされているようですが、表示されているかどうかは不明です。
Small Image URL: 72ピクセルアイコンのPNG
 ダウンロード時に表示されます。iPhone/iPad の両方とも、こちらが表示されました。
Add Shine Effect to Images: 光沢エフェクト
 アイコンに光沢エフェクトがかかります。

(9)ipa ファイルと plist ファイルをWWWサーバにアップロードします。
上の例では、OTATest.ipa と OTATest.plist というファイルが作られているはずですので、そのファイルをWWWサーバにアップロードします。
最低限必要なファイル:
otatest-filelist.png

(10)HTMLファイルを作ります。
ダウンロードのためのリンクは下記のようにします。
<a href="itms-services://?action=download-manifest&url=http://www.example.com/app/otatest/OTATest.plist">OTATest.plist</a>

a href で指定するリンク先のファイルは plist ファイルを指定します。iOS デバイスがplist内に書き込まれている ipa ファイルへのパスを読み出して、ダウンロードします。

(11)iOSデバイスのSafariから上記のHTMLへアクセスします。
リンクをタップするとダウンロードが始まるはずです。
otatest-install.png

制約事項

通常の iOS Developer ではプロビジョニングプロファイルにUUIDが登録されているiOSデバイスでのみ実行することができますので、遠隔地にある iOS デバイスであっても、UUID をプロビジョニングプロファイルに登録しておく必要があります。

登録できる UUID の最大数は100までとなっていますので、それより多くの iOS デバイスにインストールする必要があるときは、iOS Developer Enterprise に切り替える必要があります。

iOS Developer Enterprise の場合、iOS デバイスの台数制限はありませんが、インストールできるiOSデバイスは組織内までとなっていますので、顧客(他社)や不特定多数に対してインストールすることは規約違反となります。(違反するとペナルティが課せられるとの噂もありますので注意が必要です)

iOS Developer Enterprise の場合の方法については次の記事で紹介します。
posted by はるこち at 17:07| Comment(0) | TrackBack(0) | iOSアプリ開発 | このブログの読者になる | 更新情報をチェックする

2013年05月01日

Xcode エラー: Could not read from the device.

iOS端末をUSBで接続してデバッグしているときに、急にエラーメッセージが表示され実行できなくなってしまうことがあります。
スクリーンショット
Could not read from the device.

スクリーンショット
Xcode cannot run using the selected device.
No provisioned iOS devices are available with a compatible iOS version.
Connect an iOS device with a recent enough version of iOS to run your application or choose an iOS simulator as the destination.

原因はわかりませんが、Xcodeをいったん終了して起動し直すと、これまでと同様に起動できるようになります。
posted by はるこち at 10:23| Comment(0) | TrackBack(0) | iOSアプリ開発 | このブログの読者になる | 更新情報をチェックする

error: PCH file built from a different branch ((clang-425.0.24)) than the compiler ((clang-425.0.28))

ある開発中のアプリのソースを1ヶ月ぶりに開いてコンパイルしたら、変なエラーメッセージが表示されました。

error: PCH file built from a different branch ((clang-425.0.24)) than the compiler ((clang-425.0.28))
1 error generated.


何を言っているのかさっぱり分かりませんが、調べてみたところ、メニューから Clean build folder というコマンドを実行すれば良いということが分かりました。

通常、Xcode の Product メニューを開くと Clean というコマンドがありますが、キーボードの Option キーを押しながら Product メニューを開くと Clean build folder に変わっていますので、それを実行します。

その後、コンパイルしたら無事に終了しました。
posted by はるこち at 10:17| Comment(0) | TrackBack(0) | iOSアプリ開発 | このブログの読者になる | 更新情報をチェックする

Xcodeのワーニング「Incomplete Implementation」を解決するには

Xcodeで開発していて Incomplete implementation というワーニングが表示されることがあります。エラーではないので実行すると起動できることがありますが、しばらく操作しているとアプリが落ちることがあります。

このメッセージは、「@interface で宣言されているけど @implementation にないよ」という意味です。メソッドを作ったはずなのにワーニングが消えない、というときは、名前の微妙な間違い、特に大文字/小文字が違っていないか、引数の型が違っていないか、という点に注意して調べます。

今まで気がつかなかったのですが、簡単に調べる方法があります。ワーニングメッセージが表示されている黄色いアイコンの左側にある三角形をクリックすると詳細情報が表示されます。これを見ると、何が見つからないのかわかります。ここを見れば簡単です。
スクリーンショット
ラベル:Xcode
posted by はるこち at 10:06| Comment(0) | TrackBack(0) | iOSアプリ開発 | このブログの読者になる | 更新情報をチェックする

2012年11月21日

iOSアプリの状態遷移について

先日の記事(Application does not run in backgroundについて)の内容を図にしました。

Application does not run in background を YES にすると、大まかに言って2つの違いがあるようです。

(1)閉じるとバックグラウンドに入らずTerminateされる(5本指ジェスチャーで閉じるのも同じ)
バックグラウンドでの実行を禁止しているので、想像通りの挙動です。

(2)ロック状態になると非アクティブになるだけ
この点について把握していなかったので今回苦労してしまいました。
ホームボタンを押したらアプリが終了するという挙動にしたかったので、Application does not run in background を YES にしていましたが、そのためスリープボタンを押してロックされたことの検出ができなくなっていました。

Application does not run in background が NO(標準設定)の場合は、ロックされるとバックグラウンドに入り(applicationDidEnterBackgroundが呼ばれ)、しかも[UIApplication sharedApplication].applicationState が UIApplicationStateInactive になります。

しかし、YES になっているとバックグラウンドに入らないため applicationDidEnterBackground が呼ばれず、applicationWillResignActive が呼ばれるだけです。そのため、ホームボタンダブルクリックのタスク切り替えと見分けがつきません。

このあたりについて調べた結果を、図にしました。
左側が Application does not run in background = NO(標準設定)の場合、右側が YES の場合です。

起動
20121121-174703-3309.png

ホームボタンを押してバックグラウンドに入れる
20121121-174708-3310.png

アイコンをタップ(再起動)
20121121-174713-3311.png

ホームをダブルクリックしてタスク選択を表示
20121121-174720-3312.png

タスク選択から復帰
20121121-174725-3313.png

電源ボタンを押してロック
20121121-174734-3314.png

ロックから復帰
20121121-174741-3315.png

5本指で閉じる
20121121-174749-3316.png
posted by はるこち at 18:01| Comment(0) | TrackBack(0) | iOSアプリ開発 | このブログの読者になる | 更新情報をチェックする

2012年11月12日

Application does not run in backgroundについて

「ホームボタンを押してアプリを閉じたら再度ログイン画面から始まるようにしてほしい」
という要望があったので、プロジェクト設定の Application does not run in background を YES にしてアプリを作成しました。アプリ内でログイン画面に戻る遷移を作成するのが難しかったので、バックグラウンド動作をさせずにアプリを終了させることで、次回は必ずログイン画面が表示されることを期待したわけです。

ところが、電源ボタンを押してスリープモードに入った後、スリープを解除したときログイン画面が表示されず、前回の画面からそのまま使用できることがわかりました。電源ボタンを押したり、自動スリープ機能によってスリープモードに入るときは ApplicationWillResignActive が呼ばれることが分かりましたので、ここに exit(1); を入れて終了するようにしてみました。

※このアプリはAppStoreを通さずに、エンタープライズで配信されるアプリなのでAppleによる審査を受けないことになっています。AppStoreに登録するアプリでは状況が異なると思いますのでご注意ください。

すると、電源ボタンを押してスリープに入ったときはちゃんと終了されるようになりましたが、ホームボタンをダブルクリックでタスク切り替え画面を表示しようとするとアプリ画面がパッと消えてしまったり、5本指でアプリを閉じるときにアニメーションせずにパッと消えてしまうようになってしまいました。

そこで、スリープ時や終了時に呼び出される delegate を調べてみました。

[1]Application does not run in background = NO のとき(標準設定)

起動
didFinishLaunchingWithOptions → applicationDidBecomeActive

ホームボタンを押してバックグラウンドに入れる
applicationWillResignActive → applicationDidEnterBackground(2)

アイコンをタップ(再起動)
applicationWillEnterForeground → applicationDidBecomeActive

ホームダブルクリック
applicationWillResignActive

タスク選択から復帰
applicationDidBecomeActive

電源ボタンを押してロック
applicationWillResignActive → applicationDidEnterBackground(1)

ロックから復帰
applicationWillEnterForeground → applicationDidBecomeActive

5本指で閉じる
applicationWillResignActive → applicationDidEnterBackground

[2]Application does not run in background = YES の場合

起動
didFinishLaunchingWithOptions → applicationDidBecoemActive

ホームボタンを押してバックグラウンドに入れる
applicationDidEnterBackground(2) → applicationWillTerminate

アイコンをタップ(再起動)
didFinishLaunchingWithOptions → applicationDidBecomeActive

ホームボタンをダブルクリック
applicationWillResignAcgive

タスク選択から復帰
applicationDidBecomeActive

電源ボタンを押してロック
applicationWillResignActive

ロックから復帰
applicationDidBecomeActive

5本指で閉じる
applicationWillResignActive → applicationDidEnterBackground(2) → applicationWillTerminate
posted by はるこち at 13:14| Comment(0) | TrackBack(0) | iOSアプリ開発 | このブログの読者になる | 更新情報をチェックする

2012年10月11日

iOS6のCFStringTransform()がおかしい件

ひらがなを半角カタカナに変換する処理と、半角英数字を全角英数字に変換する処理を持つアプリがインストールされていたiPadをiOS6.0にバージョンアップしたところ、この2つの処理がうまく動かなくなりました。

結論から言うと簡単な回避策はなく、iOSのバージョンアップを待てない場合は自前で変換処理を作成する必要があると思います。

今回問題となったアプリでは、下記の2種類の処理を CFStringTransform() を使って作成しています。

(1)入力されたひらがな半角カタカナに変換する処理
(2)入力された半角英数字を全角英数字に変換する処理

ちなみに(1)の半角カタカナにする処理では、途中でいったん全角カタカナに変換するので CFStringTransform() を2段階かけています。

CFStringTransform() についてググってみると、どうもiOS6.0SDKのバグっぽい感じがしました。
iOS6.0SDKのCFStringTransform()

ユーザーからのレポートでは「同じ操作を2回繰り返すと処理される」というような話がありましたので、2回繰り返すようにしたところ、たしかに(1)の半角カタカナに変換する処理はどうにか動くようになりました。1回ではダメで2回やるとうまくというところが、バグっぽいです。

修正前
NSMutableString *convertedString = [[textArea text] mutableCopy];
CFStringTransform((__bridge CFMutableStringRef)convertedString, NULL, kCFStringTransformHiraganaKatakana, false);
CFStringTransform((__bridge CFMutableStringRef)convertedString, NULL, kCFStringTransformFullwidthHalfwidth, false);

修正後
NSMutableString *convertedString = [[textArea text] mutableCopy];
CFStringTransform((__bridge CFMutableStringRef)convertedString, NULL, kCFStringTransformHiraganaKatakana, false);
CFStringTransform((__bridge CFMutableStringRef)convertedString, NULL, kCFStringTransformFullwidthHalfwidth, false);
NSMutableString *convertedString2 = [[textArea text] mutableCopy];
CFStringTransform((__bridge CFMutableStringRef)convertedString2, NULL, kCFStringTransformHiraganaKatakana, false);
CFStringTransform((__bridge CFMutableStringRef)convertedString2, NULL, kCFStringTransformFullwidthHalfwidth, false);

ただし、この同じ処理を2回繰り返すというやり方では、(2)の半角文字列を全角にするという処理ではうまくいきませんでした。(1回目はうまくいくが2回目以降は逆に半角に変換されてしまう)。

そこで、NSStringクラスに全角文字列変換を行う処理をカテゴリで追加することにしました。変なところにiOSのバグが潜んでいると困るので、文字をひとつひとつチェックして差し替えるというベタな方法で記述しました。

下記にソースコードを掲載しますが限られた時間の中で作成したものであり、動作について細かくチェックしてある訳ではないので、参考程度にしていただければと思います。またこのサンプルでは「ガギグゲゴ」などの濁音、半濁音については特に考慮してありませんので、用途によってはうまく修正していただく必要があります。

//
// NSString+Zenkaku.h
// Proto01
//
// Created by 竹下 治彦 on 12/10/11.
//
//

#import

// NSStringクラスを拡張し全角文字列に変換して返す機能をカテゴリとして追加
@interface NSString(Zenkaku)
- (NSString *)mojiToZenkaku:(NSString *)moji;
- (NSString *)convertToZenkaku;
@end


//
// NSString+Zenkaku.m
// Proto01
//
// Created by 竹下 治彦 on 12/10/11.
//
//

#import "NSString+Zenkaku.h"

@implementation NSString(Zenkaku)

//渡された文字に対応する全角文字を返す
- (NSString *)mojiToZenkaku:(NSString *)moji
{
if([moji isEqualToString:@" "]) return @" ";
if([moji isEqualToString:@"!"]) return @"!";
if([moji isEqualToString:@"\""]) return @"”";
if([moji isEqualToString:@"#"]) return @"#";
if([moji isEqualToString:@"$"]) return @"$";
if([moji isEqualToString:@"%"]) return @"%";
if([moji isEqualToString:@"&"]) return @"&";
if([moji isEqualToString:@"'"]) return @"’";
if([moji isEqualToString:@"("]) return @"(";
if([moji isEqualToString:@")"]) return @")";
if([moji isEqualToString:@"*"]) return @"*";
if([moji isEqualToString:@"+"]) return @"+";
if([moji isEqualToString:@","]) return @",";
if([moji isEqualToString:@"-"]) return @"−";
if([moji isEqualToString:@"."]) return @".";
if([moji isEqualToString:@"/"]) return @"/";
if([moji isEqualToString:@"0"]) return @"0";
if([moji isEqualToString:@"1"]) return @"1";
if([moji isEqualToString:@"2"]) return @"2";
if([moji isEqualToString:@"3"]) return @"3";
if([moji isEqualToString:@"4"]) return @"4";
if([moji isEqualToString:@"5"]) return @"5";
if([moji isEqualToString:@"6"]) return @"6";
if([moji isEqualToString:@"7"]) return @"7";
if([moji isEqualToString:@"8"]) return @"8";
if([moji isEqualToString:@"9"]) return @"9";
if([moji isEqualToString:@":"]) return @":";
if([moji isEqualToString:@";"]) return @";";
if([moji isEqualToString:@"<"]) return @"<";
if([moji isEqualToString:@"="]) return @"=";
if([moji isEqualToString:@">"]) return @">";
if([moji isEqualToString:@"?"]) return @"?";
if([moji isEqualToString:@"@"]) return @"@";
if([moji isEqualToString:@"A"]) return @"A";
if([moji isEqualToString:@"B"]) return @"B";
if([moji isEqualToString:@"C"]) return @"C";
if([moji isEqualToString:@"D"]) return @"D";
if([moji isEqualToString:@"E"]) return @"E";
if([moji isEqualToString:@"F"]) return @"F";
if([moji isEqualToString:@"G"]) return @"G";
if([moji isEqualToString:@"H"]) return @"H";
if([moji isEqualToString:@"I"]) return @"I";
if([moji isEqualToString:@"J"]) return @"J";
if([moji isEqualToString:@"K"]) return @"K";
if([moji isEqualToString:@"L"]) return @"L";
if([moji isEqualToString:@"M"]) return @"M";
if([moji isEqualToString:@"N"]) return @"N";
if([moji isEqualToString:@"O"]) return @"O";
if([moji isEqualToString:@"P"]) return @"P";
if([moji isEqualToString:@"Q"]) return @"Q";
if([moji isEqualToString:@"R"]) return @"R";
if([moji isEqualToString:@"S"]) return @"S";
if([moji isEqualToString:@"T"]) return @"T";
if([moji isEqualToString:@"U"]) return @"U";
if([moji isEqualToString:@"V"]) return @"V";
if([moji isEqualToString:@"W"]) return @"W";
if([moji isEqualToString:@"X"]) return @"X";
if([moji isEqualToString:@"Y"]) return @"Y";
if([moji isEqualToString:@"Z"]) return @"Z";
if([moji isEqualToString:@"["]) return @"[";
if([moji isEqualToString:@"\\"]) return @"¥";
if([moji isEqualToString:@"]"]) return @"]";
if([moji isEqualToString:@"^"]) return @"^";
if([moji isEqualToString:@"_"]) return @"_";
if([moji isEqualToString:@"`"]) return @"‘";
if([moji isEqualToString:@"a"]) return @"a";
if([moji isEqualToString:@"b"]) return @"b";
if([moji isEqualToString:@"c"]) return @"c";
if([moji isEqualToString:@"d"]) return @"d";
if([moji isEqualToString:@"e"]) return @"e";
if([moji isEqualToString:@"f"]) return @"f";
if([moji isEqualToString:@"g"]) return @"g";
if([moji isEqualToString:@"h"]) return @"h";
if([moji isEqualToString:@"i"]) return @"i";
if([moji isEqualToString:@"j"]) return @"j";
if([moji isEqualToString:@"k"]) return @"k";
if([moji isEqualToString:@"l"]) return @"l";
if([moji isEqualToString:@"m"]) return @"m";
if([moji isEqualToString:@"n"]) return @"n";
if([moji isEqualToString:@"o"]) return @"o";
if([moji isEqualToString:@"p"]) return @"p";
if([moji isEqualToString:@"q"]) return @"q";
if([moji isEqualToString:@"r"]) return @"r";
if([moji isEqualToString:@"s"]) return @"s";
if([moji isEqualToString:@"t"]) return @"t";
if([moji isEqualToString:@"u"]) return @"u";
if([moji isEqualToString:@"v"]) return @"v";
if([moji isEqualToString:@"w"]) return @"w";
if([moji isEqualToString:@"x"]) return @"x";
if([moji isEqualToString:@"y"]) return @"y";
if([moji isEqualToString:@"z"]) return @"z";
if([moji isEqualToString:@"{"]) return @"{";
if([moji isEqualToString:@"|"]) return @"|";
if([moji isEqualToString:@"}"]) return @"}";
if([moji isEqualToString:@"~"]) return @" ̄";
if([moji isEqualToString:@"。"]) return @"。";
if([moji isEqualToString:@"「"]) return @"「";
if([moji isEqualToString:@"」"]) return @"」";
if([moji isEqualToString:@"、"]) return @"、";
if([moji isEqualToString:@"・"]) return @"・";
if([moji isEqualToString:@"ヲ"]) return @"ヲ";
if([moji isEqualToString:@"ァ"]) return @"ァ";
if([moji isEqualToString:@"ィ"]) return @"ィ";
if([moji isEqualToString:@"ゥ"]) return @"ゥ";
if([moji isEqualToString:@"ェ"]) return @"ェ";
if([moji isEqualToString:@"ォ"]) return @"ォ";
if([moji isEqualToString:@"ャ"]) return @"ャ";
if([moji isEqualToString:@"ュ"]) return @"ュ";
if([moji isEqualToString:@"ョ"]) return @"ョ";
if([moji isEqualToString:@"ッ"]) return @"ッ";
if([moji isEqualToString:@"ー"]) return @"ー";
if([moji isEqualToString:@"ア"]) return @"ア";
if([moji isEqualToString:@"イ"]) return @"イ";
if([moji isEqualToString:@"ウ"]) return @"ウ";
if([moji isEqualToString:@"エ"]) return @"エ";
if([moji isEqualToString:@"オ"]) return @"オ";
if([moji isEqualToString:@"カ"]) return @"カ";
if([moji isEqualToString:@"キ"]) return @"キ";
if([moji isEqualToString:@"ク"]) return @"ク";
if([moji isEqualToString:@"ケ"]) return @"ケ";
if([moji isEqualToString:@"コ"]) return @"コ";
if([moji isEqualToString:@"サ"]) return @"サ";
if([moji isEqualToString:@"シ"]) return @"シ";
if([moji isEqualToString:@"ス"]) return @"ス";
if([moji isEqualToString:@"セ"]) return @"セ";
if([moji isEqualToString:@"ソ"]) return @"ソ";
if([moji isEqualToString:@"タ"]) return @"タ";
if([moji isEqualToString:@"チ"]) return @"チ";
if([moji isEqualToString:@"ツ"]) return @"ツ";
if([moji isEqualToString:@"テ"]) return @"テ";
if([moji isEqualToString:@"ト"]) return @"ト";
if([moji isEqualToString:@"ナ"]) return @"ナ";
if([moji isEqualToString:@"ニ"]) return @"ニ";
if([moji isEqualToString:@"ヌ"]) return @"ヌ";
if([moji isEqualToString:@"ネ"]) return @"ネ";
if([moji isEqualToString:@"ノ"]) return @"ノ";
if([moji isEqualToString:@"ハ"]) return @"ハ";
if([moji isEqualToString:@"ヒ"]) return @"ヒ";
if([moji isEqualToString:@"フ"]) return @"フ";
if([moji isEqualToString:@"ヘ"]) return @"ヘ";
if([moji isEqualToString:@"ホ"]) return @"ホ";
if([moji isEqualToString:@"マ"]) return @"マ";
if([moji isEqualToString:@"ミ"]) return @"ミ";
if([moji isEqualToString:@"ム"]) return @"ム";
if([moji isEqualToString:@"メ"]) return @"メ";
if([moji isEqualToString:@"モ"]) return @"モ";
if([moji isEqualToString:@"ヤ"]) return @"ヤ";
if([moji isEqualToString:@"ユ"]) return @"ユ";
if([moji isEqualToString:@"ヨ"]) return @"ヨ";
if([moji isEqualToString:@"ラ"]) return @"ラ";
if([moji isEqualToString:@"リ"]) return @"リ";
if([moji isEqualToString:@"ル"]) return @"ル";
if([moji isEqualToString:@"レ"]) return @"レ";
if([moji isEqualToString:@"ロ"]) return @"ロ";
if([moji isEqualToString:@"ワ"]) return @"ワ";
if([moji isEqualToString:@"ン"]) return @"ン";
if([moji isEqualToString:@"゙"]) return @"゛";
if([moji isEqualToString:@"゚"]) return @"゜";
//対象文字にひっかからなかったら、渡された文字をそのまま返す
return moji;
}

//渡された文字列中の半角文字列を全角に置換して返す
- (NSString *)convertToZenkaku
{
NSMutableString *target = [self mutableCopy];
for(int p = 0; p < [target length]; p++){
NSRange range = NSMakeRange(p, 1);
NSString *moji = [target substringWithRange:range];
[target replaceCharactersInRange:NSMakeRange(p, 1) withString:[self mojiToZenkaku:moji]];
}
return target;
}

@end

posted by はるこち at 13:34| Comment(0) | TrackBack(0) | iOSアプリ開発 | このブログの読者になる | 更新情報をチェックする

2012年10月03日

これはヤバい!setDateFormat:@"YYYY/MM/dd" は誤り

iPhoneアプリで「2012/12/31」と入力してるのに、「2013/12/31」になっちゃうんですけど!!
という問い合わせを受けて、調べました。
ソースコードは、こんな感じ。
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"YYYY/MM/dd"];
NSDate *inputDate = [formatter dateFromString:[textBox text]];
NSString *dateStr = [formatter stringFromDate:inputDate];


NSDate型に変換するところは問題ないのに、NSString型に変換するときに、1年後の日付になっているようです。

NSDateFormatterに指定する書式文字列に関しては資料が少ないのですが、このページ書式文字列の表へのリンクがありました。

YYYYは下記のように書かれていて、カレンダーの年と違った値になることがあると書かれています。
It's because the format string YYYY represent the "Year (of "Week of Year"), used in ISO year-week calendar. May differ from calendar year."
YYYY ではなく yyyy を使えということです。

どのような日付が影響を受けるのか、2012年から2014年までの3年分を調べてみました。
yyyy/MM/dd YYYY/MM/dd
2012/12/30 2013/12/30
2012/12/31 2013/12/31
2013/12/29 2014/12/29
2013/12/30 2014/12/30
2013/12/31 2014/12/31
2014/12/28 2015/12/28
2014/12/29 2015/12/29
2014/12/30 2015/12/30
2014/12/31 2015/12/31

おもに年末の数日間が影響を受けるようです。

YYYY/MM/dd でもほとんどの場合は問題なく動作するので気がつきにくいのですが、みなさんも注意した方が良いと思います。

posted by はるこち at 09:49| Comment(0) | TrackBack(0) | iOSアプリ開発 | このブログの読者になる | 更新情報をチェックする

2012年06月18日

iOS Enterprise Developerの更新

iOS Enterprise Developer Program で配布しているアプリのユーザーから問い合わせがありました。
skitched-20120618-104052.png
有効期限が近づいているとのことで、このあたりについて調べてみました。

まず、手元のiPadの時計を進めて期限を過ぎた状態にしてみたところ、アプリは起動しませんでした。
skitched-20120618-105116.png

アプリはOTA(Over the Air)で配布することになっているのですが、再インストールしようとしてダウンロードを行ったところエラーメッセージが表示されました。
skitched-20120618-105153.png

新規ダウンロードもできなくなっているようです。少し調べたところ、Appleのドキュメントにたどり着きました。FA_Wireless_Enterprise_App_Distribution.pdf というファイルのP10に記載がありました。

配信証明書の期限が切れると、アプリケーションは実行できません。現在、配信証明書の有効期限は1年です。証明書の期限が切れる数週間前に、iOS Dev Centerから新しい配信証明書を請求してください。その配信証明書を使用して新たな配信プロビジョニングプロファイルを作成し、アップデートされたアプリケーションを再コンパイルしてユーザに配布します。


エンタープライズでも証明書の期限が切れると実行できなくなり、期限が切れる前にアプリケーションをアップデートする必要があるということのようです。Appleとの契約を更新するだけで済むと思っていたのですが、毎回アプリのアップデートを行わなければならないとすると、結構めんどうな感じです。

posted by はるこち at 10:56| Comment(0) | TrackBack(0) | iOSアプリ開発 | このブログの読者になる | 更新情報をチェックする

2012年05月28日

receiver type "hoge" for instance message does not declare a method with selector "func:"

Xcode4.3 で開発していたソースコード一式を、Xcode4.2 の環境にコピーしてコンパイルしたら、エラーメッセージが大量に発生しました。

receiver type "hoge" for instance message does not declare a method with selector "func:"


Xcode4.3 では、同じ .m ファイル内で参照する場合は .h に宣言しておかなくてもコンパイルが通ってしまうのですが、Xcode4.2 では厳密に書いておかないとだめらしいです。

今回問題になったソースコードでは、xxxencode と、xxxdecode の2つを使用していて xxxencode のほうだけヘッダファイルに宣言があるという状況だったため、混同してしまい発見が遅れました。

ビルドしてエラーメッセージを確認して修正、またビルドして確認して修正、という作業を何回か繰り返して、起動するようになりました。

Xcode4.2 と Xcode4.3 が混在しているチームで開発作業を進めるときは、.h に宣言をもれなく書いておくようにする必要があるというわけです。

posted by はるこち at 11:05| Comment(0) | TrackBack(0) | iOSアプリ開発 | このブログの読者になる | 更新情報をチェックする

×

この広告は180日以上新しい記事の投稿がないブログに表示されております。