数独パズルアプリを作り直した。旧アプリはキーチェーン周りの問題でアップデートできなくなり、ストアから削除。後継アプリを新規で出した。
ところが、機能は後継のほうが上なのにまったくダウンロードされない。新規ユーザーはほぼゼロ。一方で削除したはずの旧アプリは、既存ユーザーが今も毎日遊んでいて広告も回っている。
この「旧アプリの現役ユーザーを新アプリへ送りたい」を、アプリの更新なしで実現した話を書く。
なぜ新アプリは埋もれるのか
データを見て原因がはっきりした。新アプリの新規ユーザーは、アナリティクス上**全て (direct)**だった。
(direct) はApp Store検索経由ではなく、URLを直接知っている人(=自分や既存ファン)からの流入を意味する。つまりApp Store検索結果に一切出てきていない。
理由は単純で、新アプリは別アプリ扱いだから:
- レビュー・評価がゼロからの再スタート
- ランキング・検索順位の蓄積がない
- 「数独」「ナンプレ」のような激戦キーワードで新規アプリが上位に出るのはほぼ不可能
旧アプリが積み上げた資産は、新アプリには1ミリも引き継がれない。
旧アプリには「現役ユーザー」がいる
ここで効くのが、削除済みの旧アプリだ。
ストアから消えても、すでにインストール済みのユーザーのアプリは動き続ける。広告レポートを見ると、旧アプリは月に数万インプレッションを叩き出していた。数百人規模が今も遊んでいる。
この人たちに「後継アプリがあるよ」と伝えられれば、検索流入ゼロの問題を一気に飛び越えられる。一番確実な見込み客が、すでに旧アプリの中にいる。
問題は、旧アプリが更新できないこと。普通なら「アプリ内に新アプリへのバナーを追加してアップデート」だが、それができない。
自社広告サーバーという逃げ道
幸い、旧アプリ(Objective-C世代)には自前の広告配信の仕組みが入っていた。
起動時に自社サーバーへ問い合わせ、JSONで「どの広告を出すか」を受け取る方式だ。
https://example.com/ad.php?code=<アプリ識別コード>
レスポンスはこんな形:
{
"ad_type": 10002,
"text1": "ca-app-pub-xxxxx/yyyyy",
"per": 100
}
ad_type で広告の種類を切り替える。値はDB(media / tag テーブル)で管理されていて、サーバー側のレコードを書き換えるだけで配信内容が変わる。アプリ側のコードには一切触らない。
つまり、アプリを更新できなくても、広告枠の中身は今からでも差し替えられる。
HTML広告でバナーを差し込む
ad_type にはAdMob以外にHTML広告のタイプがあった。
10001 = HTML
10002 = AdMob
HTMLタイプを返すと、アプリ側は受け取った文字列を UIWebView に流し込む:
NSString *html = [NSString stringWithFormat:
@"<body style='margin:0;text-align:center;'>%@</body>", adText];
[adWeb loadHTMLString:html baseURL:nil];
ここに新アプリへのリンク付きHTMLを入れればいい。広告枠に自前のバナーを描画できる。
タップでApp Storeが開くか
懸念は「UIWebView 内のリンクをタップしたとき、ちゃんとApp Storeが開くか」だった。小さなWebView内でApp StoreのURLを開こうとすると詰む。
旧アプリの実装を確認したら、ちゃんとデリゲートで処理していた:
- (BOOL)webView:(UIWebView *)webView
shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType
{
if (navigationType == UIWebViewNavigationTypeLinkClicked) {
if ([[UIApplication sharedApplication] canOpenURL:[request URL]]) {
[[UIApplication sharedApplication] openURL:[request URL]];
return NO; // WebView内では読み込まない
}
}
return YES; // 初期表示(loadHTMLString)はそのまま許可
}
リンククリック(UIWebViewNavigationTypeLinkClicked)を検知したら openURL: で外部に投げ、WebView内ロードは return NO で止める。初回の loadHTMLString はクリック扱いではないので return YES で正常に表示される。
App StoreのURL(https://apps.apple.com/app/idXXXX)を渡せば、iOSがApp Storeアプリに引き渡してくれる。狙い通りだった。
古いWebViewでもCSSアニメは動く
UIWebView は古いAPIだが、中身はWebKitだ。CSSアニメーションもトランジションも動く。
せっかくなので、ただの静止バナーではなく、目に留まる演出をつけた:
- アプリアイコンが左右にゆらゆら揺れる(花のモチーフに合わせて)
- バナー全体を斜めの光が定期的に横切る(shine)
- CTAボタンがぷるぷる拡縮する
@keyframes shine {
0% { left: -60%; }
60%, 100% { left: 120%; }
}
.banner::after {
content: '';
position: absolute; top: 0; left: -60%;
width: 40%; height: 100%;
background: linear-gradient(120deg,
transparent, rgba(255,255,255,.35), transparent);
animation: shine 3s ease-in-out infinite;
}
広告枠は 320×50(iPhone)。アイコンを height:100% で枠にフィットさせ、右にテキストとCTAを並べる横長レイアウトにした。
配信割合とA/Bテスト
DBの per カラムは配信割合(重み付け抽選)だ。AdMobと新アプリ誘導を半々にしたいなら:
-- AdMob枠
UPDATE tag SET per=50 WHERE id=<AdMobのタグ>;
-- 新アプリ誘導枠
UPDATE tag SET ad_type=10001, per=50, text1='<バナーHTML>'
WHERE id=<誘導タグ>;
per の合計に対する割合で抽選されるので、50:50 なら半々で配信される。実際に何度もリクエストして比率を確認した。
さらに面白いのは、誘導バナーを複数登録すればA/Bテストになること。表示・クリックはタグ単位でDBに記録されるので、どのコピーが一番刺さるかを後から数字で比較できる。
タグA: 「神ヒントで解き方が学べる」 per=17
タグB: 「もう詰まらない」 per=17
タグC: 「新作・解法ヒントが神レベル」 per=16
3案を均等に配信し、クリック率の高いコピーに寄せていく。広告枠ひとつで小さなマーケティング実験ができる。
やってみた結果(途中経過)
設定して半日で、誘導バナーは800回以上表示された。旧アプリの現役ユーザーがちゃんと見ている。
ただ、クリックは数回どまり。新アプリのダウンロードにはまだ繋がっていない。表示はされるが、コピーがまだ弱いのだと思う。だからこそA/Bテストで「どう言えば刺さるか」を探っている最中だ。
まとめ
| やったこと | ポイント |
|---|---|
| 旧アプリの自社広告サーバーを使う | アプリ更新なしでDB書き換えだけ |
| HTML広告タイプで自前バナーを描画 | loadHTMLString |
| タップでApp Storeを開く | shouldStartLoadWithRequest で openURL: |
| CSSアニメで目立たせる | 古いUIWebViewでもWebKitなので動く |
per で配信割合を制御 | AdMobと混在配信・A/Bテスト |
「アプリが更新できない」は詰みに見えて、配信の仕組みを自前で持っていれば抜け道がある。旧アプリの現役ユーザーは、新アプリにとって一番確実な見込み客だ。検索流入をゼロから積み上げるより、すでにいる人に届けるほうがずっと早い。