忍者ブログ
  • 2024.10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 2024.12
[PR]
×

[PR]上記の広告は3ヶ月以上新規記事投稿のないブログに表示されています。新しい記事を書く事で広告が消えます。

【2024/11/24 03:43 】 |
JNIで不具合起きたら
以下のようにして例外を出力できる

if (methodInfo.env->ExceptionCheck()) {
methodInfo.env->ExceptionDescribe();
}



また、ポインタ変数を確認したいときは以下のようにすればよい。

jstring jarg = methodInfo.env->NewStringUTF(arg.c_str());
jobject objResult = methodInfo.env->CallStaticObjectMethod(methodInfo.classID , methodInfo.methodID, jarg);
printf("classId=%p, jarg=%p, objResult=%p", methodInfo.classID, jarg, objResult);



辿ってみるとわかるが、jstringとかjobjectは全部
_jobject
クラスの子孫になっている。



PR
【2015/05/25 13:19 】 | Android | 有り難いご意見(0)
androidでtwitter投稿機能
参考ページは以下。

http://qiita.com/gabu/items/673288c3a5b39f89aa92

twitterのアプリケーション登録ページは以下
https://apps.twitter.com/app


基本的に参考ページのコードを書き写せば良いが、
気をつける点いくつか。


・CallbackURLも設定する
CallbackURLとは、OAuth認証が済んだ後に戻る先のURLである。
androidアプリの場合設定は不要なのだが、(コードから設定するので)何かしら書いておく必要がある。何も書いていないと、コールバック自体をしなくなってしまうため。
何でもいいから適当に書いておく

マニフェストのTwitterOAuthActivityの中で、
コールバックURLをhost,schemeの形で指定する。
これが
scheme://host
の形でコールバックURLとして使われることになる。(スキームが先)






【2015/05/24 15:12 】 | Android | 有り難いご意見(0)
multiple dex files define エラー
以下のようなエラーが出た。


Unable to execute dex: Multiple dex files define ****;
Conversion to Dalvik format failed: Unable to execute dex: Multiple dex files define ****;

つまり、同名のファイルが複数存在していることが問題。

今回の場合、
Lcom/android/vending/billing/IInAppBillingService
がmultipleだというエラー。

元々自分でこのファイルを追加していた。
android公式の課金サンプルからコピペした
src/com/android/vending/billing/IInAppBillingService.aidl
をプロジェクトに追加していた。

で、その後
appCCloudのjarファイルを追加したところこのエラー。
appCCloudには課金機能も含まれているので、jar内にIInAppBillingServiceクラスの定義があったのだろう。


◯対処

そこで、自分でファイル追加していたsrc/以下のaidlファイルを削除。
aidlはインターフェース定義だけなので、実体は元々別にあるものなので
ここで削除しても問題ないはず。

で、やってみたらエラーが消えた。

【2015/05/23 18:52 】 | Android | 有り難いご意見(0)
androidでpush通知設定
大前提

google play developer console

google developers console
は別物。
とても紛らわしい


google play developer console
https://play.google.com/


google developers console
https://console.developers.google.com/



0.
google play developer consoleで、アプリを作成
これは済んでいる前提。
アプリ名もストアに並ぶものそのままで書いている。

1.
google developers consoleで、プロジェクトを作成。プロジェクト名はアプリ名と同じが良いだろう。ただし英数字しか使えない

2.
PUSH通知サービスを有効にする。
左タブ「APIと認証」を選択→APIを選択→「Google Cloud Messaging for Android」を有効にする


3.
左タブ「APIと認証」→認証情報から、「公開 API へのアクセス」の「新しいキーを作成」をクリック。
テスト用にIPアドレスは 「0.0.0.0/0」としておく。
すると、「API キー」が発行される。

4.
google play developer console
に移動

5.
アプリの「サービスとAPI」を選択→「GOOGLE クラウド メッセージング(GCM)」を作成。
先ほどの「APIキー」を入力。
すると「リンクしている送信者ID」が作成される
(appCCloud使用の場合は不要の手順)


appCCloudの場合

1.
google developers consoleのみ使用。

appCCloudの管理画面に行き
APIキーに「APIキー」を、
SenderIDに「プロジェクトID」(consoleの「概要」タブから確認)を入力

2.
appc_cloud_x.x.x.jarファイルをプロジェクトに追加

3.
AndroidManifest.xmlにappCCloud周りの必要事項を追記。
管理画面に書き方参考があるので、それを見てそのまま追記すればよい

4.
コード記述。
MainActivity.javaに以下の3行を追記するだけ

import net.app_c.cloud.sdk.AppCCloud; //<-ここ

public class MainActivity extends Cocos2dxActivity {
private AppCCloud appCCloud; //<-ここ
protected void onCreate(final Bundle savedInstanceState) {
appCCloud = new AppCCloud(this).on(AppCCloud.API.PUSH).start(); //<-ここ
}
}




