結論
ボタンを押したらストアページを表示するケースで示す
@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
動きはするが望ましくないコード
- (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
やloadProductWithParameters
で検索した際に上位に出てくるページをベースにした。
まず[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を貼って確認してみてほしい。
- (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に入ることなく、音沙汰がなかった。
加えて上述の解釈にもとづいて実装すれば、以下のようになるだろう。
- (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だけ無効にしたコードだ。
- (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の挙動が改善されたわけでもない。
ということで、結論のコードに至る。
- (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外の変数を参照せず、ネストも浅く書くことができる。