[iOS] Habit Keeper Freeがレビューサイトなどに載せてもらえました

2013/03/5

こんにちは。きんくまです。

だんだん、暖かくなってきましたね。三寒四温です。
今年は梅をみることなく2月が終わってしまったので、桜が待ち遠しいです。

さてさて、先日無料版のHabit Keeper Freeを出しまして、おかげさまでレビューサイトなどに取り上げてもらえました。
皆様どうもありがとうございます! 自分で検索して気がついた分だけなので、漏れがあったらスミマセン!

レビューサイト

※掲載日順です
>> iPhone 5用防水ハードケースほか【2月21日版】新着アプリ・アクセサリー情報(Touch Lab) – エキサイトニュース(タッチ ラボさんはエキサイトと契約しているのか、同じ記事が出ていました。)
>> iPhone 5用防水ハードケースほか【2月21日版】新着アプリ・アクセサリー情報 – Touch Lab – タッチ ラボ
>> 無料:日課や習慣、ルーチンワークを管理できるライフログアプリ「Habit Keeper Free」 | iStation – おすすめiPhoneアプリ紹介、ニュース配信
>> Habit Keeper Free: アイディア次第で色々な習慣を記録できるアプリ。無料! – たのしいiPhone! AppBank

個人ブログ

>> 今日のキニナル(2013/02/21)
>> Habit Keeper に無料版出てる | K’s Blog

分析

前に有料版を初リリースしたときにレビューサイトに申し込んだときは全く駄目でした。それで、今回は運良く載せてもらえたのですが、どうしてか自分なりに考えてみました。

1. 無料だった
2. それなりに機能がついていた
3. 有料版で一度上位(といってもカテゴリ18位だけど)に行った実績があった

自分としては、1の無料はかなり必要条件のような気がしています。
アプリ開発をしながら徐々に調べてわかってきたのですが、やはり一般ユーザーの方が何もしらない有料アプリにポンとお金を出して買うというのは、相当にハードルが高いようです。だから、紹介するレビューサイトも有料よりも無料の方が紹介しやすいというのはあると思います。

iPhoneが出始めた当初は、ガジェット好きな人が多くて、多少の冒険をしてお金を払ってもよいという人の割合が多かったんじゃないでしょうか。それが、一般層にまで普及してきて、「基本無料でしょ」みたいな考えの人の割合が増えてきたと。無料アプリでも優秀なアプリがたくさんあるというのも背景にあると思います。

で、有料アプリは、

・アイデアが画期的で「ぜひ手にとってみたい」もの(人に紹介したいぐらいの)
・機能が豊富で、かつよくまとまっている
・もともとブランド力がある開発元が出す(過去にヒットしたアプリがあるとか、もともと大きなWebサービスを運営しているとか)

この中のどれかがあったりしたら、レビューされたり、購入してもらえる可能性があるかもしれません。(購入した後の評価は別のはなしです)
自分の場合は画期的なアイデア出しは弱いので、自分の考えた方向性にユーザーの意見をとりいれつつ、使いやすいように改良してまとめるという感じです。
あとこれはツール系のアプリを出してみて感じだことで、ソーシャルゲームやミニゲーム系ではまた違ったことになると思います。

また今回は、同一アプリ内で課金でアップグレードする最近の流れではなく、有料版とは別のFree版を出すという従来のやり方でやってみました。「いまどき別アプリなんて古い。ありえない」という感じに受け取られるかと思ったのですが、有料版が安定しているところを見ると、まだまだそんなことはないんじゃないかなと思っています。でも、もし次に別アプリを出すときはたぶんアプリ内課金にすると思うケド。

今後もアップデートしていきますので、よろしくお願いします。

LINEで送る
Pocket

[iOS] 自作で効果音を作って組み込む流れのまとめ

2013/03/4

こんにちは。きんくまです。

先日iPadアプリで効果音を作る記事をかきました。
>> [iOS] アプリ開発日記 効果音つくるのにKORG iPolysix for iPadが便利

それでそこから実際のアプリに組み込む流れをまとめてみました。
といっても、このあたりのことを特に詳しいわけではないので、個人的メモな感じです。

アプリから書き出すときに気をつけること

自分の場合はiPolysixというiPadアプリで効果音を作りました。
んで、気に入った音ができたのでwavで書き出します。そのときに、どうやら音量のバランスを見てあげてから書き出した方が良かったです。

というのは、いざ全部の音を取り込んでみると、すごく耳についたり、逆に小さすぎちゃったりと音によってマチマチでして、音のバランスが上手くとれていないように感じたのです。

プログラムで音量を調節することも可能なのですが、自分で作るのだったら書き出し時に最初からある程度はバランスをとっておいた方が良さそうです。
たいていのソフトにはミキサー(Mixer)という部分があるみたいで、そこで最終出力の音を調整してあげると良いです。

基本は赤いメーター部分に、ゲージや針が入らないようにします。そこに入ると音がビビるというか、割れるというかそんな感じになります。
なので、緑色部分の中で収まるようにする。

作ってて感じたのは、音量メーターでは同じ音量のはずなのですが、高音の方が耳につく感じになりやすく、逆に低音は聞こえにくい印象でした。さらに、ヘッドホンを通した音とiPhoneのスピーカーでもかなり音の聞きやすさや聞こえ方は違うので、両方ざっくりと聞きながら、アプリ全体を通してバランス良くなるように気をつけると良いかと思いました。