5.
一度実機で起動。
アプリ起動のタイミングでデバイスがpush対象として登録される。

6.
管理画面からpush送信予約をする

7.
待つ。5分前後。
端末がpush通知受け取れる設定になってるか確認

8.
届けば成功



注意点
android-support-v4.jarが必要。エラーにはならないが実行時にワーニングが出る



【2015/05/23 18:03 】 | Android | 有り難いご意見(0)
Androidのアプリ内課金処理
ご多分に漏れずいろいろ罠がある。


1.
AndroidManifest.xmlに課金のパーミッション追加

<uses-permission android:name="com.android.vending.BILLING"></uses-permission>



2.
この状態でまずapkを一度作る。(課金部分は実装していなくてよい)

3.
作ったapkをgoogle play developer console から、ベータ版としてアップロード
なぜかというと、課金パーミッション付きのapkをアップロードしないと、商品の登録ができないからだ。

4.
課金アイテム他、各種情報を記入。

すべてを記入。

課金アイテムについて。
これは見れば分かる。
「有効」にするのを忘れずに。
プロダクトID(SKUとも言う)はアプリ内でユニークであればよい。なお大文字アルファベットは使えない。
一度使ったIDは、その後二度と使えなくなるので注意。
すべて記入を済ますことで、アプリのベータ公開ができるようになる。

5.
ベータ版を公開する
ここが必須!(これに気付かず数時間無駄にした)
公開していないと、この後課金処理を動かしたときに課金アイテム情報が取得できない。
ベータ版を公開しても、google playに公開される訳ではないので安心しよう。(google+でグループに招待したメンバーにだけ見えるようになる。その方法はここでは関係ないので触れない。)
公開には時間がかかる。consoleで「公開済み」の表示が出るまで待とう(数十分〜数時間)
(ベータ公開しないで実行すると、「指定したアイテムは購入できません」というエラーが出る)

6.
Eclipseを開く。
SDK Managerから、Google play billing library を追加。
$ANDROID_SDK_ROOT/extras/google/play_billing/ 以下のディレクトリができるので確認しておく。
AndroidStudioを開く。
ProjectStructureのDependenciesから、play-servicesを選択する。


以下、この記事を参考。
AndroidStudioでは、この記事も参考になった。

7.
$ANDROID_SDK_ROOT/extras/google/play_billing/IInAppBillingService.aidl
を自分のプロジェクトにコピーする。
$PROJECT_DIR/src/com.android.vending.billing/IInAppBillingService.aidl
AndroidStudioでは、以下の場所に変更になっている
$PROJECT_DIR/app/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl

8.
$ANDROID_SDK_ROOT/extras/google/play_billing/samples/
以下にサンプルコードがあり、ほぼそのまま使える。

