忍者ブログ
  • 2024.03
  • 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.05
[PR]
×

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

【2024/04/26 13:24 】 |
Firebaseのクラッシュレポートを使う
以下のページが参考になる
http://llcc.hatenablog.com/entry/2016/05/21/130359


要点
・Firebaseコンソールから、プロジェクトのSymbolUploadサービスを有効化する
・秘密鍵を作成し、PCに保存
・RunScriptにコマンドを記述


なおAndroidは以下など参考
http://skys.co.jp/archives/4445
未対応
PR
【2016/08/13 13:18 】 | iPhone | 有り難いご意見(0)
swiftのクラスは
SDKの機能を使うなら、NSObjectを継承しておこう

class MyClass: NSObject{


Argument of '#selector' refers to a method that is not exposed to Objective-C
のようなエラーや、

Add @ObjC to expose this method to Objective-C
みたいなエラー修正レコメンドが出たりする


【2016/05/10 18:40 】 | iPhone | 有り難いご意見(0)
StoryboardのSizeClassedを扱う
Storyboardで設定するとき、SizeClassedが使われるようになってから、
縦横各3種類の設定ができるようになっている


これ、Constraintに置いては、どのSizeClassで適用するものなのかがいちいち内部的に記録されている

面倒なのは、w:Any h:Any でConstraintを作っても、
w:Compact h:Compact で作ったConstraintがあれば、iPhoneで実行したときは後者が優先される
なので、気付かず前者の設定でずっとConstraintを作っていたりすると、「なぜ反映されないんだ!」と悶絶することになる。

作成したはずのUIViewやConstraintが半透明で表示されている場合、現在のSizeClassed以外での設定が優先されている可能性がある
以下のように、全部の要素が不透明で表示されていれば問題なし



では、Constraintを現在のSizeClassedに取り込むにはどうするかというと、以下のように「Installed」にチェックを入れれば良い



storyboardファイルのxml的には、
「variation」
というタグで書かれている部分がポイント。

各UIオブジェクトの中に



のようにある場合、記述ののSizeClassed(この場合だとwidthがcompact)の場合に、rect情報がここの情報の通りになる。
これを削除すれば、どのSizeClassedでも同じrectに成る

また、末尾の方に



のように書いてあった場合、Constraintが無効になる場合がある

variationタグが一切なければ、SizeClassedの違いが何も起きない、ということになる。
注:それがいいことというわけではない。
【2016/05/10 18:02 】 | iPhone | 有り難いご意見(0)
BITCODEのエラーが出たら
BITCODEで以下のエラーが出た場合


以下のように設定する
【2015/12/11 22:29 】 | iPhone | 有り難いご意見(0)
ObjCのカテゴリとクラス拡張
本当に今更なのだがまとめ。

参考

◯カテゴリ(category)
カテゴリは元々クラスを分割して書くことが出来るようにするため考えられた言語機能
インスタンス変数を追加できない
「分割実装」

実行時に解決される
だから、インスタンス変数を追加できない
ソースコードが存在しない、ライブラリのクラスを扱うことができる


「実装を」分ける。
macの公式実装とか見ると、カテゴリ複数は「AppController.h」に書いてあって
実装側だけ複数の.mファイルに分かれていたりする。

既存のライブラリの拡張も可能(「拡張」と言っているがカテゴリー)

1クラスに1つしか書けない@implementationを、複数に分けるためにカテゴリーを作っている、とも言える


カッコの中にカテゴリ名を書く。

@interface Document(Ex)



実装側にインスタンス変数の宣言はできない。以下はエラーになる

@implementation Document(Ex){
//こういうことはできない。カテゴリでない大元の実装ファイルに書く
}




◯クラス拡張(extension)
privateな変数、メソッドの宣言に使う
インスタンス変数を宣言できる
宣言したメソッドはクラス本体の(=カテゴリ無しの)@implementationで実装
公式で「無名カテゴリ」ともいうらしい

コンパイル時に解決される
だからインスタンス変数を追加できる


記述は、カッコのカッコの中は空

@interface Document()



元の宣言(.hの@interface)とは別に、.mファイルの方に書くのが普通。

実装側でインスタンス変数の宣言も可能

@implementation{
//変数宣言
//@propertyは不可
}




【2015/11/17 12:38 】 | iPhone | 有り難いご意見(0)
GameCenterの実装
参考
【2015/10/17 16:15 】 | iPhone | 有り難いご意見(0)
iOSで通知タイマーを管理する
アプリ内でNSTimerを発行したいときがある。

◯記述場所
NSTimerは、アプリが起動している間だけ生きていれば良かったので、Model層では扱う必要が無い。
だからViewControllerで完結させるように記述した。


◯破棄
さて、NSTimerはキャンセルすることもできるようにしたい。
ということは、作成したNSTimerのインスタンスを保持しておく必要がある。
NSTimerインスタンスの生成・破棄及び参照保持と削除のタイミングは、

・作成ボタン押下
インスタンス化、参照保持

・キャンセルボタン押下
無効化、参照削除

・タイマー発動
参照削除(←忘れがちだがここでもちゃんと削除しておく。そうでないとvalidでないNSTimerインスタンスの参照がずっと残ることになる)

ということになる。

さてインスタンスの保持の方法だが、
[NSTimer]
Array<NStimer>
Dictionary
NSMutableSet

といろいろあるが、試してみて一番便利なのは

NSMutableSet

だった。
なぜなら、削除したいインスタンスを直接
set.removeObject(timer)
という形で指定できるからである。
それ以外の型だと、削除していいインスタンスかどうかの判定のロジックを書かなければならなくなる。


◯別のViewControllerから生成・削除させる

キャンセルは実は別の画面から行われることもある、という場合。
あるViewControllerから別のViewControllerにどうやって情報を伝達するか。

NotificationCenterを使う。

・通知を発信する側

var notification = NSNotification(name: "RemoveInAppNotification", object: self, userInfo: ["data": declareData!])
NSNotificationCenter.defaultCenter().postNotification(notification)

なおuserInfoは必ず Dictionary? 型でないといけない

・通知を受け取る側

NSNotificationCenter.defaultCenter().addObserver(self, selector: "removeInAppNotifications:", name: "RemoveInAppNotification", object: nil)

func removeInAppNotifications(notification: NSNotification) {
//notification.userInfo などを使って処理
}

となる。
nameはもちろん揃っている必要がある。
通知を受け取る側は、addObserver()やremoveObserver()を正しく書いておく必要がある。
画面に出入りしたときにadd/removeObserverしたいところだが、この場合はいけない。
「別の画面から呼び出されたとき」に通知を受け取りたいからである。
もちろん、該当のViewControllerが常にメモリ上に存在している保証が必要だ(そんなことできるのか?)


以上まとめるとコードは以下のようになる


class MyViewController{

//宣言タイマー
var reminderTimers: NSMutableSet = NSMutableSet()

override func viewDidLoad() {
NSNotificationCenter.defaultCenter().addObserver(self, selector: "removeInAppNotifications:", name: "RemoveInAppNotification", object: nil)
}

func makeInAppNotification(data: DeclareData, delayedSec: NSTimeInterval, message: String){
var userInfo = ["data": data, "message": message]
var t = NSTimer.scheduledTimerWithTimeInterval(delayedSec, target: self, selector: "showInAppNotification:", userInfo: userInfo, repeats: false)
reminderTimers.addObject(t)
}

//指定した宣言のタイマーを削除
func removeInAppNotifications(notification: NSNotification) {
var targetDeclareId = (notification.userInfo?["data"] as DeclareData).declareId

for obj in reminderTimers {
var timer = obj as NSTimer
if timer.valid {
var dic = timer.userInfo as NSDictionary
var timerData = dic["data"] as DeclareData
if timerData.declareId == targetDeclareId {
timer.invalidate()
reminderTimers.removeObject(timer)
}
}
else{
reminderTimers.removeObject(timer)
}
}
}

//NSTimerから呼び出されるメソッド
func showInAppNotification(timer: NSTimer){
var dic = timer.userInfo as NSDictionary
//var data = dic["data"] as DeclareData
var message = dic["message"] as String

UtilAlertView("", message, "OK")

reminderTimers.removeObject(timer)
}

}

class OtherViewController{
@IBAction func cancelButtonPressed(sender: UIButton) {
var notification = NSNotification(name: "RemoveInAppNotification", object: self, userInfo: ["data": declareData!])
NSNotificationCenter.defaultCenter().postNotification(notification)
}
}







【2015/07/12 18:54 】 | iPhone | 有り難いご意見(0)
リジェクト理由:ゲーム内容と異なるアイコン
以下のリジェクト理由が来た


3.3 - Apps with names, descriptions, screenshots, or previews not relevant to the content and functionality of the App will be rejected


さらに後ろにその詳細に付いても書いてある。


3.3 Details

Also, we noticed your icon includes the following image, which is not relevant to the application content and functionality:

We've attached the screenshot for your reference.



というわけで添付されていたスクショを見ると、それはcocos2d-xのアイコンであった。
なるほど。



さっそくアイコンを削除。

バージョン番号、ビルド番号を上げ、アーカイブを作成してサブミット!
…と思ったら、サブミットでエラー発生。



とのこと。
で、見てみる。

プロジェクトビューからプロジェクトを選択。
Infoタブを見てみると、確かに
IconFile
という項目が。その値が
icon57.png
になっている。

これはxcassetsが導入されてから不要になったはず。
というわけでそれを項目ごと削除。

これでOK、通った
【2015/06/17 21:43 】 | iPhone | 有り難いご意見(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)
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)
| ホーム | 次ページ>>