さきほどの記事に書いたのですが、このあたりの試行錯誤中は自分の端末にwebサーバーをたてて、そこをiPhoneから見に行くような作りにしておくと楽でした。

トリミング

書き出したwavデータは無音部分が含まれていると思うので、そこをカットしてあげると容量が節約できます。

自分の場合は無料のAudacityを使ってやりました。
>> Audacity: フリーのオーディオエディタ・レコーダー

Audacityにwavファイルを読み込んだところ。こんな感じに波形データが見えます。左端に波形があって、右側の方は無音部分になっています。

audacity_fig1

無音部分がいらないので、ギリギリにして切ってしまいたくなるのですが、0.5秒ぐらいは確保しつつ切り取りました。
これは何でかといいますと、以前にFlashでだったか覚えてないのですが、あまりに短すぎるサウンドファイルがうまく読み込めないことがあった経験があったためです。
iPhoneではこんなことが起きるのかは不明ですが、念のためにそうしています。

無音をきりとったところ。このあと、File > ExportでまたWAVで書き出します。

audacity_fig2

これで、いろんな形式に変換する前のオリジナルデータができました。

形式を変換する

このWAVデータは非圧縮で44100Hzの16BitのCD音質となっております。
周波数とビットレートについて、何年か前に書いた記事がありますので、これ何ぞ?という方はもし良かったらみてみてくださいませ。

>> はじめてのサウンドプログラミング2(サンプリング周波数と量子化ビット数)

このままアプリに組み込んでも良いのですが、実際には気にならない程度まで音質を落として使った方が容量も軽くなって良さそうです。

またiPhoneやMacのネイティブフォーマットのAIFFに変換することにしました。
長いBGMなどは圧縮形式のmp3形式にした方が容量的に良いと思いますが、今回は短い効果音なので、非圧縮形式にしました。

——————————–
完全に余談なのですが、最初は記事を書いていて、ここで「iPhoneはARMプロセッサでリトルエンディアンだからドヤ顔でAIFFだよ」と書こうとしたのですが、調べてみるとwavでも問題ない気がしてきました。

>> A few things iOS developers ought to know about the ARM architecture

WAVはMicrosoftとIBMが作ったwindowsのネイティブフォーマット。RIFFを元に作った。
リトルエンディアン。非圧縮。
>> WAV

AIFFはAppleが作ったmacのネイティブフォーマット。IFFを元に作った。
もともとはビックエンディアンだった。MacOS-Xの開発とともに、リトルエンディアン版のAIFF-Cを作った。現在iTunesでAIFFとして読み込むときはAIFF-Cが使われている。非圧縮。
現在でもどちらのエンディアン版も使われている。なので拡張子がAIFFでも、中身を見てみないとエンディアンはどちらかわからない。
>> Audio Interchange File Format

ここでビッグエンディアンのIFFをベースに、リトルエンディアンのRIFFは生まれたので、WAVもAIFFも根っこは同じらしい。
>> Interchange File Format
——————————–

さて、今回はAIFF形式で128kHz 16bitにすることにしました。

Cocoaの日々さんに、ファイルフォーマットのコマンドツールが紹介されてました。
>> Cocoaの日々: オーディオフォーマット変換 afconvert

で、ファイル1枚につき、コマンド1回たたくと面倒なので、1回でフォルダ内の全てのwavを目的のaiffにするスクリプトを書きました。

#!/bin/sh

for dir in ./*.wav; do
    test_path=$dir
    string_filename=${test_path##*/}
    string_filename_without_extension=${string_filename%.*}
#    string_path=${test_path%/*}
#    string_extension=${test_path##*.}

    afconvert -f AIFF -d BEI16@12800 ${string_filename_without_extension}.wav ${string_filename_without_extension}.aiff
done

ファイル名の取得部分はこちらに掲載されているものを使わせていただきました。
>> 【FreeBSD】シェルスクリプトでパス文字列からファイル名/ディレクトリ名/拡張子を抽出する

使い方はwavの入っているフォルダに、上のテキストを適当な名前(例えばconvert_aiff.sh)で保存します。

Terminalで、cdでそのフォルダに移動した後に、実行権を与えます。

chmod a+x ./convert_aiff.sh

ls -l でこんな感じになっていれば準備完了

-rwxr-xr-x  1 kinkuma  staff     535 Feb 27 10:27 convert_aiff.sh*

実行します。最初の./というカレントディレクトリを意味するパスをつけないとうまく実行できません。

./convert_aiff.sh

これで、目的のファイルが全て変換されて音素材として完成しました。
あとはアプリに組み込んでAVAudioPlayerなどを使って鳴らしてあげれば完成です。

——————————-
書き終わってきづいたのですが、さきほどのコマンドafconvertでAIFFに変換しているときにビッグエンディアンに強制的にしている(AIFF-Cでなく)ので、これよりもWAV形式のままサンプリングレートを落とした方が良い気がしているんですけど、どうなんでしょうね、、?
——————————-

LINEで送る
Pocket

[iOS] 無料版の日課 / 習慣 / ルーチンワークを記録するiPhoneアプリHabit Keeper Freeをリリースしました

