プロダクト情報の取得

購入プロセスの最初の段階では、AppがApp Storeからプロダクトに関する情報を取得し、ユーザーに対してストアUIを表示した後、ユーザーがプロダクトを選択します(図 2-1を参照)。

図 2-1 購入プロセスの各段階—ストアUIの表示

プロダクトIDのリストの取得

Appで販売するすべてのプロダクトには、一意のプロダクトIDがあります。AppではこのプロダクトIDを使用して、価格などのプロダクトに関する情報をApp Storeから取得し、ユーザーがプロダクトを購入すると支払い要求を送信します。AppはプロダクトIDのリストをAppにバンドルされるファイルから読み込むか、またはサーバからフェッチします。表 2-1に、2つのやり方についての相違点をまとめてあります。

表 2-1 プロダクトIDを取得する方法の比較

Appバンドルに埋め込み

サーバからフェッチ

購入目的

機能のアンロック

コンテンツ配信

プロダクトのリストの変更

Appの更新時に可能

いつでも可能

サーバの必要性

不可

Appのプロダクトリストが、広告の削除や機能の有効化など固定されている場合は、Appバンドルにリストを埋め込みます。追加のレベルやキャラクターをサポートするゲームのように、プロダクトIDのリストがAppの更新しなくても変更できるようにするには、サーバからリストをフェッチするようにします。

特定のAppに対してApp Store Connectで設定されているすべてのプロダクトのリストをフェッチする実行時のメカニズムはありません。Appは、プロダクトリストの管理と、この情報のAppへの提示に対応します。多数のプロダクトを管理する必要がある場合は、App Store ConnectのXMLの一括アップロード/ダウンロード機能を使用することを検討します。

Appバンドル内へのプロダクトIDの埋め込み

Appバンドルに次のようなプロダクトIDの配列を含んだプロパティリストのファイルを含めます。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
 "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
    <string>com.example.level1</string>
    <string>com.example.level2</string>
    <string>com.example.rocket_car</string>
</array>
</plist>

プロパティリストからプロダクションIDを取得するには、Appバンドルでファイルを探して、これを読み込みます。

NSURL *url = [[NSBundle mainBundle] URLForResource:@"product_ids"
                                     withExtension:@"plist"];
NSArray *productIdentifiers = [NSArray arrayWithContentsOfURL:url];

サーバからのプロダクトIDのフェッチ

プロダクトIDを含んだJSONファイルをサーバ上でホストします。以下に例を挙げます。

[
    "com.example.level1",
    "com.example.level2",
    "com.example.rocket_car"
]

サーバからプロダクトIDを取得するには、リスト 2-1に示すように、JSONファイルをフェッチして読み込みます。旧版を破壊することなく、Appの将来の版の構造を変更できるよう、JSONファイルのバージョン管理を考慮してください。たとえば、古い構造を使用するファイルにproducts_v1.json、新しい構造を使用するファイルにproducts_v2.jsonという名前を付けます。これは、使用するJSONファイルが例の単純な配列よりも複雑な場合に特に有益です。

リスト 2-1 サーバからのプロダクトIDのフェッチ

- (void)fetchProductIdentifiersFromURL:(NSURL *)url delegate:(id)delegate
{
    dispatch_queue_t global_queue =
           dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(global_queue, ^{
        NSError *err;
        NSData *jsonData = [NSData dataWithContentsOfURL:url
                                                  options:NULL
                                                    error:&err];
        if (!jsonData) { /* Handle the error */ }
 
        NSArray *productIdentifiers = [NSJSONSerialization
            JSONObjectWithData:jsonData options:NULL error:&err];
        if (!productIdentifiers) { /* Handle the error */ }
 
        dispatch_queue_t main_queue = dispatch_get_main_queue();
        dispatch_async(main_queue, ^{
           [delegate displayProducts:productIdentifiers]; // カスタムメソッド
        });
    });
}

NSURLConnectionを使用したファイルのダウンロードの詳細については、NSURLConnectionの使用(『URL Session Programming Guide』内)を参照してください。

Appが反応し続けるためには、JSONファイルのダウンロードとプロダクトIDリストの抽出にバックグラウンドスレッドを使用します。データの転送量を最小化するには、Last-ModifiedヘッダやIf-Modified-Sinceヘッダなど、HTTPの標準キャッシュメカニズムを使用します。

プロダクト情報の取得

ユーザーが実際に購入できるプロダクトのみが表示されることを確認するには、AppのストアUIを表示する前にApp Storeに問い合わせます。

