Posted by Kentaro Ishizaki on 2011年1月3日
CGContextSetShadowWithColor の offset の Y軸指定は、iOS 3.2から向きが変わっています。
iOS 3.0機でテストしていたら、影が上下逆に表示されていて、いろいろ調べたところ、APIレベルで向きが変わっていた、というオチでした…。
Appleのドキュメントによると、
Note: Prior to iOS 3.2, Core Graphics and UIKit shared the same convention for shadow direction: positive offset values make the shadow go down and to the right of an object.
とのこと。
Posted by Kentaro Ishizaki on 2010年12月20日
SKProductsRequest で指定した productIdentifier が invalidProductIdentifiers になって返ってくるときは、以下の点を確認してください。(ここを参考にしました)
- そのアプリのApp IDでIn-App Purchasesは有効になっていますか?
- そのプロダクトは「Cleared for Sale」になっていますか?
- そのアプリの新しいバージョンをiTunes Connect上で追加していますか?
参考にしたページでは、 Have you submitted (and optionally rejected) your application binary?
- Xcodeプロジェクトの.plistのBundle IDはApp IDと一致していますか?
- そのApp IDで新しいProvisioning Profileを作成してインストールしていますか?
- アプリがそのProvisioning Profileでコードサインがされるように設定されていますか?
- iOS 3.0以降が対象になるようにビルドされていますか?
- SKProductRequestに渡しているProduct IDはiTunes Connectで指定したものと一致していますか?
- iTunes Connectでプロダクトを追加してから数時間待ってみましたか?
- iTunes Connectに正しく銀行口座情報が登録されていますか?
- アプリをいったんデバイスから削除して再インストールしてみましたか?
この最後のはできるだけしたくないので避けていたのですが、これをしたら正常に通りました。Sandbox環境で実行されているかということなんですね。XcodeのオーガナイザのデバイスのApplicationsのところで該当アプリがグレイアウトされていたら、それはSandbox環境で実行されて“いない”ということです。その場合はいったんデバイス上でアプリを削除し、あらためてXcodeからインストールし直す必要があります。
App Storeから自分のアプリを試しにダウンロードしていたりすると、Sandbox環境を外れてしまうようです。
Posted by Kentaro Ishizaki on 2010年12月3日

拙作のToToDoは、無料アプリケーションとして公開し、広告で収入を得るというモデルを採用しています。表示される広告は、iAdとAdMobです。公開から約2ヶ月が経過して、ダウンロード数は50,000にも達しましたが、結論から言えば、現状では生活してゆくに足るだけの広告収入は得られていません。
iAdについて
iAdは現時点では日々の収益は1ドル弱ぐらいが平均で、トータルでも50ドル強です。iAdは現時点で以下のような状況です。
| リクエスト |
2,691,127 |
| インプレッション |
9,839 |
| Fill Rate |
0.37% |
| CTR(クリック率) |
0.45% |
| eCPM(1000インプレッションあたりの収益) |
$5.39 |
ToToDoはアメリカとイギリスではあまり人気がないため、iAdのインプレッションはトータルでも10,000未満で、評価はしにくい感じです。人気がないのは、文化的相違なのか、ローカライズがうまくいっていないのか、理由はよく分かりません…。
iAdのFill Rateは,アメリカで50.86%、イギリスで29.68%、と決して高くありません。クリック率も、アメリカで0.46%、イギリスでは0.1%、とこちらもかなり悪いです。広告のバリエーションが少ないのではないかと予測しています。
ただし、iAdはクリックされると収益が一気に伸びます。クリック単価が高いようです。クリック単価は1ドル程度の印象です。iAdはインプレッションでも収益があがりますが、さすがにこちらの方は単価が低いようです。
以下のグラフは、ダウンロード数の遷移とiAdのeCPM(1000インプレッションあたりの収益)のグラフです。突如eCPMが20ドル以上になったりしているのは、やはりクリック単価の関係のように思います。ダウンロード数との相関関係は、必ずしも一致しないところもありますが、やはり新規ユーザーはクリック率が高く、その後クリック率は下がっていくと考えてよさそうです。

