個人開発の壁紙アプリを長く運用していると、ユーザーが一番自然に友人へ「いいよ」と勧めてくれる瞬間は、アプリストアのリンクを送るときではなく、気に入った一枚をそのまま会話に貼るときだと気づきます。だとすれば、その共有の場であるメッセージそのものに、アプリの世界観を持ち込めないか。そう考えて取り組んだのが iMessage 拡張でした。
iMessage 拡張は、Messages フレームワークに依存するネイティブの仕組みです。React Native からは事実上扱えませんが、Rork Max はネイティブ Swift のプロジェクトを生成するため、拡張ターゲットを足して MSMessagesAppViewController を継承する道が開けます。ここでは配布導線という観点から、表示の切り替え・メッセージ送信・つまずきどころを順に整理します。
なぜ「アプリ本体」ではなくメッセージ拡張なのか
アプリの紹介をストアのリンクで送ると、受け手はアプリを離れて App Store を開く必要があります。一方、iMessage 拡張から送った素材は会話の中に残り、受け手はその場で軽く触れられます。私の感覚では、後者の方が「使ってみようかな」という心理的な距離が圧倒的に近いです。AdMob 中心の無料アプリにとって、入口を一つ増やす意味は小さくありません。
ただし拡張は本体アプリとは別のターゲットであり、メモリ制限も厳しめです。素材を丸ごと読み込む発想だと、すぐに限界に当たります。軽さを前提に設計するのが出発点になります。
Step 1: 拡張ターゲットを追加し、本体と素材を共有する
Rork Max が生成したプロジェクトに、iMessage 拡張のターゲットを追加します。本体アプリと素材(画像など)を共有するため、App Group を有効化して共有コンテナ越しに読み出す構成にします。拡張内に画像を二重に抱えると、配布サイズもメモリも無駄に膨らみます。
// 共有コンテナから素材の URL を解決する
func sharedAssetURL(_ name: String) -> URL? {
let groupID = "group.net.rorklab.sample"
let base = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: groupID)
return base?.appendingPathComponent("assets/\(name)")
}
App Group の識別子は本体と拡張で完全に一致させる必要があります。ここがずれていると、拡張側で素材が常に nil になり、原因の特定に時間を取られます。
Step 2: コンパクト表示と拡張表示を切り替える
iMessage 拡張には、キーボード位置に収まる「コンパクト」と、画面の大半を使う「拡張」の二つの表示があります。最初はコンパクトで素材の一覧をさっと見せ、選択後に拡張へ広げて仕上げを確認する、という流れが自然です。
import Messages
final class MessagesViewController: MSMessagesAppViewController {
override func willBecomeActive(with conversation: MSConversation) {
super.willBecomeActive(with: conversation)
presentList() // まず一覧を見せる
}
func didSelectAsset(_ name: String) {
// 選択されたら拡張表示へ広げて確認画面を出す
requestPresentationStyle(.expanded)
presentDetail(for: name)
}
}
requestPresentationStyle(_:) は要求であって即時の切り替えではありません。実際の遷移後に UI を組み直すには didTransition(to:) を併用します。ここを混同すると、表示が広がる前に古いレイアウトのまま描画され、一瞬ちらつきます。
Step 3: 素材をメッセージとして組み立てて送る
共有の核になるのが MSMessage の組み立てです。会話に残る吹き出しの見た目は MSMessageTemplateLayout で決めます。受け手の画面に何が残るかを、ここで設計します。
func send(asset name: String, in conversation: MSConversation) {
let layout = MSMessageTemplateLayout()
layout.image = UIImage(contentsOfFile: sharedAssetURL(name)?.path ?? "")
layout.caption = "お気に入りの一枚を送りました"
let message = MSMessage()
message.layout = layout
conversation.insert(message) { error in
if let error = error {
print("insert failed: \(error)") // 送信枠への挿入失敗を握りつぶさない
}
}
}
conversation.insert は、メッセージを「送信枠に挿入する」だけで、実際の送信はユーザーの送信ボタンに委ねられます。ここを「送信した」と誤解すると、勝手に送られないという仕様を不具合と取り違えてしまいます。私は最初それで悩み、挿入と送信が分かれている設計だと理解して腑に落ちました。
Step 4: ドロワーに出てこないときの切り分け
拡張を作って実機に入れたのに、メッセージのアプリドロワーに出てこない——これは iMessage 拡張で頻発する症状です。原因はコードよりも構成側にあることが大半です。
確認する順序は次の通りです。第一に、拡張ターゲットの Bundle Identifier が本体の識別子に正しくぶら下がっているか。第二に、拡張の Info.plist に Messages 拡張としての NSExtensionPointIdentifier が設定されているか。第三に、デバイスのメッセージ設定でアプリが有効化されているか。私の場合、二番目の拡張ポイントの取り違えが原因で、ビルドは通るのにドロワーに現れない状態に陥っていました。
Step 5: 配布効果を測れる形にしておく
拡張からの共有がどれくらい新規につながったかは、後から必ず知りたくなります。送信メッセージに識別用のクエリを付けた URL を MSMessage.url に持たせ、受け手がそこからアプリを開いた経路を本体側で計測できるようにしておきます。私は AdMob 収益と並べて、この共有経由の起動数を簡易に追う形にしました。数字が見えると、拡張に手をかける価値があるかを淡々と判断できます。
個人開発で拡張に投資すべきかの判断
iMessage 拡張は、作り込もうとすれば無限に凝れる領域です。ですが配布導線として見るなら、まずは「気に入った素材を一手で会話に貼れる」ところまでで十分に効果が出ます。私が個人開発で採った方針は、共有の一往復を軽く速く仕上げることに集中し、装飾的な機能は後回しにすることでした。
メッセージは、ユーザーが自分の言葉で誰かを思い浮かべている場所です。そこにアプリの世界観をそっと差し出せること自体に、宣伝以上の意味があると感じています。Rork Max でネイティブの拡張に手が届くようになった今、配布をコードで設計する発想を持てるかどうかが、地味ながら効いてくるはずです。