個人開発のアプリが少しずつ海外で使われ始めたとき、最初に恥ずかしい思いをしたのが課金画面でした。日本で作ったまま「月額 480 円」と数字を直接書いていたため、米国のユーザーには通貨記号も金額もちぐはぐに見えていたのです。App Store は国ごとに価格を持っているのに、アプリの表示がそれを無視していました。
Rork Max が生成するのはネイティブの Swift アプリなので、課金は StoreKit 2 で素直に書けます。そして StoreKit 2 を正しく使えば、価格の表示は Apple に任せられます。ここでは、価格をハードコードしないペイウォールの作り方を起点に、年額の「お得感」を通貨ごとに破綻なく出す計算や、本番で必ず遭遇する地域価格・為替のズレへの向き合い方まで整理します。
なぜ価格を自分で書いてはいけないのか
StoreKit 2 の Product は、ユーザーのストアフロントに応じた価格と通貨をすでに知っています。product.displayPrice は「¥480」「$3.99」「€4,49」のように、その地域の表記・記号・桁区切りまで整った文字列を返します。これを自分で "¥\(price)" のように組み立てた瞬間、ヨーロッパの小数点(カンマ区切り)や通貨記号の位置がすべて壊れます。
私はここで一度痛い目を見ました。価格を数値でハードコードしていたため、Apple 側で地域価格を調整したのに、アプリの表示は古いままという二重管理に陥ったのです。原則はひとつ、価格に関する文字列は StoreKit が返すものだけを表示する、です。
Step 1: 商品を読み込み、displayPrice をそのまま出す
まず商品を取得します。プロダクト ID は App Store Connect で設定したものを使います。
import StoreKit
@MainActor
final class PaywallModel : ObservableObject {
@Published var products: [Product] = []
func load () async {
do {
// ID は App Store Connect で定義したもの
let ids = [ "pro_monthly" , "pro_yearly" ]
let fetched = try await Product. products ( for : ids)
// 月額→年額の順に安定ソート
products = fetched. sorted { $0 .price < $1 .price }
} catch {
print ( "product load failed: \( error ) " ) // 本番では UI に再試行導線を出す
}
}
}
表示側では displayPrice をそのまま使います。
ForEach (model.products) { product in
VStack ( alignment : .leading) {
Text (product.displayName)
Text (product.displayPrice) // ← 自前整形しない。これが全通貨対応の核
. font (.title2). bold ()
}
}
この displayPrice だけで、円・ドル・ユーロ・ウォン、どの地域から見ても正しい表記になります。自分のコードに通貨記号が一文字も出てこないのが、正しく書けている合図です。
Step 2: 年額が「何%お得か」を通貨非依存で計算する
サブスクの定番訴求は「年額なら月額の何ヶ月分が無料」「年額は約 N% お得」です。ここで金額の文字列をいじってはいけません。割引率は product.price(Decimal の数値)から計算し、表示の整形だけ別に行います。
extension PaywallModel {
/// 年額が月額×12 に対して何%安いかを返す
func yearlyDiscountPercent ( monthly : Product, yearly : Product) -> Int {
let monthlyYear = monthly.price * 12
guard monthlyYear > 0 else { return 0 }
let saved = (monthlyYear - yearly.price) / monthlyYear
// Decimal → パーセント整数
return Int ((saved * 100 as Decimal). rounded ( 0 , . down ). description ) ?? 0
}
}
price は通貨の数値そのものなので、円でもドルでも割合は同じロジックで出せます。たとえば月額 ¥480・年額 ¥3,800 なら、(480×12 − 3800) / (480×12) ≒ 34% となり、米国で月額 $3.99・年額 $29.99 でも (3.99×12 − 29.99) / (3.99×12) ≒ 37% と、各地域の実際の価格に基づいた割引率が出ます。固定で「30% お得」と書いてしまうと、地域価格を調整した途端に嘘になります。
Step 3: 文言そのものはローカライズファイルに分ける
価格は StoreKit に任せますが、「お得」「無料トライアル」「いつでも解約可能」といった文言はアプリ側でローカライズします。価格と文言を混ぜて一つの文字列に組み立てると、語順が言語で変わったときに破綻します。
// String Catalog (Localizable.xcstrings) のキーを使う
Text ( "paywall.yearly.save \( model. yearlyDiscountPercent ( ... ) ) " )
// en: "Save %d%% with yearly"
// ja: "年額で %d%% お得"
英語と日本語では「%」と語順の扱いが違うため、%d%% のプレースホルダを使い、数値だけを差し込みます。私はここを横着して日本語前提の固定文に数字を足していたため、英語版で「34% Save yearly」のような不自然な並びになり、レビューで指摘されました。
Step 4: 無料トライアルの有無も地域で変わる
導入オファー(無料トライアルや初回割引)は、StoreKit 2 では product.subscription?.introductoryOffer から取れます。これも地域やユーザーの購入履歴で有無が変わるため、決め打ちで「7日間無料」と書いてはいけません。
if let intro = product.subscription ? .introductoryOffer,
intro.paymentMode == .freeTrial {
let days = intro.period.localizedDays // 期間も StoreKit の値から出す
Text ( "paywall.trial \( days ) " ) // ja: "%@日間無料でお試し"
} else {
Text ( "paywall.subscribe" ) // トライアル非対象ユーザー向け
}
トライアル対象でないユーザー(過去に使い切った人など)に「無料」と見せると、購入後に「無料のはずでは」というクレームと返金につながります。オファーの有無は必ず実データで分岐させます。
本番で「価格が合わない」と言われたとき
リリース後にときどき届くのが「表示価格と請求額が違う」という声です。私はまず、ユーザーのストアフロント(国・地域)を確認します。VPN や Apple ID の国設定で、本人が思っているのと別の地域価格を見ていることが大半です。次に、Apple が地域価格を改定した直後でないかを見ます。為替が大きく動くと、Apple は世界の価格表(Price Tier 相当)を更新し、表示が変わります。アプリ側は displayPrice を出しているだけなので、これは「バグ」ではなく Apple の価格改定が反映された結果です。この切り分けができていないと、コードを疑って消耗します。
収益設計の観点では、私は無料機能を AdMob で広く支え、サブスクは「広告を消す+詳細機能」をまとめた一つの軸に絞ることを推奨しています。プランを通貨ごとに細かく刻むより、StoreKit に価格表示を任せて月額・年額の二択をきれいに見せる方が、海外ユーザーの離脱が少なく感じています。
ロード失敗時に空のペイウォールを出さない
本番でいちばん収益を削るのは、価格表示の崩れよりも「商品が読み込めずボタンが消えたペイウォール」です。Product.products(for:) はネットワークやストアの一時障害で空配列を返すことがあり、そのまま描画すると課金導線が丸ごと消えます。私はここで、初回ロード失敗時に1回だけ自動リトライし、それでも空なら「読み込みに失敗しました・再試行」を出す設計を推奨します。
func loadWithRetry () async {
await load ()
if products. isEmpty {
try? await Task. sleep ( for : . seconds ( 2 ))
await load () // 一時的な失敗は1回の再試行で大半が解決する
}
}
地味ですが、この一手でペイウォールの表示成功率が体感で改善し、無言の機会損失が減りました。価格を正しく出すことと同じくらい、課金画面を「必ず出し切る」ことが収益に効きます。
次の一手
まず、いまの課金画面のコードを開いて、通貨記号や金額が文字列として直接書かれている箇所を一つ残らず探してください。それを全部 displayPrice と割引率の計算に置き換えるだけで、対応通貨は一気に世界中へ広がります。私自身、この置き換えを終えてから海外比率がじわじわ伸びました。最初の一歩は、ハードコードされた数字をコードから消すことです。