平均eCPM(1000インプレッションあたりの収益)は$5.39なので、この点はAdMob(eCPM: $0.56)に比べてかなり好成績です。インプレッションがこちらの期待通りに上がっていけば、収益はかなり改善するはずです。日本ではトータル200万リクエストなので、半分の100万インプレッションならば5,000ドルという計算になります。これならば生活できなくはありません(^^;。とはいえ、時間が経つにつれてじりじりとeCPMが下がってきているのも事実で、見極めは難しいですね。
12月からフランスで、1月からはドイツで、来年からは日本でiAdがスタートする予定となっています。そうしたら評価に足るだけの数字がそろうものと思います。そのときにあらためてレポートします。
AdMobについて
AdMobはスマートフォンアプリ広告では大手で、世界中で広告を配信していますので、選択肢としては良いと思います。ただ、実際に自分のアプリに表示される広告を見ていると、ほとんどがiPhoneアプリの広告で、iAdが大手企業の広告が表示される(こともある?)ことを考えると、広告内容としてはいまいちなイメージです。
AdMobの状況は現時点では以下のようになっています。
| リクエスト |
1,271,711 |
| インプレッション |
1,072,644 |
| Fill Rate |
84.35% |
| CTR(クリック率) |
0.77% |
| eCPM(1000インプレッションあたりの収益) |
$0.56 |
iAdと比べるとFill Rateの高さが目を引きます。つまりそれだけリクエストに対して表示する広告があるということです。クリック率もiAdよりは高いです。そのかわりeCPMはかなり低いです。これはたくさん表示されても収益がなかなか増えないことを意味します。一日100ドルを目指すのであれば、Fill Rateも考慮に入れて、一日あたり21万リクエスト以上が必要な計算となります。ToToDoではあと7倍程度のユーザー数を確保しなければこの数字は達成できません。数百万ダウンロードを達成しているアプリであれば、このeCPMでもかなりの収入になることは分かります。
eCPMとダウンロード数の関係も見てみましょう。

ダウンロード数と完全に連動しているのが見て取れます。基本的に広告というのは継続的に使用しているユーザーはクリックしなくなっていく傾向があると思います。現在ではeCPMは0.3~0.4ドル程度であり、先ほどの計算からするともっと多くのユーザーが必要ということになります。
ダウンロード数とリクエスト数とクリック率の関係も見てみましょう。

リクエスト数の増加に対してクリック率は下がっていっています。継続的に使うようになるとクリック率は下がってくるということだと思います。
まとめ
- 同一ユーザーの広告収益は下がっていくため、一定の広告収入を維持するためには新規ダウンロードユーザーの確保が必要
- iAdはeCPMが高いので、アメリカでウケるアプリを作るか、iAdが世界展開されるようになれば期待できる
- AdMobはFill Rateは高いがeCPMが低いため、生活するためには、最低一日あたり20万リクエストを確保できるアプリを作るか、数万のリクエストがあるアプリを複数リリースする必要がある
Posted by Kentaro Ishizaki on 2010年12月3日
せっかくアプリをダウンロードしても、使ってもらわなければ意味がありません。僕もiTunesには登録されていても使っていないアプリはたくさんあります。がんばって作ったアプリがそういうアプリの一つになってしまうのは悲しいことです。また、拙作ToToDoのように広告モデルを採用しているアプリは、使ってもらうことが収益につながります。
自分の作ったアプリが実際どの程度利用されているのかは、広告モデルアプリの場合、広告システムへのリクエスト数である程度はかることができます。以下のグラフはToToDoのダウンロード数と広告リクエスト数の遷移のグラフです。ToToDoの初期バージョンは、最初にiAdの広告をリクエストするため、iAdリクエスト数がアプリの起動回数とほぼ同じであると考えることができます。

特にiAdリクエスト数の後半の遷移に注目すると、ダウンロード数が少ないと、一定の割合で利用ユーザー数が減ってゆきます(TODOアプリという性質上、週末や祝日にはリクエストが減ります)。新しいアプリが次々と登場するので、使い続けてもらうためには継続的な改良が必要と思われます。
ダウンロード数そのものを増やすにはどうしたらいいのか、それが分かったら教えてほしいぐらいですが、印象としては、ランキングがすべてです。ランキングが上位に来れば来るほど目に留まりやすいためダウンロード数は増えてゆきます。ここにジレンマが生じます。ダウンロード数を増やすためにはランキングをアップさせる必要がありますが、ランキングをアップさせるためにはダウンロード数を増やす必要があります。個人的には、その最初のきっかけを作ってくれるのがAppBankなどのレビューサイトであるという認識でいます。
レビューサイトの効果
グラフには、いくつかのレビューサイトに掲載されたタイミングを表示してあります。
認識している範囲では、最初に取り上げてくださったメジャーレビューサイトはiPhone女史さんでした。うれしかったです。
AppBankの爆発的な訴求力についてのうわさは聞きますが、ToToDoに関して言えば、AppBankに紹介される前からダウンロード数は増えていることが分かります。むしろmeet-iに紹介された後に爆発的に増えています。AppBankにはそれをもう少し持続させていただいたという印象で、爆発的な訴求力とまでは感じませんでした。毎日いくつものアプリのレビューが載せられているわけですから、過剰な期待はできないですね。
持続的に利用してもらうために
自分自身もそうですが、いったんお蔵入りアプリとなってしまうと、そのアプリが日の目を見ることはめったにありません。なので、最初にダウンロードしてもらったときが勝負です。
ユーザーがダウンロードしたものの使用しなくなってしまう原因は何か? 一つには機能が自分のほしいものと異なるケースです。これはある意味致し方ないと思います。アプリのコンセプトが受け入れられなかったということだからです。
もう一つ考えられるのは、アプリが不安定である場合です。ToToDoの場合、最初のiOSアプリだったこともあり、若干不安定な部分があったままリリースしてしまったため、良くなかったと感じています。
アプリの安定性を高めるためにはデバッグが重要であり、そのために役立つのがクラッシュログであるという記事を書きました。また、バージョン管理をよく行なって、バグ修正版をすばやくリリースすることも大切だと思います。Subversionによるバージョン管理についてはこちらの記事で書いています。
アプリの不安定さは低評価につながります。ToToDoも評価1がそれなりに入っていて、コメントがないために理由はわかりませんが、おそらく不安定さが原因ではないかと考えています。それで、バージョン1.1ではかなり安定性に気をつかったつもりです。
まとめ
- 最初から安定したアプリをリリースすることは重要
- レビューサイトはAppBankだけではない
- バグ修正版はすばやくリリースし、機能向上版はちょっとずつリリースしていくのがいいかも
こんなところでしょうか。
Posted by Kentaro Ishizaki on 2010年11月24日
ブランチの作成は基本的にリポジトリ内でコピーするだけです。
$ svn copy file:///Users/ishi-ken/Documents/svn/SubVersionSample/trunk file:///Users/ishi-ken/Documents/svn/SubVersionSample/branches/new-branch -m "Branching"
$ cd ~/Documents/SubVersionSample/
$ svn switch file:///Users/ishi-ken/Documents/svn/SubVersionSample/branches/new-branch
At revision 17.
$ svn info
最後の svn info でどのリポジトリがチェックアウトされている状態なのかを確認できます。
Posted by Kentaro Ishizaki on 2010年11月22日
やはり開発にはバージョン管理(ソースコード管理)は必須です。小規模で開発していると必要ないと思ってしまいがちですが、次期バージョンを開発中にリリース済みのバージョンにバグが見つかった場合などはどうでしょうか。バージョン管理をしていないと、修正だけ行なおうにも、すでにいろいろな新機能の実装が進んでいて、バグ修正版を出すことが難しくなります。
能書きはこのあたりにしておいて、バージョン管理ソフトはいろいろありますが、Mac環境には最初から含まれているSubVersionを使うのが面倒が少なくていいと思います。
この記事では、すでに開発中のXCodeプロジェクトをSubVersionでローカル管理する方法を取り上げます。自分への備忘録的に書いていますので、間違っていたら指摘してください。
リポジトリのパスを環境変数に登録しておくと便利みたいですが、ここではしていません。
1. SubVersionのリポジトリを作成する
個人的にはプロジェクトごとのリポジトリを作成した方がいいような気がするので、そうします。今回は “SubVersionExample” というプロジェクトをバージョン管理することにします。
$ cd ~/Documents/
$ mkdir svn
$ cd svn/
$ mkdir SubVersionExample
$ svnadmin create SubVersionExample/
2. trunk, tags, branchesを作る
SubVersionはtrunk, tags, branchesで管理するように推奨されています。
- trunk: メインの開発ソース
- tags: スナップショットを保存
- branches: メインの開発とは別の更新をするために使用
イメージとしては、リリースを行なった時点で tags にディレクトリを作成して(”ver1.0″ など)、リリースバージョンのスナップショットを取っておきます。リリースバージョンにバグが見つかった場合などに、スナップショットを元に branches の中にブランチを作成して、バグ修正ブランチを作成する、という感じです。
ここでは、この構成を持ったディレクトリをコミットします。
$ mkdir tmp
$ cd tmp/
$ mkdir trunk tags branches
$ svn import ./ file:///Users/ishi-ken/Documents/svn/SubVersionExample/ -m "Initial import."
Adding trunk
Adding branches
Adding tags
Committed revision 1.
$ cd ..
$ rm -rf tmp
ここらへんがちょっと分かりにくいのですが,リポジトリの内部は特段何かの決まりとか分類とかがあるわけではなくて、自分で勝手に決めていいわけです。それでリポジトリ内部にその三つのディレクトリを作成し、trunk内をメイン開発ディレクトリとして定めます。
3. 既存のプロジェクトをリポジトリに登録する
まず、リポジトリのtrunkを既存のプロジェクトディレクトリにチェックアウトします。上書きされることはありませんが、心配なら別のところにコピーして取っておくこともできます。
$ cd ~/Documents/SubVersionExample/
$ svn checkout file:///Users/ishi-ken/Documents/svn/SubVersionExample/trunk .
Checked out revision 1.
次に、プロジェクトファイルをリポジトリに追加します。
$ svn add --force .
A build
…
A Classes
A Classes/SubVersionExampleAppDelegate.h
A Classes/SubVersionExampleAppDelegate.m
A Classes/SubVersionExampleViewController.h
A Classes/SubVersionExampleViewController.m
A main.m
A MainWindow.xib
A SubVersionExample-Info.plist
A SubVersionExample.xcodeproj
A SubVersionExample.xcodeproj/ishi-ken.mode1v3
A SubVersionExample.xcodeproj/ishi-ken.pbxuser
A SubVersionExample.xcodeproj/project.pbxproj
A SubVersionExample_Prefix.pch
A SubVersionExampleViewController.xib
いろいろなファイルが登録されます。しかし、build以下はバージョン管理をしなくてもよいファイルです。なので、buildの登録を解除し、build以下を無視する設定にします。
$ svn revert build --recursive
Reverted 'build'
$ svn propset svn:ignore "build" .
property 'svn:ignore' set on '.'
.xcodeproj以下にはユーザー独自のファイルがあるので、それもはずしてもいいかもしれません。
$ svn revert SubVersionExample.xcodeproj/ishi-ken.*
Reverted 'SubVersionExample.xcodeproj/ishi-ken.mode1v3'
Reverted 'SubVersionExample.xcodeproj/ishi-ken.pbxuser'
最後にコミットしてプロジェクトファイルの登録が終了です。
$ svn commit -m "New XCode project."
Sending .
Adding Classes
Adding Classes/SubVersionExampleAppDelegate.h
Adding Classes/SubVersionExampleAppDelegate.m
Adding Classes/SubVersionExampleViewController.h
Adding Classes/SubVersionExampleViewController.m
Adding MainWindow.xib
Adding SubVersionExample-Info.plist
Adding SubVersionExample.xcodeproj
Adding SubVersionExample.xcodeproj/project.pbxproj
Adding SubVersionExampleViewController.xib
Adding SubVersionExample_Prefix.pch
Adding main.m
Transmitting file data ..........
Committed revision 2.
4. XcodeのSCMと連携させる
SubVersionExampleのリポジトリを消してしまったので、ToToDoのリポジトリになっていますが、手順は以下の通りです。
- SCMメニューから「SCMリポジトリを構成…」を選択
.png)
- 表示されるウィンドウの左下の「+」ボタンをクリック
-300x271.png)
- リポジトリの名前を入力。SCMシステムは「Subversion」を選択して、OKをクリック
-300x126.png)
- リポジトリが追加されるので、URLにSubversionリポジトリのURLを入力。「認証されました」というのが下に出るのを確認したら、OKをクリックして閉じる
-300x271.png)
- Subversionリポジトリに対応するXcodeプロジェクトを開き(あらかじめ開いておいてもOK)、プロジェクトメニューの「プロジェクト設定を編集」を選択
-257x300.png)
- 「一般」タブに切り替えて、「ルートとSCMを構成…」ボタンをクリック
-300x249.png)
- プロジェクトルートとSCMを関連づけて、OKをクリック
-300x166.png)
- 以上で終了
5. スナップショットの作成
スナップショットやブランチの作成は、svn copyを使います。単純コピーですね。簡単です。
$ svn copy file:///Users/ishi-ken/Documents/svn/SubVersionExample/trunk file:///Users/ishi-ken/Documents/svn/SubVersionExample/tags/ver1.0
Committed revision 3.
SVN_EDITORが設定されていない、みたいなエラーメッセージが出る場合は、
これを実行。
Posted by Kentaro Ishizaki on 2010年11月22日
UIActionSheetは便利ですが、長い文字列を表示させようとすると、truncateされてしまいます。日本語は母国語のせいか短めに表現できるのでいいのですが、フランス語とかになると結構長くなってしまって困ることがあります。
この問題を解決するには、ボタンの画像を用意して、UIButtonとしてUIActionSheetにaddSubViewするのが手っ取り早いです。
画像は以下のを使えます。Retina Display対応画像も用意してあります。
コードは以下のような感じです。
ボタン一つにつき53.0fの高さが使われるようなので、必要に応じてoffset値などを変更してください。ボタンが複数あるときでも53.0f間隔で配置されるようにすれば良さそうです。
文字のサイズをループで取得しているのは、ボタンのtitleLabelのサイズを適当な値で設定して、adjustsFontSizeToFitWidth = YES;とすると、文字列が縦方向の真ん中にこないためです。
そして、ボタンのインデックスをtagに設定しておいて、それをイベントハンドラ内で読み取って、dismissWithClickedButtonIndex: でUIActionSheetの通常のdelegateで処理できるようにしています。
- (void)actionButtonDidTouchUpInside:(id)sender {
CGFloat offset = 0.0f;
NSInteger buttonIndex = 0;
NSString *longTitleString = NSLocalizedString(@"longTitle1", nil);
UIActionSheet *actionSheet = [[[UIActionSheet alloc] initWithTitle:nil delegate:self cancelButtonTitle:NSLocalizedString(@"cancel", nil) destructiveButtonTitle:nil otherButtonTitles:longTitleString, nil] autorelease];
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
UIImage *image = [UIImage imageNamed:@"ActionSheetButton.png"];
[button setBackgroundImage:image forState:UIControlStateNormal];
[button setBackgroundImage:[UIImage imageNamed:@"ActionSheetButtonHighlighted.png"] forState:UIControlStateHighlighted];
[button setFrame:CGRectMake(21.0f, offset + 21.0f, image.size.width, image.size.height)];
[button setTitle:longTitleString forState:UIControlStateNormal];
[button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[button setTitleColor:[UIColor whiteColor] forState:UIControlStateHighlighted];
for (CGFloat size = 20.0f; size >= 10.0f; size -= 1.0f) {
CGSize stringSize = [longTitleString sizeWithFont:[UIFont boldSystemFontOfSize:size]];
if (stringSize.width <= image.size.width - 10.0f) {
button.titleLabel.font = [UIFont boldSystemFontOfSize:size];
break;
}
}
button.tag = buttonIndex++;
[button addTarget:self action:@selector(customActionSheetButtonDidTouchUpInside:) forControlEvents:UIControlEventTouchUpInside];
[actionSheet addSubview:button];
[actionSheet showInView:self.view];
}
- (void)customActionSheetButtonDidTouchUpInside:(id)sender {
if ([sender isKindOfClass:[UIButton class]]) {
UIButton *button = (UIButton *)sender;
if ([[button superview] isKindOfClass:[UIActionSheet class]]) {
UIActionSheet *actionSheet = (UIActionSheet *)[button superview];
[actionSheet dismissWithClickedButtonIndex:button.tag animated:YES];
}
}
}
Posted by Kentaro Ishizaki on 2010年11月15日
コントロールを配置したあとにまた取得する方法として、プロパティの形で保持しておく方法もありますが、最近気に入っているのは、あらかじめコントロールにtagを設定しておいて、
UIView *taggedView = [self.view viewWithTag:${TAG}];
とする方法です。${TAG}のところは自分で設定したタグ値に置き換えてください。
Posted by Kentaro Ishizaki on 2010年11月8日
まともな開発者なら常識なのかもしれませんが、最近になってここでやり方を知ったので、書いておきます。
デバッガで動作中に落ちたりすれば、簡単にスタックトレースを参照できるため、問題の特定はしやすいのですが、持ち歩いている最中に自作アプリが落ちた場合の問題箇所の特定は意外と難しいものです。
このときに使えるのがクラッシュログ( *.crash )です。iPhoneの場合、iTunesと同期したときに ~/Library/Logs/CrashReporter/MobileDevice/(iPhone名)/ にクラッシュログが保存されます。また、リリース後のアプリについては、iTunes Connectにある程度クラッシュログがあがってきます。このクラッシュログにはスタックトレースらしきものが保存されていますが、
Thread 0 Crashed:
0 libobjc.A.dylib 0x3002d7d8 0x3002b000 + 10200
1 TextInput_ja 0x34c94648 0x34c8e000 + 26184
2 TextInput_ja 0x34c92888 0x34c8e000 + 18568
3 UIKit 0x31e93e10 0x31e4d000 + 290320
こんな感じで、アドレスしか記録されていません。そこで、gdb と保存されている dSYM を使って、このアドレスから該当ソースコードのファイル名と行番号を取得することができます。
iPhoneの場合、armベースのため、使用するgdbは /Developer/Platforms/iPhoneOS.platform/Developer/usr/libexec/gdb/gdb-arm-apple-darwin になります。
dSYMはプロジェクトディレクトリの build/(DebugまたはRelease)-iphoneos/ の下にあります。拙作のToToDoの場合、
$ cd ~/Documents/ToToDo/build/Release-iphoneos/ToToDo.app.dSYM
$ cd Contents/Resources/DWARF
$ /Developer/Platforms/iPhoneOS.platform/Developer/usr/libexec/gdb/gdb-arm-apple-darwin ToToDo
こんな感じで dSYM を読み込ませて gdb を起動します。たぶん何かをすれば、最後の行は gdb ToToDo と書けばよくなるようなのですが、方法が分かりませんでした。どなたか教えてください。
その後、クラッシュログのモジュール名の次のアドレスを指定して、info line *(アドレス) とすると以下のようにソースファイル名と行番号が取得できます。
(gdb) info line *0x0000a79a
Line 979 of "/Users/nlights-iapps/Documents/ToToDo/Classes/TDInputViewController.m" starts at address 0xa78e <-[TDInputViewController didReceiveMemoryWarning]+6> and ends at 0xa7a0 <-[TDInputViewController didReceiveMemoryWarning]+24>.
ということで、リリースしたアプリの dSYM は絶対取っておいたほうがいいですね。
symbolicatecrash というツールもあるようです。ここに簡単な説明がありました。
Posted by Kentaro Ishizaki on 2010年11月5日
現在のFirstResponderを取得するには、以下のコードが使えます。
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
UIView *firstResponder = [window performSelector:@selector(firstResponder)];
しかし、これはプライベートAPIなので、これを使うとリジェクトの対象になってしまいます。
そもそも、FirstResponderを取得したい理由は、resignFirstResponderを実行したいだけの場合がほとんどです。その場合、以下の一行だけで要件は済んでしまいます。
[self.view endEditing:YES];
純粋にFirstResponderを取得するコードは、ここを参照してください。