App Storeに問い合わせるには、プロダクト要求オブジェクトを使用します。最初に、SKProductsRequestのインスタンスを作成し、プロダクトIDのリストで初期化します。要求オブジェクトへの強い参照を保持してください。そうしないと、要求が完了しないうちに、システムが割り当て解除してしまうおそれがあります

プロダクト要求により無効なプロダクトIDのリストとともに有効なプロダクトについての情報が取得されます。次にオブジェクトのデリゲートを呼び出して結果を処理します。App Storeからの応答を処理するために、デリゲートではSKProductsRequestDelegateプロトコルを実装している必要があります。リスト 2-2に、両方のコードの一部の簡単な実装を示します。

リスト 2-2 プロダクト情報の取得

// カスタムメソッド
- (void)validateProductIdentifiers:(NSArray *)productIdentifiers
{
    SKProductsRequest *productsRequest = [[SKProductsRequest alloc]
                    initWithProductIdentifiers:[NSSet setWithArray:productIdentifiers]];
 
    // リクエストの強い参照を保持。
    self.request = productsRequest;
    productsRequest.delegate = self;
    [productsRequest start];
}
 
// SKProductsRequestDelegateプロトコルメソッド
- (void)productsRequest:(SKProductsRequest *)request
     didReceiveResponse:(SKProductsResponse *)response
{
    self.products = response.products;
 
    for (NSString *invalidIdentifier in response.invalidProductIdentifiers) {
        // 無効なプロダクトIDの処理
    }
 
    [self displayStoreUI]; // カスタムメソッド
}

ユーザーがプロダクトを購入すると、デリゲートに返されるプロダクトオブジェクトの配列に対する参照を維持するために、支払い要求の作成に対応するプロダクトオブジェクトが必要になります。Appで販売するプロダクトのリストを変更できる場合は、サーバからフェッチする画像や説明のテキストなどの他の情報とともに、プロダクトオブジェクトへの参照をカプセル化したカスタムクラスを作成する場合があります。支払い要求については、「支払いの要求」で説明されています。

無効として返されたプロダクトIDは、AppのプロダクトIDのリストにエラーがあることを表している場合がほとんどですが、App Store Connectでプロダクトが適切に設定されていないことを意味している場合もあります。この種の問題をより容易に解決するには、適切なログ記録と適切なUIが重要になります。本番用のビルドでは、Appはエラーに「賢く」対処する必要があります。このことは通常、Appの残りのストアUIを表示しながら、無効なプロダクトを省略することを意味しています。開発用のビルドでは、問題に対する注意を喚起するために、エラーを表示します。本番用ビルド、開発用ビルドの両方に対して、無効なIDについて記録を残すために、NSLogを使用してコンソールにメッセージを書き込みます。Appがサーバからリストをフェッチしてくる場合は、無効なIDのリストをAppからサーバに送信するためにログを記録するメカニズムを定義することもできます。

AppのストアUIの表示

AppのストアのデザインはApp内での販売に大きく影響するため、これを適切に行うために時間と労力を割くことには意義があります。Appのほかの部分と統合されるように、ストアのUIをデザインします。StoreKitではストアUIを提供することはできません。プロダクトを最高の条件で展示し、Appのほかの部分とシームレスに連携するストアUIをデザインできるのは、Appとそのコンテンツを熟知しているAppの作成者だけです。

AppのストアUIをデザインするときには、次のガイドラインを考慮してください。

ユーザーが支払いを行える場合にのみストアを表示します。ユーザーが支払いを行えるかどうかを判断するには、SKPaymentQueueクラスのcanMakePaymentsクラスメソッドを呼び出します。ユーザーが(たとえば保護者による制限のために)支払いを行えない場合は、ストアは使用できないことを示すUIを表示するか、UIのストア部分全体を省略します。

Appのフローの中で自然にプロダクトを表示します。AppのストアUIを表示するのに最適な場所をUIの中で見つけます。ユーザーが使用できるコンテキストでプロダクトを表示します。たとえば、ユーザーがプレミアム機能を使用しようとしたときに機能をアンロックできるようにします。ユーザーがAppに初めて触れるときの体験に注意を払ってください。

楽しく簡単に体験ができるようにプロダクトを構成します。Appのプロダクトが多すぎなければ、すべての1つの画面に表示して構いません。そうでない場合、プロダクトをグループやカテゴリに分け、ナビゲートしやすくします。号数の多いコミック本リーダーや雑誌など、多数のプロダクトがあるAppでは、ユーザーが購入する新規アイテムを見つけやすいインターフェイスが特に有用です。プロダクト間の違いを明記するため、それぞれに異なる名前や外観を設定します。必要な場合、明示的な比較を記載します。

