値上げの同意を求めるシートが、新規ユーザーのオンボーディングの3画面目で突然せり上がってきたことがありました。私自身、個人開発で課金のあるアプリをいくつか運用していますが、この割り込みが起きると、その回のオンボーディング完了率がはっきり落ちます。まだ価値を感じる前の人に「価格に同意してください」と迫っているのですから、当然かもしれません。
App Store のシステムメッセージ(値上げ同意・請求エラー・ウィンバックなど)は、既定ではアプリのフォアグラウンド復帰時に StoreKit が勝手に表示します。出すこと自体は避けられませんが、いつ出すかは開発側で握れます。StoreKit 2 の Message を購読し、表示の引き金を自分のタイミングで引く設計をまとめます。Rork Max が生成するネイティブ Swift を前提にしていますが、考え方は react-native-iap などをブリッジする場合にもそのまま使えます。
既定の挙動と、握れるポイント
理解の起点は「メッセージは保留できる」という一点です。StoreKit.Message.messages という非同期シーケンスを購読しておくと、システムが出そうとしたメッセージがアプリに流れてきます。ここで何もしなければ自動表示に任せることになりますが、シーケンスを受け取った時点で for await のループに入っていれば、表示は message.display(in:) を自分で呼ぶまで起きません。
つまり「購読する」イコール「自動表示を止めて、表示権をアプリ側に移す」ことになります。受け取ったメッセージはアプリが明示的に出すまで保持され、出さなければ次回起動時にまた届きます。取りこぼしの心配は要りませんが、永遠に出さないのは規約・売上の両面で不利なので、必ずどこかで出し切る設計にします。
メッセージの購読を起動時に1本だけ張る
購読はアプリ全体で1本に保ちます。複数箇所で for await を回すと、同じメッセージが二重に処理されかねません。@MainActor のコーディネーターを1つ用意し、起動直後にループを開始します。
import StoreKit
import SwiftUI
@MainActor
final class StoreMessageCoordinator: ObservableObject {
/// 表示待ちのメッセージ(届いた時刻つき)
private(set) var pending: [(message: Message, since: Date)] = []
/// いま表示してよいか(画面状態で更新する)
var canPresent: Bool = false { didSet { flushIfPossible() } }
private var listenTask: Task<Void, Never>?
func start() {
guard listenTask == nil else { return }
listenTask = Task { [weak self] in
for await message in Message.messages {
// ここで display を呼ばない限り、システムは自動表示しない
self?.enqueue(message)
}
}
}
private func enqueue(_ message: Message) {
// 値上げ同意と請求エラーは売上・規約に直結するので、保留が長引かないよう先頭へ
switch message.reason {
case .priceIncreaseConsent, .billingIssue:
pending.insert((message, Date()), at: 0)
default:
pending.append((message, Date()))
}
flushIfPossible()
}
private func flushIfPossible() {
guard canPresent, let scene = Self.activeScene() else { return }
let next = pending
pending.removeAll()
for item in next {
do { try item.message.display(in: scene) }
catch { pending.append(item) } // 出せなければ戻して次の機会に
}
}
private static func activeScene() -> UIWindowScene? {
UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.first { $0.activationState == .foregroundActive }
}
}canPresent を画面状態に応じて切り替えるだけで、表示タイミングを完全に制御できます。ポイントは display(in:) が UIWindowScene を要求することです。シーンがまだ前面でない瞬間に呼ぶと失敗するので、foregroundActive なシーンが取れたときだけ出します。