2013/02/21

こんにちは。きんくまです。

日課 / 習慣 / ルーチンワークを記録するiPhoneアプリHabit Keeperの無料版をリリースしました。

>> 日課/習慣/ルーチンワークをサクサク記録!- Habit Keeper Free

有料版との違いは

・広告が表示されます
・持てる日課は最大で3つです
・いくつかのオプションが使えません(アプリアイコンバッジ、パスコードロック、書き出したデータの読み込みなど)

などとなっております。
基本的な日課や習慣を記録する部分はそのまま使用することが可能です。

今回のバージョンから数値入力する際に数値キーボードが同じ画面で出せるようになりました。

keyboard_sample

カスタムキーボードのやり方を調べてみたら思ったよりも簡単そうだったので実装してみました。
それにともない、有料版もこの点のみのアップデートをしました。
次回はもうちょっと機能をつけたいと思っています。

あと細かいところなのですが、アプリ名をHabitKeeper -> Habit Keeperと単語間のスペースを空けてみることにしました。これはiTunesのSEO対策のためです。

参考のためのリンクです。
>> AppStore SEO
>> AppStore SEOでダウンロードを増やす方法(初級者編)【2012/9/1改訂 含Chomp対策】

海外で少しでも探しやすいように、単語で分けましたです。

どうぞよろしくお願いします。

LINEで送る
Pocket

[iOS] アプリ開発日記 効果音つくるのにKORG iPolysix for iPadが便利

2013/02/16

こんにちは。きんくまです。

初めにステマやってしまうと、

HabitKeeperが仕事効率化の有料18位になりました!

itunes_store_snapshot

※キャプチャが英語のタイトルなのは、macの設定を英語にしてるとJPストアでも英語で表示されるためです。

わー。パチパチ。
これまで売り始めて4ヶ月間、最高でも40位より上がったことがなかったので、かなり嬉しいです。
25位の壁は本当に高くて高くて、、。この記事書いてる現在は27位なのですが、たまたまの一度でも入ることができたのは良かったです。
それで、自分でも何で上がったのかよくわからなかったので、ググったところブログに取り上げてくださった方がいて、それの影響で上がったみたいです。
ありがとうございます!

>> Day Oneと並べるしかないでしょう!毎日の習慣やルーチンタスクを記録するHabitkeeper | Punksteady

こちらの方も、1月に書いてくださってました。ありがとうございます!
>> [習慣化] 毎日の運動をより楽しむならどれだけやったかが見える『HabitKeeper』を活用したらいいと思う | nasukedotnet

というわけでこれからも開発頑張ります。

ステマ終わり。

アプリに効果音をつけたい

でここから開発のはなし。アプリに効果音をつけようと思いました。
最近はいくつかのアプリでも効果音を使ってるものがありまして、買ったアプリの中ですと

・Day One
・Clear
・Grid Diary

などが入っておりました。ちなみにこれらのアプリはシンプルでデザインもカッコいいです。

効果音をつけるには、効果音のデータがないと始まりません。
効果音のデータはフリーや有料の素材集を使うか、人に頼むか自分で作れば良いです。
今回は、自分で作ってみようと思いました。

とはいえ、DTMの知識などほとんどなく、数年前に興味があってDS-10を買ったときにつけた基本的な知識のみです。

しばらくは初代iPadが出たときにたまたま買ったシーケンサーアプリがありまして、それをいじっていたのですが、どうも出力した音に何か透明感というかそういうのが欠ける気がしておりました。

で最近はどんなのがあるのかと思い、ググったりiPadのランキングを見てよさそうなiPolysixというアプリにしてみました。

>> KORG iPolysix for iPad|KORG INC.

値段は2600円なのですが自分的にはすごく満足してます。
技術書一冊分で効果音が自分で作れればいいし。
何よりアプリの雰囲気がすごいカッコいい!です。これが一番いいたかった。
正直、DTMは門外漢なので他のアプリと比較することはできないのですが、RetinaのiPadで触っていると「作り込まれた製品」という感じでデザイナーさんもディベロッパーさんもすげえ!などと、一人でうなってしまいました。こんなアプリ作ってみたい。

余談ですがアプリ調べてるときに思ったのですが、楽器というかDTMみたいなアプリってどれもデザインがシャレ乙ですね。

iPolysixなのですが、機能はDS-10のときに覚えたうる覚えの知識をフル稼働しつつ、「そういやローパスフィルタかけると音がこもるやつで、レゾナンスがミョンミョンするやつだっけ?」などとやってました。

>> できたサンプル音

つくった音はこんな感じにwavに出力できるので、そのまま使うなり変換するなりしてアプリにとりこめます。
カチッとかピヨッとかのクリック音も作れるし、なにより「もう少し丸く」とか「もう少し金属っぽく」とか「長め」「短め」など好みの音を作ってるのが楽しいです。
ただ生楽器をサンプリングしたような音を作りたい場合は、ひょっとすると別のアプリの方が良いかもしれないです。

自分は作曲はできませんが、こんな感じの効果音ならなんとか作れそうな気がしてきました。

試行錯誤しながら音を作って取り込みたい

できた音をいざXcodeのプロジェクトに組み込んでみると「コレジャナイ」感がすごい出てたりして、ガックリすることがよくあります。もう少しこういう感じが良いのかなと作り変えてすぐにプレビューしたい。

