7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

適切なSKStoreProductViewControllerの使い方

Last updated at Posted at 2016-12-06

結論

ボタンを押したらストアページを表示するケースで示す

answer
@interface ViewController () <SKStoreProductViewControllerDelegate>
@end

@implementation ViewController

- (IBAction)openStore:(UIButton *)sender {
    SKStoreProductViewController* store = [SKStoreProductViewController new];
    store.delegate = self;

    NSNumber* validItunesItemId = @(/* valid number */);
    NSDictionary* param = @{SKStoreProductParameterITunesItemIdentifier: validItunesItemId};
    [store loadProductWithParameters:param
                     completionBlock:^(BOOL result, NSError * _Nullable error) 
    {
        NSLog(@"complete load store");
        if (error) {
            NSLog(@"%@", error.description);
        }
    }];
    [self presentViewController:store animated:YES completion:^{
        NSLog(@"presented view controller");
    }];
}

- (void)productViewControllerDidFinish:(SKStoreProductViewController *)viewController {
    [viewController dismissViewControllerAnimated:YES completion:^{
        NSLog(@"close store viewController");
    }];
}

@end

以下、読み物。

調査した

経緯

アプリ内からAppStoreページを呼ぶためにSKStoreProductViewControllerの使い方を調べることになった。
Appleのリファレンスには大して使い方が載っておらず、いくつかWebサイトを巡ってみたところ、どうも使い方にしっくり来なかったので調査した。

環境

  • OS X El Capitan (10.11.6)
  • Xcode 8.0

動きはするが望ましくないコード

code-1
- (IBAction)openStorePatternTwo:(UIButton *)sender {
    SKStoreProductViewController *store = [[SKStoreProductViewController alloc] init];
    store.delegate = self;
    
    [self presentViewController:store animated:YES completion:^() {
        NSNumber* validItunesItemId = @(/* valid number */);
        NSDictionary* param = @{SKStoreProductParameterITunesItemIdentifier: validItunesItemId};

        [store loadProductWithParameters:param
                         completionBlock:^(BOOL result, NSError *error)
         {
             NSLog(@"complete load store");
             if (error) {
                 NSLog(@"%@", error.description);
             }
         }];
    }];
}

コードはSKStoreProductViewControllerloadProductWithParametersで検索した際に上位に出てくるページをベースにした。
まず[self presentViewController:animated:completion:]にてSKStoreProductViewControllerを表示させ、完了してからストアページのロードを始める。というもの。

何が望ましくないのか

1. ドキュメントに沿っていない

ちなみにドキュメントの方を確認すると、このメソッドの使い方としてこう書いてある。

In most cases, you should load the product information and then present the view controller.
(多くの場合、プロダクト情報を読み込んでから、ビューコントローラを表示するべきでしょう)

引用元: SKStoreProductViewControllerでハマったこと

上記の例だと、ビューコントローラを表示してからプロダクト情報を読み込んでいる。

2. Block外の変数を参照するのは気を使う

これがとても気疲れする。下手をうつといつ循環参照に陥ったりするので疲れる。
なるべくblockの引数で、せいぜいweak selfで処理を解決してほしい。

3. ネストを浅くできるなら浅くすべき

これは深く説明できると良かったのだが、思慮が足らず思想・心情の領域。

なぜそのように書かなかったのか

これ、そのまんまの通りに解釈すれば、loadProductWithParameters:を呼んで、それが成功したらpresentViewController:する、って思うでしょ。始めそれでやってみたんだけど、それだとアクセスに失敗したときにエラーも何も返ってこない。completion blockがまったく呼ばれないの。最初に実験したとき、間違ったプロダクトIDを与えていて、それでなんの音沙汰もなし。

なんでじゃー、といろいろいじくっていたら、presentViewController:を呼んでおかないと、エラーのときのcompletion blockは呼ばれなかった。ということで、なにはともあれpresentViewController:することにした。でもそれなら、ドキュメントに書いてある事まぎらわしくねー?

引用元: SKStoreProductViewControllerでハマったこと

上記のうち、

loadProductWithParameters:を呼んで、それが成功したらpresentViewController:する

この解釈に則った実装が望ましくないコードを産んだとみている。
とは言え、読み込みを始めてからなのか、完了してからなのかを明記していないので、このloadがどの状況にあるのか曖昧に感じる。
きっとドキュメント筆者の暗黙知の想定と行間を読む必要があるのだろう。日本語かな?