ユーザーにとってのプロダクトの価値を伝えます。ユーザーは購入しようとしているものについて正確に知りたいと考えています。プロダクトの価格や説明といったApp Storeからの情報を、プロダクトの画像やデモといったサーバまたはAppバンドルからの情報と組み合わせます。購入前はプロダクトでの体験を制限します。たとえば、ユーザーに新しいレースカーを購入してもらうゲームでは、新しいレースカーで1周テストできるようにします。同様に、追加のブラシを購入してもらうお絵描きAppでは、小さな画面で新しいブラシで試し描きして違いを確認できるようにします。この種のデザインにより、ユーザーはプロダクトを体験する機会を得て、確信を持って購入できるようになります。

App Storeによって返されるロケールと通貨を使用して価格を明示します。プロダクトを見つけやすく、説明を読みやすくします。ユーザーのロケールと価格と価格のロケールが異なっている場合であっても、異なる通貨間の変換をUI内で行わないようにしてください。たとえば、米国にいるユーザーが(長さの単位と英国式の日付の形式を使用するために)United Kingdomのロケールを設定する場合を考慮します。Appは、United Kingdomのロケールに従ってUIを表示しますが、プロダクトの情報はApp Storeで指定されたロケールで表示する必要があります。インターフェイスのほかの部分に合わせてUnited Kingdomのロケールに合わせて価格を英国ポンドに変換すると、不正確になることがあります。ユーザーは米国でApp Storeアカウントを持ち、米ドルで支払うため、Appでは価格が米ドルで表示されます。同様に、Appでも米ドルで価格が表示されます。リスト 2-3に、プロダクトのロケール情報を使用して価格を正しくフォーマットする方法を示します。

リスト 2-3 プロダクト価格のフォーマット

NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
[numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
[numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
[numberFormatter setLocale:product.priceLocale];
NSString *formattedPrice = [numberFormatter stringFromNumber:product.price];

ユーザーによる購入するプロダクトの選択後、Appはプロダクトの支払いを要求するためにApp Storeに接続します。

推奨されるテスト手順

正しく実装されていることを検証するために、コードの各部分をテストします。

テスト用アカウントによるApp Storeへのサインイン

テスト用ユーザーアカウントの生成』で説明しているように、App Store Connectでテスト用のユーザーアカウントを作成します。

開発用のiOSデバイスの「設定(Settings)」で、App Storeからサインアウトします。次にXcodeでAppをビルドして実行します。

開発用のmacOSデバイスで、Mac App Storeからサインアウトします。次にXcodeでAppをビルドして、Finderから起動します。

Appを使用して、App内で購入を行います。App Storeにサインインするメッセージが表示されたら、テスト用アカウントを使用します。このメッセージの一部に“[Environment: Sandbox]”というテキストが表示されるのは、テスト環境に接続されていることを示しています。

“[Environment: Sandbox]”というテキストが表示されない場合は、本番用の環境を使用していることを示しています。Appの開発用に署名されたビルドを実行していることを確認してください。本番用に署名されたビルドは本番用の環境を使用します。

プロダクトIDリストのフェッチテスト

プロダクトIDがAppに埋め込まれている場合は、コードにブレークポイントを設定した後でコードをロードし、NSArrayのインスタンスにプロダクトIDのリストが予想通りに格納されていることを確認します。

プロダクトIDがサーバからフェッチされる場合は、JSONファイルを手動で(SafariなどのWebブラウザ、またはcurlなどのコマンドラインユーティリティを使用して)フェッチして、サーバから返されるデータにプロダクトIDのリストが予想通りに格納されていることを確認します。また、サーバにHTTPの標準キャッシュメカニズムが正しく実装されていることを検証します。

無効なプロダクトIDの処理のテスト

AppのプロダクションIDのリストに意図的に無効なIDを設定します(テスト後に削除するのを忘れないようにしてください)。

本番用のビルドで、AppにストアUIの無効なID以外の部分が表示され、ユーザーがプロダクトを購入できることを確認します。開発用ビルドで、Appの問題について注意が喚起されることを確認します。

コンソールログをチェックして、無効なプロダクトIDが正しく識別されていることを確認します。

プロダクト要求のテスト

テストするプロダクトIDのリストを使用して、SKProductsRequestのインスタンスを作成して送信します。コードにブレークポイントを設定し、プロダクトIDが有効なリストと無効なリストを検査します。無効なプロダクトIDがあった場合は、App Store Connectでプロダクトを確認して、JSONファイルまたはプロパティリストを修正します。