iPadで作って書き出した音はUSB接続して、iTunesアプリ経由で持ってくることが可能です。でも、iPhoneに組み込むためには、またUSBをつけかえてビルドしないといけないという、、。これは面倒くさい。

で、解決方法を考えてみました。
そしたら、NSDataってURL上のデータをそのまま使えるので、開発時には自分の端末にwebサーバーを立ち上げて、そこにできたwavを放り込んで、そいつをiPhoneから見に行けば良いことに気がつきました。

一回開発用のコードでiPhoneをビルドしてしまえば、iPadは差しっぱなしでOKです。
コード的にはこんな感じで。

    NSString *path = [NSString stringWithFormat:@"http://192.168.x.x/%@", sampleFileName]];
    NSError *error = nil;
    _audioPlayer = [[AVAudioPlayer alloc] initWithData:[[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:path]] error:&error];

    if(error){
        NSLog(@"play sound error%@", error);
        _audioPlayer = nil;
        return;
    }
    _audioPlayer.delegate = self;
    [_audioPlayer play];

この場合、実際に音を出すときはネットワーク上のデータを読み込むため、音が出るまでラグが発生します。なので気に入った音ができれば、本番用にCAFに変換したりしてプロジェクトにコピーすれば大丈夫だと思います。

LINEで送る
Pocket

[サーバー管理] CentOSをParallelsにインストールしたメモ

2013/02/14

こんにちは。きんくまです。

Cent OSというLinuxのディストリビューションがあるらしいです。
サーバーによく使われているらしいので自分のmacのParallelsに入れたメモです。

インストール前

以下のページが参考になるのですが、何故かすごく重いため
googleのキャッシュも貼っておきます。

>> Parallels 上に CentOS 6.2 のセットアップ その1 – 仮想マシンの作成(もとのページ)
>> googleのキャッシュ

isoイメージはここからとりました。ダウンロードには結構時間がかかったです。

i386とx86_64とあるのですが、調べたところi386が32bit版でx86_64が64bit版とのこと。
自分はi386にしました。

中に入るとisoイメージがたくさんあるのですが、READMEを読んだところCentOS-6.3-i386-bin-DVD1.isoだけで良さそう。

後述するのですが今回Minimalインストールしたので、CentOS-6.3-i386-minimal.isoだけでもよいかもしれないです。

インストール

Parallelsのメニュー File > NewでInstall Windows or another OS from DVD or image fileを選んで先ほどのisoを選択

ファイルチェックするとその先にすすめなかったので、チェックはしませんでした。

GUIのデスクトップ環境は今回特に必要なかったことと、いちど勉強のため1からいろいろと入れてみたかったのでMinimalでインストールしてみました。
このあたりの流れは以下のページが詳しかったです。

>> 本章では CentOS Linux 6 のインストールと、インストール後の設定変更について掲載します。
>> CentOS6.2 を最小インストールする方法 « SEECK.JP サポート

今回Minimalインストールだったせいか5分もかからずにインストールできました。

ネットワーク

最初はネットワークにつながってなかったので、viで

/etc/sysconfig/network-scripts/ifcfg-eth0

を編集すればOKみたいです。

>> Mac の VirtualBox に CentOS 6.3 minimal をインストールしてターミナルから ssh で操作できるようにする – とあるへぼプログラマの雑念

自分の場合は

ONBOOT=”yes”

に書き換えて再起動しただけでネットワークにつながりました。

ユーザー作成

このままrootで暮らしてもよいのですが、ユーザーを作って生活した方が良さそうです。

—-
useradd ユーザー名
passwd ユーザー名
そのあとに希望のパスワードを入力する
—-

でユーザーを作りました。

sudo

この状態でmacからsshでログインしたところ、sudoできないと怒られました。
どうやら設定する方法があるみたいです。

>> sudo による管理者権限の付与

visudoコマンドを使うとテキスト編集画面になります。
真ん中へんにAllows people in group wheel to run all commands
とか書いてあったので、この下あたりに

%ユーザー名 ALL=(ALL) ALL

と追記

macからsshで入る

端末のipアドレスを調べます。
CentOS本体でifconfigコマンドうってeth0のinet addrにipアドレスが書いてあるのでそれを覚えておいて

macのターミナルから

ssh ユーザー名@ipアドレス

などと打ちます。例えばusername@10.231.34.8とか(ipアドレスはダミー)

ログイン直後はそのユーザーのhomeディレクトリに入ってました。
/home/username

そうそう、macでログインすると

centos -bash: warning: setlocale: LC_CTYPE: cannot change locale (UTF-8)
とかいうエラーが出たので調べました。

>> cloudpackブログ – cloudpack(クラウドパック)Amazon EC2などクラウドの導入設計、運用・保守サービス: CentOS(6.0)でロケール関係の設定

/etc/sysconfig/i18n

LANG=”ja_JP.UTF-8″
LC_CTYPE=”ja_JP.UTF8″

としたところ、ログインしても警告がでないようになりました。

yumでemacsなどをインストール

ここまでできたらあとはmacのターミナルでいろいろとできそうです。
とりあえずemacsがなかったので

yum install emacs

などと打つとうまい具合にインストールしてくれました。