$ANDROID_SDK_ROOT/extras/google/play_billing/samples/TrivialDrive/src/com/example/android/trivialdrivesample/util/*.java
を丸ごと自分のプロジェクトのsrc以下にコピー。package名だけ変える

$ANDROID_SDK_ROOT/extras/google/play_billing/samples/TrivialDrive/src/com/example/android/trivialdrivesample/MainActivity.java
を参考にしながら自分のプロジェクトのアクティビティクラスに書く。

サンプルには多くのファイルがあるが、直接使うのはIabHelper.javaだけ。
このファイルが IInAppBillingService への処理を肩代わりしている。
あとは簡単なデータ構造体として
全体的な情報(登録アイテム情報や購入済み=非消費型アイテム情報)を格納しているInventory.java,
1つの課金アイテム情報を格納しているSkuDetails.java,
1つの課金処理トランザクション情報を格納しているPurchase.javaなど。

9.
ライセンスキーをコードに記述。
base64形式で google play developer consoleに書かれているので、コピーしてコード中に記述。
サンプルコードでは、IabHelperのインスタンシエート時に必要。

10.
先ほどgoogle play developer consoleで作成した課金アイテムのプロダクトIDを適宜コードに記載。
プロダクトIDはSKUとも呼ばれる。
SkuDetails.getSku()などで取得できる。
IabHelper.queryInventoryAsync()のmoreSkusなどもこれ。

11.
適宜IabHelperを改造する。
例えば、登録している課金アイテム情報を取得する方法が無いので、
Inventory.getAllSkus() を public にしたりとか。

以上で実装は完了。引き続き動作確認に入る

12.
テストアカウントを作成。
google play developer consoleの設定→アカウント詳細に ライセンステスト という項目があり、そこにメールアドレス(google playアカウント)を記述する。これがテスト購入できるアカウントとなる。
なお、このconsole自体のgoogleアカウントはテストには使えないようになっている。デベロッパー自身が課金購入できないようになっているからだと思われる。

13.
実機端末で、先ほどのアカウントでgoogle playにログイン。
実機のgoogle playは複数アカウントでログインできるものもあるが、「その端末で最初にログインしたアカウント」でしかテスト実行ができないという記事もあった。
その場合、端末を初期化して再度テストアカウントでログインする、という大変なことになるとか。

で、ベータ公開設定画面の「オプトインURL」にアクセス。
すると、「テスターになる」ボタンがあるのでクリック。
これでテストユーザになれる。
これを忘れると、apkのベータ公開後でも「指定したアイテムは購入できません」と出てしまう。


14.
署名付きでapkを作成する
署名が無いと課金購入テストができない。
(署名無しのアプリでテストアカウントで課金購入すると、「このバージョンのアプリは、Google Playを通じたお支払いはご利用になれません。詳しくはヘルプセンターをご覧ください。」というエラーが出る。)
(また、署名つまりkey_storeが異なっていても上記のエラーが出る。そもそも署名が変わるとandroid consoleに提出できなくなる。くれぐれもkey_storeのパスワードなど忘れないように)
(また、課金テストしているバージョンと、ベータ公開しているバージョンが同一である必要がある。公開したら開発中のバージョンは上げたくなるものだが、そこはこらえて)
eclipseでプロジェクトを右クリック→Android tools→Export Signed Android Packageをクリック

なお、ここでLintまわりのエラーが起きることがある。
その場合の対処はこの記事などを参照。
「◯◯の文字列のベラルーシ語が無い」のようなローカライズ周りのエラーの場合、プロジェクト設定→Android Lint Preferences→Correctness:Messages:MissingTranslation をwarningに変更することになる。

15.
出力したapkにzipalign

16.
$adb install コマンドで実機転送

17.
動作確認。
当たり前だがネットワークが繋がっていることを確認のこと。
購入するとき、「これはテスト用の注文です。課金は発生しません。」と表示されるので安心。




その他注意点。

onIabPurchaseFinished や onConsumeFinished コールバックの引数は、
結果を表す IabResult と、アイテム情報を渡す Purchase があるが
IabResultが失敗の場合、Purchaseがnullになる。


参考
参考
参考

チェック項目一覧
参考URL https://groups.google.com/forum/#!topic/android-group-japan/hOFJKNFDCe8
---
AndroidManifestに必要な権限等を設定している。
APKには自分の証明書をセットしている(debug証明書ではない)
デベロッパーコンソールにテストアカウントのメールアドレスを登録している。
確認に使用するAPKと同じVersionCodeのAPKをデベロッパーコンソールへアップロードしている。
テストアカウントはGoogleCheckoutの設定を正しくおこなっている。
テストアカウントがプライマリアカウントとして端末に設定して稼働させている。
GooglePlayアプリで使用許諾に同意している。
署名の確認をおこなうならばアプリの公開鍵は正しいものを使用している。
---
【2015/05/20 00:50 】 | Android | 有り難いご意見(0)
all_loadやら
AdColonyのSDKを導入していて、エラーが出た。


+[NSData adc_dataWithWeaklyEncryptedBase64EncodedString:]: unrecognized selector sent to class 0x25db7e0"


こんな感じ。
同じ問題にぶつかっている人も→いた


adcというのはおそらく AdColony の略だし、要するに、NSDataの拡張(Extension)が読み込まれていないということだろう。

それに対応するため、
Other Linker Flagsに

-ObjC


を追加しろ、とAdColonyのマニュアルには書いてある。ここ

これはObjective-Cのイマイチなところだ。


しかしこれで別のところに問題が出た。


Undefined symbols for architecture arm64:
"_OBJC_CLASS_$_GCController", referenced from:
objc-class-ref in libcocos2dx iOS.a(CCController-iOS.o)
(maybe you meant: _OBJC_CLASS_$_GCControllerConnectionEventHandler)
"_GCControllerDidDisconnectNotification", referenced from:
-[GCControllerConnectionEventHandler observerConnection:disconnection:] in libcocos2dx iOS.a(CCController-iOS.o)
"_GCControllerDidConnectNotification", referenced from:
-[GCControllerConnectionEventHandler observerConnection:disconnection:] in libcocos2dx iOS.a(CCController-iOS.o)



どうやらcocos2d-xのどこかに、未定義の変数が残っているようだ。
GCController***
という名前のものらしい。
該当するソースを読んでみると、どうもコントローラー対応周りの実装だ。

というわけで、
GameController.framework
をプロジェクトに追加して再ビルドしてみる。

…するとうまく行った。

同じ要領で、
GoogleAnalytics周りでもエラーが起きた。
これは
libsqlite3.dylib
libAdIdAccess.a
を追加することで治る。


◯まとめ

・実装があるはずなのにリンクエラーになるメソッド、変数などは
-all_load(または-ObjC)を付加するとうまくいく

・-all_loadをつけたことでエラーになる場合は、実装がプロジェクトに追加されていない。
実装がありそうなライブラリ、フレームワークを予想して追加してみる



本当は、-all_loadってむやみに使いたくない。実行ファイルが大きくなるから。
各社SDKは考慮しておいてほしいところ。



【2015/04/30 20:11 】 | iPhone | 有り難いご意見(0)
cocos2d-xでのデバイスの画面サイズ調整方法
デバイスによって画面の縦横比は違う。
これをどう対応するかというと、
AppDelegate.cppの
bool AppDelegate::applicationDidFinishLaunching();
関数の中で、以下の記述をする。


glview->setDesignResolutionSize(SCREEN_WIDTH, SCREEN_HEIGHT, ResolutionPolicy::SHOW_ALL);


ここで、SCREEN_WIDTH, SCREEN_HEIGHTは自分で決めた値。
この値でcocos2d-xは画面を作る。
ゲームの実装部分は、この画面サイズと見なして作ればいい。

このサイズはいわば仮想画面のサイズで、デバイスの画面サイズとは違う。
この2つの画面をどう合わせるかが第3引数での指定になっている。

以下、指定できる値を列挙。


EXACT_FIT //デバイス画面にぴったり収まるように描画。仮想とデバイス画面の縦横比が合っていなくてもはみ出たり周囲に黒帯がついたりはしないが、縦横比が変化する

NO_BORDER //縦横比を維持。デバイス画面に描画の無い領域を作らないように描画する。そのため、縦横比が合っていないときははみ出る領域がある

SHOW_ALL //縦横比を維持。ゲーム画面全体が中心に描画される。縦横比が合っていないときは周囲に描画の無い黒帯ができる

FIXED_HEIGHT //縦がぴったり収まるように描画する。縦横比は維持する。デバイス画面の右寄せで描画される。縦横比が合わない場合、右がはみ出たり帯が入ったりする。

FIXED_WIDTH //横がぴったり収まるように描画する。縦横比は維持する。デバイス画面の下寄せで描画される。縦横比が合わない場合、上がはみ出たり帯が入ったりする




デフォルトはEXACT_FITになっている。
ぴったり描画するのは良いのだが、縦横比が変わるのはやはりよくない。

SHOW_ALLを使うのが良いと思う。

【2015/04/24 15:04 】 | cocos2d-x | 有り難いご意見(0)
cocos2d-x + Androidでの実装するときは、描画処理発行に気を配るべし
Androidアプリを作るときは、
必要に応じてスレッドを使いながら実装しないといけない。

とくに描画周りの処理を行うなら、必ず
runOnUiThread
の中に実装する。そうでないとすぐ落ちる。

cocos2d-xで動かしている場合、
mainLoopから呼び出される
Layer::update()
メソッドは、描画スレッドの内部であるようで、描画処理(リソースの読み込みやラベルの表示等)をしてもよい(当たり前。いつもそうしてる)

だが、たまにupdate()でないところから描画処理を呼び出す流れになる場合がある。

例えば
・JNI経由でJavaからC++側を呼び出したとき
・・アラートのコールバックからゲーム側を呼び出す
・・シェア結果のコールバックからゲーム側を呼び出す
・・など
・AppDelegateの中から呼び出す
・・applicationDidFinishLaunching
・・applicationWillEnterForeground
・・など
・RootViewControllerからゲーム側を呼び出す
・AppControllerからゲーム側を呼び出す
など

こういうとき、描画処理を行うと、

フリーズ(描画スレッドの停止。タッチには反応し、音楽も鳴るなど)
表示くずれ

などの現象が起きる

上記のような流れのときは、フラグを建てるだけにしておいて、
実際の描画処理はLayer::update()からフラグをチェックして行う
のように実装する。


【2015/04/21 17:53 】 | cocos2d-x | 有り難いご意見(0)
android5系で音が鳴らない
Qiitaのまとめ

修正した結果のファイル

LINEツムツムとかも動かなくなったらしい

Eclipseにおいては、cocos2d-xは外部ライブラリとしてリンクしているので、
修正する場合は、外部ライブラリのソースを書き換える。
ゲームディレクトリに内包されているほうのソースではない。

外部ライブラリのソースを変更し、外部ライブラリをビルドしてから
ゲームのプロジェクトをビルドすると、cocos2d-xへの変更が反映される。
【2015/04/21 16:04 】 | cocos2d-x | 有り難いご意見(0)
App内課金
App内課金。
なかなか大変だが避けて通れない道。
手順を追っていこう。

◯契約
1. iTunes Connect へ行く。
「契約/税金/口座情報」のページへ行き、必要事項を記入。これについては長くなるので、ググって別記事を参照のこと。
これをしていなくても以降の手順は行えるが、実際に端末でテストした時に課金アイテム情報が取得できず、詰まることになるので注意!

◯アイテム登録
1. iTunes Connect へ行く。
最近また変わったので、スクリーンショットも付けて。
まずアプリのページを開き、「App内課金」を選択。


2. App内課金の画面。
以下のようになっている(これはすでにいくつか課金アイテムを登録した後の画面)
Create New をクリック。


3. 課金タイプを選択
消耗型(Consumable)…何度でも購入できるアイテム。
非消耗型(Non-Consumable)…一度しか購入できないアイテム。アプリを削除しても、ユーザがリストアできるようになっていなければならない(後述)
その他は定期購読雑誌などに使うタイプの物。省略


4. 情報を登録
アイテム名、ID、価格、説明文を記述する。
またスクリーンショットも登録する。これは必須。無いとテストもできない。何度でも差し替えられるので、とりあえず何でもいいから画像を登録しておく。



5. 登録できたことを確認
Doneをクリックすると、App内課金のトップ画面に戻り、今つくったアイテムが表示される。
ステータスが「Ready to Submit」になっていればOK。


◯実装
Xcode側の実装

1. ライブラリ登録
StoreKit.framework
を追加

2.
とりあえずRootViewController.mmに実装したものが以下。
外部から呼び出されて駆動するイメージで書いている。
(本筋と関係のない一部メソッドは省略)



#import <UIKit/UIKit.h>
#import "RootViewController.h"
#import "AppInfo.h"

@interface RootViewController()<UIAlertViewDelegate, SKProductsRequestDelegate, SKPaymentTransactionObserver>
{
//purchase 関係
NSArray *allProducts;
PurchaseCallback purchaseCallback;
PurchaseCallback restoreCallback;
}

@end

@implementation RootViewController

- (void)dealloc {
[self purchaseUnregister];
}

#pragma mark - purchase

- (void)purchaseUnregister{
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];

allProducts = nil;
}

- (void)purchaseRegister{
DNSLog(@"purchaseRegister start");

if (![SKPaymentQueue canMakePayments]) {
UIAlertView *alert =[[UIAlertView alloc]initWithTitle:@""
message:@"お使いの端末ではApp内課金の機能が制限されています。[設定]>[一般]>[機能制限]>[App内課金]をONに設定したうえで再度お試しください"
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil];
[alert show];
return;
}

//PaymentQueueにオブザーバー登録(これで課金処理の同期ができるようになる)
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];

[self showIndicator:@"App Storeに問い合わせ中…"];

//productRequest
NSSet *productSet = [NSSet setWithArray:ALL_PRODUCT_IDS];
SKProductsRequest *productRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productSet];
productRequest.delegate = self;
[productRequest start]; //-(void)productsRequest: didReceiveResponse: が呼び出される

allProducts = nil;

DNSLog(@"purchaseRegister end");
}


#pragma mark - SKProductsRequestDelegate

- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
DNSLog(@"productsRequest: didReceiveResponse: start");

#if DEBUG
NSLog(@"Valid PRODUCTS ");
for (SKProduct* prod in response.products) {
[self productDescription:prod];
}
NSLog(@"Invalid PRODUCTS");
for (NSString* prodId in response.invalidProductIdentifiers) {
NSLog(@"%@", prodId);
}
#endif

if ([response.products count] > 0){
allProducts = [[NSArray alloc] initWithArray:response.products];
}
else{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:nil message:@"App Storeに登録されたアイテムがありません。" delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil, nil];
[alert show];
}

[self hideIndicator];


//リストア処理を自動的に行う
//[self restoreItems];

DNSLog(@"productsRequest: didReceiveResponse: end");
}

- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
DNSLog(@"request: didFailWithError: start");

UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"エラー" message:[NSString stringWithFormat:@"App Storeに接続できませんでした。\n%@", [error localizedDescription]] delegate:self cancelButtonTitle:@"はい" otherButtonTitles:nil, nil];
[alert show];

[self hideIndicator];

DNSLog(@"request: didFailWithError: end");
}

- (void)requestDidFinish:(SKRequest *)request{
DNSLog(@"requestDidFinish: start");
[self hideIndicator];
DNSLog(@"requestDidFinish: end");
}


#pragma mark - 購入、リストアなどユーザアクション

- (void) buyItem:(NSString*)productId withCallback:(PurchaseCallback)callback{
DNSLog(@"buyItem: withCallback: start");

//待ち表示
[self showIndicator:@"購入処理をしています……"];

//productIdが実在していれば、PaymentQueueに追加
for (SKProduct *product in allProducts) {
if ([product.productIdentifier isEqualToString:productId]) {

SKPayment* payment = [SKPayment paymentWithProduct:product];
[[SKPaymentQueue defaultQueue] addPayment:payment]; //ここでトランザクション開始。payment:updatedTransactions:が呼ばれるようになる

DNSLog(@"addPayment (PRODUCT_ID=%@)", productId);
break;
}
}

purchaseCallback = callback;

DNSLog(@"buyItem: withCallback: end");
}

-(void) restoreItemsWithCallback:(PurchaseCallback)callback{
DNSLog(@"restoreItems start");

[self showIndicator:@"購入情報を復元中です……"];

[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];

restoreCallback = callback;

DNSLog(@"restoreItems end");
}


#pragma mark - SKPaymentQueue observerMethod

// Sent when the transaction array has changed (additions or state changes). Client should check state of transactions and finish as appropriate.
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions{
DNSLog(@"paymentQueue: updatedTransactions: start");

for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchasing:
//nothing to do
break;
case SKPaymentTransactionStatePurchased:
[self completeTransaction:transaction];
break;
case SKPaymentTransactionStateFailed:
[self failedTransaction:transaction];
break;
case SKPaymentTransactionStateRestored:
[self restoreTransaction:transaction];
break;
case SKPaymentTransactionStateDeferred:
[self deferredTransaction:transaction];
break;
}
}

DNSLog(@"paymentQueue: updatedTransactions: end");
}

// Sent when transactions are removed from the queue (via finishTransaction:).
- (void)paymentQueue:(SKPaymentQueue *)queue removedTransactions:(NSArray *)transactions{
DNSLog(@"paymentQueue: removedTransactions: start");

[self hideIndicator];

DNSLog(@"paymentQueue: removedTransactions: end");
}

// Sent when an error is encountered while adding transactions from the user's purchase history back to the queue.
- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error{
DNSLog(@"paymentQueue: restoreCompletedTransactionsFailedWithError: start");

[self hideIndicator];

DNSLog(@"paymentQueue: restoreCompletedTransactionsFailedWithError: end");
}

// Sent when all transactions from the user's purchase history have successfully been added back to the queue.
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue{
DNSLog(@"paymentQueueRestoreCompletedTransactionsFinished: start");

[self hideIndicator];

DNSLog(@"paymentQueueRestoreCompletedTransactionsFinished: end");
}

// Sent when the download state has changed.
- (void)paymentQueue:(SKPaymentQueue *)queue updatedDownloads:(NSArray *)downloads{
DNSLog(@"paymentQueue: updatedDownloads: start");

DNSLog(@"paymentQueue: updatedDownloads: end");
}


#pragma mark - inner method for SKPaymentQueue observerMethod

- (void) failedTransaction: (SKPaymentTransaction *)transaction{
DNSLog(@"failedTransaction: start");

[self hideIndicator];

[[SKPaymentQueue defaultQueue] finishTransaction: transaction];

//エラーコード全種
//SKErrorUnknown,
//SKErrorClientInvalid, // client is not allowed to issue the request, etc.
//SKErrorPaymentCancelled, // user cancelled the request, etc.
//SKErrorPaymentInvalid, // purchase identifier was invalid, etc.
//SKErrorPaymentNotAllowed, // this device is not allowed to make the payment
//SKErrorStoreProductNotAvailable, // Product is not available in the current storefront

if (transaction.error.code != SKErrorPaymentCancelled) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"" message:@"購入できませんでした。\n再びお試しください。" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[alert show];
}

DNSLog(@"failedTransaction: end");
}

- (void) deferredTransaction: (SKPaymentTransaction *)transaction{
DNSLog(@"deferredTransaction: start");

[self hideIndicator];

if (transaction.error.code != SKErrorPaymentCancelled) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"" message:@"購入できませんでした。\n再びお試しください。" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[alert show];
}

DNSLog(@"deferredTransaction: end");
}

//リストアしたときのトランザクション
- (void) restoreTransaction: (SKPaymentTransaction *)transaction{
DNSLog(@"restoreTransaction: start");
DNSLog(@"productId = %@ x %ld", transaction.payment.productIdentifier, (long)transaction.payment.quantity);

//リストア時処理コールバック呼び出し
NSString *productId = transaction.payment.productIdentifier;
const char *productId_c = [productId cStringUsingEncoding:NSUTF8StringEncoding];
if (restoreCallback) {
DNSLog(@"restoreCallback start");
restoreCallback(productId_c); //コールバック呼び出し。ここでリストア時処理を行うこと
DNSLog(@"restoreCallback end");
}

[[SKPaymentQueue defaultQueue] finishTransaction: transaction];

[self hideIndicator];

DNSLog(@"restoreTransaction: end");
}

//購入に成功したトランザクションを処理
// 非消費アイテムを再度購入しようとし、無料で復元された場合もここに来る
- (void) completeTransaction: (SKPaymentTransaction *)transaction{
DNSLog(@"completeTransaction: start");
DNSLog(@"productId = %@ x %ld", transaction.payment.productIdentifier, transaction.payment.quantity);

//購入時処理コールバック呼び出し
NSString *productId = transaction.payment.productIdentifier;
const char *productId_c = [productId cStringUsingEncoding:NSUTF8StringEncoding];
if (purchaseCallback) {
DNSLog(@"purchaseCallback start");
purchaseCallback(productId_c); //コールバック呼び出し。ここで購入時処理を行うこと
DNSLog(@"purchaseCallback end");
}

[[SKPaymentQueue defaultQueue] finishTransaction: transaction];

[self hideIndicator];

DNSLog(@"completeTransaction: end");
}

@end












以下、解説

◯SKProductsRequest
◯SKProductsResponse
プロダクト情報をAppleに問い合わせるためのクラス。
initWithProductIdentifiers: メソッドでプロダクトIDの配列を渡してインスタンスを作成する。
start: メソッドを呼ぶと問い合わせを開始する。
delegateをセットしたインスタンス(id)の以下のメソッドが呼ばれる。

・問い合わせに成功した場合
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response;
responseには、正常に情報が取れたSKProductの配列(products)と、情報が取れなかったIDの配列(invalidProductIdentifiers)が入っている。

・失敗したとき場合
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error;

・完了した場合(成功したあとにも呼ばれる)
- (void)requestDidFinish:(SKRequest *)request;


◯SKProduct
課金アイテムの情報を持つクラス。メソッドは持っていない。
主なプロパティは以下。

//現地語の説明文
localizedDescription
//現地語のアイテム名
localizedTitle
//金額
price
//金額のロケール
priceLocale
//ID
productIdentifier









国ごとの金額を文字列で取得する場合(日本では120円、アメリカでは$1、のようなこと)
priceとpriceLocaleを組み合わせて以下のように記述する。

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











◯SKPayment
Apple側に、購入する内容を伝えるためのクラス。プロダクトIDや数量をプロパティに持つ。
インスタンスを生成するときは、SKProductを引数に取る。
SKPaymentをSKPaymentQueueにaddPaymentすることで、購入トランザクション(SKPaymentTransaction)が作成される。


◯SKPaymentTransaction
1つの購入トランザクション。
メソッドは持っておらず、プロパティの集合体のクラス。
主なプロパティは以下。

//エラー内容。エラー発生時のみ
error
//このトランザクションを作成するときに使ったSKPaymentの内容
payment
//トランザクションの状態。列挙値は先述
transactionState
//トランザクションID
transactionIdentifier
//レシート。iOS8以降は非推奨となった
transactionReceipt (iOS 7.0)
//トランザクション完了日時。成功/リストア完了した場合だけ有効な値が入る
transactionDate
//SKDownloadの配列。ダウンロードできるデータの情報。購入成功時だけ有効な値が入る
downloads
//元のトランザクション。リストア完了時だけ有効な値が入る
originalTransaction










◯SKPaymentQueue
購入トランザクション(SKPaymentTransaction)を管理するクラス。

・購入可能かのチェック
[SKPaymentQueue canMakePayments]
で、このアプリが課金アイテム購入ができるかどうかチェック。

・オブザーバー登録
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
でオブザーバーを登録する。トランザクションに変化が起きたとき、オブザーバーに対して通知が行く(メソッドが呼ばれる)。delegateのようなもの。
selfは idであること。
オブザーバーがいないと、例えば購入が完了してもそれを関知できない。つまり課金だけしてその結果をアプリ内に反映できなかったりする。
だから必ずオブザーバーはつけておく。
バックグラウンドに行ったときにオブザーバーを削除するような実装もあるが、購入トランザクションの途中でバックグラウンドに行き、バックグラウンドの状態で購入完了してしまうと、まさに上記の現象になってしまう。
だからバックグラウンドに行ってもオブザーバーを削除しない方がいい。どうしても削除するなら、SKPaymentQueue内のトランザクションをすべて終了させてから。(ただし購入途中のトランザクションに対してfinishTransaction:を実行すると例外が投げられるので注意)

・購入トランザクションの開始
購入を始めるとき(ユーザが購入ボタンを押したとき)は、
[[SKPaymentQueue defaultQueue] addPayment:payment];
とする。paymentはSKPaymentクラスのインスタンス。

・リストアを行う
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
これですべての購入済み非消耗型アイテムのトランザクションを元に購入情報をAppleに問い合わせる。

・購入トランザクションの終了
トランザクションを終了するとき(正常に購入できた、正常にリストアできた、エラーが起きて終了するとき)は
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
とする。

・購入トランザクションに変化が起きたとき
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions;
メソッドが呼ばれる。これはSKPaymentTransactionObserver プロトコルのメソッド。
何が起きたかは、各トランザクションのtransactionStateを見れば分かる。
SKPaymentTransactionState 列挙子の値一覧

//購入中
SKPaymentTransactionStatePurchasing, // Transaction is being added to the server queue.
//購入成功
SKPaymentTransactionStatePurchased, // Transaction is in queue, user has been charged. Client should complete the transaction.
//購入失敗
SKPaymentTransactionStateFailed, // Transaction was cancelled or failed before being added to the server queue.
//リストア成功
SKPaymentTransactionStateRestored, // Transaction was restored from user's purchase history. Client should complete the transaction.
//ペアレント許可待ち
SKPaymentTransactionStateDeferred NS_ENUM_AVAILABLE_IOS(8_0), // The transaction is in the queue, but its final status is pending external action.










それぞれの状態に応じた処理を行う。
成功ならアプリ内に反映。アイテム付与など。トランザクションは終了する。
リストアならアプリ内に反映。アイテム数を確認し、正常な値にセット。トランザクションは終了する。
失敗ならエラー表示。トランザクションは終了する。
許可待ちなら、メッセージ表示。
購入中なら何もしないで待つ。

・購入トランザクションが削除されたとき
- (void)paymentQueue:(SKPaymentQueue *)queue removedTransactions:(NSArray *)transactions;
が呼び出される。
finishTransaction: の結果として呼び出される。

・リストアがすべて終了したとき
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue;
が呼び出される。
1つ1つのアイテムのリストアは
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions;
において
transaction.transactionState == SKPaymentTransactionStateRestored で呼び出される。
すべてのアイテムが終わったら、このメソッドが呼び出されることになる。

・リストアが失敗したとき
- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error;
これが呼ばれる。どのような対処が有効なのだろうか…





流れで言うと、
iTunes Connectで設定したプロダクトID(前述のスクショで言うと、TEST_ITEM。com.yourcompany.app.TEST_ITEM とかではない。むしろこれだとSKProductResponseで情報が取れない)を起点に、


起動時(情報取得)
全プロダクトID
→SKProductRequest  (initWithProductIdentifiers: PRODUCT_ID_LIST)
→SKProductResponse  (- (void)productsRequest: didReceiveResponse:)
→SKProduct  (response.products)


購入時
プロダクトID
→SKProduct  (保持SKProductから検索)
→SKPayment  ([SKPayment paymentWithProduct: product])
→SKPaymentQueueに追加  ([[SKPaymentQueue defaultQueue] addPayment:payment];)
→購入処理  (- (void)paymentQueue: updatedTransactions: で transactionState == SKPaymentTransactionStatePurchased)
→失敗処理(- (void)paymentQueue: updatedTransactions: で transactionState == SKPaymentTransactionStateFailed)
→許可待ち処理(- (void)paymentQueue: updatedTransactions: で transactionState == SKPaymentTransactionStateDeferred)


リストア時
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
→各リストア処理  (- (void)paymentQueue: updatedTransactions: で transactionState == SKPaymentTransactionStateRestored)
→全リストア完了通知  (paymentQueueRestoreCompletedTransactionsFinished:)



・どんなときリストアするか
消耗型アイテム以外を購入できる場合は、必ずリストア機能が必要だ。
リストアボタンを置く必要がある。(ないとリジェクト)

ゲームではリストアボタンを置く必要があるのか。
一度しか買えないけど、リストアする(端末間共有)することが必要かどうかはまた別問題。
例えば、取得経験値が永久2倍になるようなアイテム。ゲーム的に一度しか買えないようになっているものだが、これは非消耗型アイテムなのか。それとも、消耗型アイテムとして登録し、アプリ側で一度しか買えないように制限するのか。

非消耗型アイテムを「端末間共有されると想定される」ものとして定義するなら、この永久2倍アイテムは非消耗型アイテムではない。
消耗型アイテムとして登録するのが相応しい。
購入できる回数によって分けるということでは必ずしも無い。

非消耗型アイテムとして登録すると、ゲームの購入画面にリストアボタン(復元ボタン)を置かなければならなくなる。しかしそうすると、他の「消耗型アイテム」も復元されるはずではないか?とユーザは思ってしまう可能性がある(ユーザには消耗型、非消耗型の区別は見えないのだから当然だ)。そう考えると、すべてのアイテムを消耗型として作っておく方がユーザにとっても誤解がないということがあると思われる。


◯レシート検証

検証ロジックは開発者が各自で行うべきという考えになっている。
クラックされないようにするためだ。

レシートデータは、iOS7以前だと購入トランザクションの中に入っていて、SKPaymentTransactionStatePurchased だと正常なものが格納されている。以下で取得できる。
NSData* receiptData = transaction.transactionReceipt

iOS8以降だと、transactionReceiptは非推奨になっている。
どうするかというと、バンドル内に保存されているのでそれを読み出す。
NSBundle の appStoreReceiptURL というメソッドでパスが取得できる。
以下の流れになる。

NSURL *url = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData = [[NSFileManager defaultManager] contentsAtPath:url.path];

検証のために参考になるページ
Qiitaの記事
公式ドキュメント


◯動作確認の手順
まず、実機端末で行う事(シミューレーターでは不可)

1. メアドを作成し(gmailの+付きメアドでもいいかも)、iTunesConnectでsandboxテスターとして登録しておく
2. 実機端末で、AppStoreからサインアウトしておく
3. Xcodeからバイナリを転送。この時、開発用provisioningで作成したバイナリであること。本番用だとテスト用課金はできない
4. アプリを起動し、課金購入を行う。この時、アカウント情報を求められるので、さっき作ったアカウントを入力
5. 購入ダイアログが出る。[Environment: sandbox]と出ていれば、テスト環境での課金になっている

公式ドキュメント(日本語)

【2015/04/16 00:19 】 | iPhone | 有り難いご意見(0)
<<前ページ | ホーム | 次ページ>>