適切なSKStoreProductViewControllerの使い方

# 結論

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

```objc: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

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

```objc: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);
             }
         }];
    }];
}
```

- [SKStoreProductViewControllerでハマったこと](http://hmdt.jp/blog/?p=635)
- [アプリ内でAppStoreを表示する方法](http://qiita.com/Night___/items/4d418305106b7a7d56ea)
- [アプリ内にAppStoreをだいたい半分だけ表示してみる](http://qiita.com/ta__ho/items/46571c310708b4033784)

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

### 何が望ましくないのか

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

> ちなみにドキュメントの方を確認すると、このメソッドの使い方としてこう書いてある。
>> In most cases, you should load the product information and then present the view controller.
>> （多くの場合、プロダクト情報を読み込んでから、ビューコントローラを表示するべきでしょう）

引用元: [SKStoreProductViewControllerでハマったこと](http://hmdt.jp/blog/?p=635)

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

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

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

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

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

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

> これ、そのまんまの通りに解釈すれば、loadProductWithParameters:を呼んで、それが成功したらpresentViewController:する、って思うでしょ。始めそれでやってみたんだけど、それだとアクセスに失敗したときにエラーも何も返ってこない。completion blockがまったく呼ばれないの。最初に実験したとき、間違ったプロダクトIDを与えていて、それでなんの音沙汰もなし。
>
> なんでじゃー、といろいろいじくっていたら、presentViewController:を呼んでおかないと、エラーのときのcompletion blockは呼ばれなかった。ということで、なにはともあれpresentViewController:することにした。でもそれなら、ドキュメントに書いてある事まぎらわしくねー？

引用元: [SKStoreProductViewControllerでハマったこと](http://hmdt.jp/blog/?p=635)


上記のうち、

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

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


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

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

```objc: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に入ることなく、音沙汰がなかった。
加えて上述の解釈にもとづいて実装すれば、以下のようになるだろう。

```objc: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だけ無効にしたコードだ。

```objc: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`が呼ばれ、`result`に`NO`が与えられ、`error`にもインスタンスが存在した。

##### 答えは部分的にYES

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

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

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

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

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

```objc: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外の変数を参照せず、ネストも浅く書くことができる。