yumはmacでいうhomebrewみたいな依存関係をうまいこと解決しつつパッケージを管理してくれるコマンドみたいです。

あとは、FTPサーバーの設定などをすればmacのデータとやりとりできるかな。

自分の場合、前からmacでもターミナル(iTerm2使ってます)をちょこちょこ使っているので、割とすんなりインストールできました。

LINEで送る
Pocket

[iOS] アプリ開発日記 無料アプリに広告を組み込む

2013/02/12

こんにちは。きんくまです。

ふと、アプリのLite版というかFree版を作ってみようと思いました。
機能に制限を設けて、広告をつけたバージョン。
各種オプションと日課を持てる個数(3つ)に制限を設けたもの。

最近はフリーミアムバージョン(基本無料+アプリ内課金)が基本のようですが、すでに有料でアプリは出しているので、別で出すことにしようかと。
フリーミアムは次に別のアプリを出すときはやってみようと思います。
数年前にはこういうFree版が結構出ていたと思うのですが、今は新規アプリではあまり見かけなくなったような。
あんまり効果がなくなっちゃったんでしょうか。

で、いざ広告ってどうやってつけるのかな。いやそもそもどんな広告が今あるのよ?って思って調べたらAppBankさんに良記事がありました。

>> 主なアドネットワークサービス紹介【iPhoneアプリ広告で稼ぐ・番外編2】 – たのしいiPhone! AppBank

いろいろと用語の紹介や、今はどんな配信会社さんがあるかについてよく書かれていました。あとAppBank Networkのバナーを表示させると紹介してもらえることもあるとかないとか。

で、広告は国内と海外で読み込みを分けたかったり、あと表示する優先順位をリモートで変更したいなと思いました。
ググったらやっている方を見つけました。

>> 自前で広告ライブラリを切り替えてリスク分散しよう! » MOKYN

どうやら振り分けのサービス自体すでにあるみたいですが、とりあえず自分としてはこんな感じにしようかなと。

1) 設定ファイル(JSON)を海外用と国内用に切り分けて自分のサーバーに置いておく
2) アプリからLocaleごとに(国内=JP or 海外)設定ファイルを読みにいく
3) 設定ファイルに書かれた優先順位で読み込む広告会社を決定
4) 順番に広告を読んで、もし取得できなかったら次の順位の広告を読む
5) 最終的にどれも読み込めなかったらアプリの有料版のAppStoreへの画像リンクを出す(自社バナー的な)
6) 2のJSON設定ファイルが万が一取得できなかった場合は、アプリにあらかじめ順位を埋め込んでおきその順番とする

一度自前で使いやすいものを作っておけば、別アプリにもすぐに流用できそうですね。

情報もらうばっかりだとわるいので、つくったソースを書いときます

HKAdManager.h

#import <Foundation/Foundation.h>
#import <iAd/iAd.h>
#import "GADBannerView.h"
#import "GADBannerViewDelegate.h"
#import "NADView.h"

@interface HKAdManager : NSObject<NSURLConnectionDataDelegate, ADBannerViewDelegate, GADBannerViewDelegate, NADViewDelegate>
{
    NSURLConnection *_urlConnection;
    NSMutableData *_loadedData;
    NSArray *_adNetworkLoadOrder;
    NSInteger _adNetworkLoadIndex;
    
    //banner
    ADBannerView *_iAdBannerView;
    GADBannerView *_gAdBannerView;
    NADView *_nendAdView;
    NADView *_appBankAdView;
    UIButton *_ownBannerButton;
}
@property (nonatomic, weak) UIViewController *rootViewController;
@property (nonatomic, strong) UIView *adContainerView;
- (id)initWithRootViewController:(UIViewController *)rootViewController;
- (void)setUp;
- (void)reset;
@end

HKAdManager.m

#import "HKAdManager.h"

NSString *const kHKAdManagerWorldSettingsFileURL = @"http://sample.com/world_settings.json";
NSString *const kHKAdManagerJPSettingsFileURL = @"http://sample.com/jp_settings.json";

//ad type
NSString *const kHKAdManagerAdTypeiAd = @"iAd";
NSString *const kHKAdManagerAdTypeAdMob = @"AdMob";
NSString *const kHKAdManagerAdTypeNend = @"Nend";
NSString *const kHKAdManagerAdTypeAppBank = @"AppBank";
NSString *const kHKAdManagerAdTypeOwn = @"Own";

@implementation HKAdManager

- (id)initWithRootViewController:(UIViewController *)rootViewController
{
    self = [super init];
    if(self){
        _rootViewController = rootViewController;
        CGSize screenSize = [UIScreen mainScreen].bounds.size;
        _adContainerView = [[UIView alloc] initWithFrame:CGRectMake(0, 44 + 20, screenSize.width, 50)];
        [self setUpOwnBanner];
    }
    return self;
}