ストアページにアクセスできない場合の挙動を確認する

いったいどのような現象に遭遇したのかを追体験する。
手元に環境があれば、以下のコードのNSLog(@"complete load store");あたりにBreak pointを貼って確認してみてほしい。

code-2
- (IBAction)cantOpen:(UIButton *)sender {
    SKStoreProductViewController* store = [SKStoreProductViewController new];
    store.delegate = self;
    NSDictionary* param = @{SKStoreProductParameterITunesItemIdentifier: @(/* invalid number */)};
    
    [store loadProductWithParameters:param
                     completionBlock:^(BOOL result, NSError * _Nullable error)
     {
         NSLog(@"complete load store");
         if (error) {
             NSLog(@"%@", error.description);
         }
     }];
}

たしかにcompletetionBlockに入ることなく、音沙汰がなかった。
加えて上述の解釈にもとづいて実装すれば、以下のようになるだろう。

code-3
- (IBAction)cantOpen:(UIButton *)sender {
    SKStoreProductViewController* store = [SKStoreProductViewController new];
    store.delegate = self;
    NSDictionary* param = @{SKStoreProductParameterITunesItemIdentifier: @(/* invalid number */)};
    
    __weak ViewController* wself = self;
    [store loadProductWithParameters:param
                     completionBlock:^(BOOL result, NSError * _Nullable error)
     {
         NSLog(@"complete load store");
         if (error) {
             NSLog(@"%@", error.description);
         }
         // 表示処理を追加した
         [wself presentViewController:store animated:YES completion:^{
             NSLog(@"presented view controller");
         }];
     }];
}

これではうんともすんとも言わないのは想像に難くない。

presentすればcompletionBlockは呼ばれるのか

上述の引用文に気になる文言があった。

presentViewController:を呼んでおかないと、エラーのときのcompletion blockは呼ばれなかった。

presentしていれば問題のある接続時にもcompletion blockが呼ばれるという。試してみよう。
以下はcode-1をベースにストアIDだけ無効にしたコードだ。

code-4
- (IBAction)cantOpenPatternTwo:(UIButton *)sender {
    SKStoreProductViewController *store = [[SKStoreProductViewController alloc] init];
    store.delegate = self;
    
    [self presentViewController:store animated:YES completion:^() {
        NSDictionary* param = @{SKStoreProductParameterITunesItemIdentifier: @(/* invalid number */)};
        
        [store loadProductWithParameters:param
                         completionBlock:^(BOOL result, NSError *error)
         {
             NSLog(@"complete load store");
             if (error) {
                 NSLog(@"%@", error.description);
             }
         }];
    }];
}

NSLog(@"complete load store");にbreak pointを貼って待ってみたが、completionBlockが呼ばれることはなかった。

唯一呼ばれるケース

それはキャンセルボタンを押したときだった。
キャンセルボタンを押したときだけ、completionBlockが呼ばれ、resultNOが与えられ、errorにもインスタンスが存在した。

答えは部分的にYES

つまりはストア情報の読み込みを待たずに、presentViewControllerを実行することで、少なくともキャンセルボタンを押せるようにした。
結果、result==NOかつerror!=nilcompletionBlockが呼ばれるケースを発生させることができた。

しかし、presentViewControllerを実行したことで、問題のある接続は確実にcompletionBlockで呼ばれるようになったかというと、NOである。

わざわざcompletionを待たなくていいじゃないか

code-4を見ると、ストア情報の読み込み状況にかかわらず、ストアページを画面に表示していた。
そして問題のある接続時のcompletionBlockの挙動が改善されたわけでもない。

ということで、結論のコードに至る。

code-5
- (IBAction)openStore:(UIButton *)sender {
    SKStoreProductViewController* store = [SKStoreProductViewController new];
    store.delegate = self;

    NSNumber* validItunesItemId = @(/* valid number */);
    NSDictionary* param = @{SKStoreProductParameterITunesItemIdentifier: validItunesItemId};
    [store loadProductWithParameters:param
                     completionBlock:^(BOOL result, NSError * _Nullable error) 
    {
        NSLog(@"complete load store");
        if (error) {
            NSLog(@"%@", error.description);
        }
    }];
    [self presentViewController:store animated:YES completion:^{
        NSLog(@"presented view controller");
    }];
}

これならば、ドキュメントに沿って、Block外の変数を参照せず、ネストも浅く書くことができる。

7
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?