- (void)startNetworkIndicatorAnimate
{
    [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
}

- (void)stopNetworkIndicatorAnimate
{
    [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
}

- (void)setUp
{
    _adNetworkLoadOrder = nil;
    _adNetworkLoadIndex = 0;
    if(![self loadSettingsJSON]){
        [self loadNextAdNetwork];
    }
}

- (void)reset
{
    [self cleariAd];
    [self clearAdMob];
    [self clearNend];
    [self clearAppBank];
    [self setUp];
}

- (BOOL)loadSettingsJSON
{
    [_urlConnection cancel];
    [self startNetworkIndicatorAnimate];
    NSString *jsonURL = nil;
    if([[[NSLocale currentLocale] localeIdentifier] isEqualToString:@"ja_JP"]){
        jsonURL = kHKAdManagerJPSettingsFileURL;
    }else{
        jsonURL = kHKAdManagerWorldSettingsFileURL;
    }
    NSURLRequest *req = [NSURLRequest requestWithURL:[NSURL URLWithString:jsonURL]];
    _urlConnection = [[NSURLConnection alloc] initWithRequest:req delegate:self];
    if(!_urlConnection){
        NSLog(@"ad settings JSON file url initialize failed");
        [self stopNetworkIndicatorAnimate];
        return NO;
    }
    return YES;
}

- (void)storeDefaultAdOrder
{
    if([[[NSLocale currentLocale] localeIdentifier] isEqualToString:@"ja_JP"]){
        _adNetworkLoadOrder = @[
                                kHKAdManagerAdTypeiAd,
                                kHKAdManagerAdTypeNend,
                                kHKAdManagerAdTypeAppBank,
                                kHKAdManagerAdTypeAdMob,
                                kHKAdManagerAdTypeOwn
                                ];
    }else{
        _adNetworkLoadOrder = @[
                                kHKAdManagerAdTypeiAd,
                                kHKAdManagerAdTypeAdMob,
                                kHKAdManagerAdTypeOwn
                                ];
    }
}

- (void)loadNextAdNetwork
{
    if(_adNetworkLoadOrder == nil){
        [self storeDefaultAdOrder];
        [self loadNextAdNetwork];
    }else{
        if(_adNetworkLoadIndex > [_adNetworkLoadOrder count] - 1){
            return;
        }
        NSString *adNetworkName = [_adNetworkLoadOrder[_adNetworkLoadIndex] lowercaseString];
        _adNetworkLoadIndex++;
        if([adNetworkName isEqualToString:[kHKAdManagerAdTypeiAd lowercaseString]]){
            [self setUpiAd];
        }else if([adNetworkName isEqualToString:[kHKAdManagerAdTypeAdMob lowercaseString]]){
            [self setUpAdMob];
        }else if([adNetworkName isEqualToString:[kHKAdManagerAdTypeNend lowercaseString]]){
            [self setUpNend];
        }else if([adNetworkName isEqualToString:[kHKAdManagerAdTypeAppBank lowercaseString]]){
            [self setUpAppBank];
        }else{
            [self loadNextAdNetwork];
            //[self setUpOwnBanner];
        }
    }
}

- (void)setUpOwnBanner
{
    if(_ownBannerButton == nil){
        //自社バナーの_ownBannerButtonの初期化処理をここにかく
    }
    [_adContainerView addSubview:_ownBannerButton];
}

- (void)clearOwnBanner
{
    if(_ownBannerButton != nil){
        [_ownBannerButton removeFromSuperview];
    }
}

- (void)ownBannerButtonTapped
{
    NSURL *url = @"リンクさせたいURL";
    [[UIApplication sharedApplication] openURL:url];
}

- (void)setUpiAd
{
    if ([ADBannerView instancesRespondToSelector:@selector(initWithAdType:)]) {
        _iAdBannerView = [[ADBannerView alloc] initWithAdType:ADAdTypeBanner];
    } else {
        _iAdBannerView = [[ADBannerView alloc] init];
    }
    _iAdBannerView.delegate = self;
}

- (void)cleariAd
{
    if(_iAdBannerView != nil){
        [_iAdBannerView removeFromSuperview];
        _iAdBannerView.delegate = nil;
        _iAdBannerView = nil;
    }
}

- (void)setUpAdMob
{
    [self startNetworkIndicatorAnimate];
    _gAdBannerView = [[GADBannerView alloc] initWithAdSize:kGADAdSizeBanner origin:CGPointMake(0, 0)];
    _gAdBannerView.adUnitID = @"xxxxx";
    _gAdBannerView.rootViewController = _rootViewController;
    _gAdBannerView.delegate = self;
    [_gAdBannerView loadRequest:[GADRequest request]];
}

- (void)clearAdMob
{
    if(_gAdBannerView != nil){
        [_gAdBannerView removeFromSuperview];
        _gAdBannerView.rootViewController = nil;
        _gAdBannerView.delegate = nil;
        _gAdBannerView = nil;
    }
}

- (void)setUpNend
{
    CGRect rect = CGRectMake(0,0,
                             NAD_ADVIEW_SIZE_320x50.width,
                             NAD_ADVIEW_SIZE_320x50.height);
    _nendAdView = [[NADView alloc] initWithFrame:rect];
    [_nendAdView setNendID:@"xxxxx" spotID:@"xxxxx"];
    [_nendAdView setRootViewController:_rootViewController];
    [_nendAdView setDelegate:self];
    [_nendAdView load];
    [self startNetworkIndicatorAnimate];
}

- (void)clearNend
{
    if(_nendAdView != nil){
        [_nendAdView pause];
        [_nendAdView removeFromSuperview];
        _nendAdView.rootViewController = nil;
        _nendAdView.delegate = nil;
        _nendAdView = nil;
    }
}

- (void)setUpAppBank
{
    CGRect rect = CGRectMake(0,0,
                             NAD_ADVIEW_SIZE_320x50.width,
                             NAD_ADVIEW_SIZE_320x50.height);
    _appBankAdView = [[NADView alloc] initWithFrame:rect];
    [_appBankAdView setNendID:@"xxxxx" spotID:@"xxxxx"];
    [_appBankAdView setRootViewController:_rootViewController];
    [_appBankAdView setDelegate:self];
    [_appBankAdView load];
    [self startNetworkIndicatorAnimate];
}

- (void)clearAppBank
{
    if(_appBankAdView != nil){
        [_appBankAdView pause];
        [_appBankAdView removeFromSuperview];
        _appBankAdView.rootViewController = nil;
        _appBankAdView.delegate = nil;
        _appBankAdView = nil;
    }
}

- (void)layoutiAdBannerView:(ADBannerView *)bannerView
{
    CGRect bannerFrame = CGRectZero;
#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_6_0
    NSString *contentSizeIdentifier = ADBannerContentSizeIdentifierPortrait;
    bannerFrame.size = [ADBannerView sizeFromBannerContentSizeIdentifier:contentSizeIdentifier];
#else
    bannerFrame.size = [bannerView sizeThatFits:contentFrame.size];
#endif
    
    bannerFrame.origin.y = 0;
    bannerView.frame = bannerFrame;
#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_6_0
    bannerView.currentContentSizeIdentifier = contentSizeIdentifier;
#endif
}

#pragma mark NSURLConnectionDataDelegate

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
    _loadedData = [[NSMutableData alloc] init];
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    [_loadedData appendData:data];
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
    [self stopNetworkIndicatorAnimate];
    NSLog(@"ad settings JSON file connection error %@", error);
    [self loadNextAdNetwork];
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    [self stopNetworkIndicatorAnimate];
    NSError *error = nil;
    id JSON = [NSJSONSerialization JSONObjectWithData:_loadedData options:NSJSONReadingMutableContainers error:&error];
    if(error){
        NSLog(@"ad settings JSON file parse error %@", error);
        [self loadNextAdNetwork];
        
    }else{
        NSMutableArray *jsonArr = JSON;
        _adNetworkLoadOrder = [NSArray arrayWithArray:jsonArr];
        [self loadNextAdNetwork];
    }
}

#pragma mark iAdDelegate

- (void)bannerViewDidLoadAd:(ADBannerView *)banner
{
    [self stopNetworkIndicatorAnimate];
    [self layoutiAdBannerView:banner];
    [_adContainerView addSubview:banner];
}

- (void)bannerViewWillLoadAd:(ADBannerView *)banner
{
    [self startNetworkIndicatorAnimate];
//    NSLog(@"will load ad");
}

- (void)bannerViewActionDidFinish:(ADBannerView *)banner
{
//    NSLog(@"action");
}

- (void)bannerView:(ADBannerView *)banner didFailToReceiveAdWithError:(NSError *)error
{
    [self stopNetworkIndicatorAnimate];
    [self cleariAd];
    [self loadNextAdNetwork];
}

#pragma mark gAdDelegate

- (void)adView:(GADBannerView *)view didFailToReceiveAdWithError:(GADRequestError *)error
{
    [self stopNetworkIndicatorAnimate];
    [self clearAdMob];
    [self loadNextAdNetwork];
}

- (void)adViewWillPresentScreen:(GADBannerView *)adView
{
    
}

- (void)adViewDidReceiveAd:(GADBannerView *)view
{
    [self stopNetworkIndicatorAnimate];
    [_adContainerView addSubview:view];
}

#pragma mark - NADView delegate

// NADViewのロードが成功した時に呼ばれる
- (void)nadViewDidFinishLoad:(NADView *)adView
{
//    NSLog(@"delegate nadViewDidFinishLoad:");
}

// 広告受信成功
-(void)nadViewDidReceiveAd:(NADView *)adView
{
    [self stopNetworkIndicatorAnimate];
    [_adContainerView addSubview:adView];
//    NSLog(@"delegate nadViewDidReceiveAd:");
}

// 広告受信エラー
-(void)nadViewDidFailToReceiveAd:(NADView *)adView
{
    [self stopNetworkIndicatorAnimate];
    if([adView isEqual:_nendAdView]){
        [self clearNend];
    }else if([adView isEqual:_appBankAdView]){
        [self clearAppBank];
    }
    [self loadNextAdNetwork];
//    NSLog(@"delegate nadViewDidFailToLoad:");
}
@end

使い方としては、initWithなんたらでインスタンス作ったらadContainerViewを任意の場所に置いて、setUpを呼ぶ。好きなタイミングでresetを呼ぶと、また1から設定ファイルを読み込みにいくようになってます。

設定jsonファイルはこんな感じ。上から優先的に呼ばれていきますです。

[
    "iAd",
    "AppBank",
    "Nend",
    "AdMob",
    "Own"
]

そうそう。各広告会社に登録し始めて気づいたのですが、申請してからそれなりに審査に日数(2〜7日?)がかかるみたいです。
もし広告つきアプリを作る場合は早めに申請しておくと吉と出てますぞ。

LINEで送る
Pocket

[iOS] 日課 / 習慣 / ルーチンワークを記録するiPhoneアプリHabitKeeperをバージョン1.4にアップデートしました

2013/02/8

こんにちは。きんくまです。

前回のエントリはこのブログ(もうすぐ丸5年)始まって以来の初のはてブホットエントリ入りになりました!
わーパチパチ。頑張って噛み砕いて1から書いた記事よりも、手順を写しただけの記事というのが残念な感じではありましたが、たくさん見てくれた人がいたので良しとしましょう。てへペロ。

さて、先週に出しておいた審査が無事通りまして、日課記録アプリのHabitKeeperがバージョン1.4にアップデートされました。

動画でのデモです。動画では英語になってますけど、日本語表記に対応しております。

新機能

ホーム画面
・左右スワイプで日付変更可能

個別の日課のカレンダー
・左右スワイプで月変更可能
・上下スワイプで日課切り替え

グラフ画面
・線グラフで画面にさわると詳細なラベルが表示
・棒グラフで各値ラベルが追加

アプリ設定
・週の曜日の始まり
・その日の残りの日課の個数を表すアプリバッジ機能追加
・パスコードロック機能
・3種類のチェックマーク画像追加(チェックマーク / バツ / まる)

データバックアップ
・メールの添付によるデータの書き出し
・メールの添付ファイルをタッチして書き出したデータの読み込み

なんかいろいろとあるんですが、まとめると

「ユーザーのみなさまの意見をとりいれて少し便利になりました。」

という感じです。

次回のアップデートもいろいろと考えておりまして。頑張ります。

雑談

ここまでが、リリースに関することで、あとは雑談みたいなものです。

いま自分のアプリの売り上げの比率は8:2で国内:海外といった感じです。
日本国内のAppStoreのカテゴリランキング(仕事効率化)はおかげさまで50位から150位くらいをゆるやかにいったり来たりしています。
自分的にはこのぐらいでゆるーく長く売れてくれた方が嬉しいので、国内はこの流れに任せてみようかと思ってます。海外ではレビュー依頼などをしたいなと。

あ、ちなみに身も蓋もない現実を書いてしまうと、こういったツール系アプリでこのぐらいの順位だとこれだけで生活できる収入にはならないです、、。25位以内に常にランクインし続けるか、ゲーム系だとまた少し違うのかもしれないケド。もしくはこのぐらいのアプリを一人で何本かリリースすることができれば何とかなるかもしれないです。

話を戻して、アプリを出した方ならわかると思うのですが、アプリはたいていの場合ランキングにも入らず、というかそもそも存在そのものが知られずに埋もれてしまうアプリがほとんどということです。
で、リリース当初にレビューサイトを全てスルーされてしまった割にランキングが健闘しているのは、何故だか正直わかりません。
思いつくのは、

1)「iphoneアプリ 日課」でGoogle検索すると1ページ目に表示される
2)年末年始のセールでランキングに入り、そこから大幅にアップデートした前回のバージョンが好評だった(レビューを書いてくれる方がいた)
3)アプリの定価を下げた(450円→250円)

ぐらいでしょうか。自分的は地味にアップデートし続けることが一番良かった気がしています。
セールについては、たとえ良いものを作っても存在を知られないと無いものとして扱われるので、一時的に価格を下げて使ってもらう機会を増やす広告費のようなものとして考えるのが良いかもしれないです。売り方だけで上げようとしてもメッキはすぐにはがれてしまうので、本体をアップデートで磨いておいた方がよいかと。

アップデートを重ねていて感じているのは、ユーザーの声をなるべく聞いてあげることが評価を上げるコツ。ということです。言ってみれば当たり前なのですが、自分自身正直に書いてしまいますと、リリース前に作っている時とリリース当初は”自分の”アプリという気持ちが強かったです。で、今は”ユーザーと自分の”アプリというふうになってきています。

んーとうまく言えないのですが、例えばゲームを作ったとしましょう。それが好評でした。続編を作ることになりました。ユーザーの意見を取り入れて続編を作りました。みたいな。
ここでよく言われるのが、ユーザーの意見を取り上げすぎると、何となく無難なものになってしまい、面白みがなくなるみたいなことです。
自分の場合は、先にこのことが頭にあってしまい、意見に素直になれなかったところがありました。クリエイターぶっちゃうみたいな。ただ今考えると、だいたいの場合は突飛な意見というよりは、本当に改善した方が良いことをきちんと指摘してくれてたと思います。

ただ、何にでも時期というかフェーズというものがありまして、初期段階の機能不足からだいたいの人が満足できる段階になった状態からは少し考えた方が良いかもしれないです。ここからは、本当の機能不足ではなくて、ある人から見ればよけいな複雑になった機能になる可能性があるからです。シンプルじゃなくなるみたいな。

この見極めは非常に難しいです。開発者がもういらないだろうと考えても、ユーザーは欲しいと思っているかもしれないし、その逆もまたあるからです。この段階にきてはじめてさきほど書いた「ユーザーの意見を取り入れすぎてうんぬん〜」の話を考えても良いと思います。で、まだその段階にない初期開発では、なるべく聞いた方が良いかと思います。今の自分のアプリはあともう少しこの初期段階かと思っています。

LINEで送る
Pocket

ページトップへ戻る