Edited at

アプリ内課金(Delegateモデル)

More than 1 year has passed since last update.

前回は、課金情報を取得するまででしたが、

今回は、実際に課金処理をします。

サンプルはこちらに置いています。

https://github.com/Shin-ch/PurchaseSample


処理の流れ概要

https://developer.apple.com/jp/documentation/StoreKitGuide.pdf

から要約抜粋です。



  1. iTunes Connectでプロダクトを作成し、設定します。

  2. プロダクトIDでSKProductsRequestのインスタンスを使用して、そのリストをApp Storeに送信します。
    App Storeから返されたSKProductのインスタンスを使用して、App Store向けのユーザインタフェー
    スを実装します。

  3. SKPaymentQueueのaddPayment:メソッドを使用してSKPaymentのインスタンスをトランザクショ
    ンキューに追加することで、支払いを要求します。

  4. paymentQueue:updatedTransactions:メソッドからトランザクションキューのオブザーバを実
    装します。

  5. 購入したプロダクトを配信するには、アプリケーションの今後の起動に備えて購入記録を持続
    し、関連するコンテンツをダウンロードした後、SKPaymentQueueのfinishTransaction:メソッ
    ドを呼び出します。


2. はProductManagerを使用すればSKProductを取得することが可能です

3. 4. 5. は今回紹介する PurchaseManagerで処理をします。


実装例

長いですが、下記3つの構成です。

・AppDelegate.swift ← 使用側(準備)

・ViewController.swift ← 使用側(課金画面)

・PurchaseManager.swift ← 課金処理

課金完了などの通知はdelegateメソッドで行う方式になっています。

また、コンテンツ解放処理を使用側で行うことを前提にしています。


AppDelegate.swift

import UIKit

import StoreKit //←インポートしてください

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate,PurchaseManagerDelegate {

var window: UIWindow?

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.

// デリゲート設定
PurchaseManager.shared.delegate = self

// オブザーバー登録
SKPaymentQueue.default().add(PurchaseManager.shared)

return true
}

func applicationWillTerminate(_ application: UIApplication) {
// オブザーバー登録解除
SKPaymentQueue.default().remove(PurchaseManager.shared)
}
}


※前回紹介した、ProductManager.swiftも使用します。


ViewController.swift (使用側)

import UIKit

import StoreKit //←インポートしてください

class ViewController: UIViewController {

let productIdentifiers = ["productIdentifier1"]

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}

///課金開始
private func purchase(productIdentifier: String) {
//デリゲード設定
PurchaseManager.shared.delegate = self

//プロダクト情報を取得
ProductManager.request(productIdentifier: productIdentifier,
completion: {[weak self] (product: SKProduct?, error: Error?) -> Void in
guard error == nil, let product = product else {
self?.purchaseManager(PurchaseManager.shared, didFailTransactionWithError: error)
return
}

//課金処理開始
PurchaseManager.shared.purchase(product: product)
})
}

/// リストア開始
private func startRestore() {
//デリゲード設定
PurchaseManager.shared.delegate = self

//リストア開始
PurchaseManager.shared.restore()
}

}

// MARK: - PurchaseManager Delegate
extension ViewController: PurchaseManagerDelegate {
func purchaseManager(_ purchaseManager: PurchaseManager, didFinishTransaction transaction: SKPaymentTransaction, decisionHandler: (Bool) -> Void) {
//課金終了時に呼び出される
/*
TODO: コンテンツ解放処理

*/
let ac = UIAlertController(title: "purchase finish!", message: nil, preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self.present(ac, animated: true, completion: nil)

//コンテンツ解放が終了したら、この処理を実行(true: 課金処理全部完了, false 課金処理中断)
decisionHandler(true)
}

func purchaseManager(_ purchaseManager: PurchaseManager, didFinishUntreatedTransaction transaction: SKPaymentTransaction, decisionHandler: (Bool) -> Void) {
//課金終了時に呼び出される(startPurchaseで指定したプロダクトID以外のものが課金された時。)
/*
TODO: コンテンツ解放処理

*/
let ac = UIAlertController(title: "purchase finish!(Untreated.)", message: nil, preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self.present(ac, animated: true, completion: nil)

//コンテンツ解放が終了したら、この処理を実行(true: 課金処理全部完了, false 課金処理中断)
decisionHandler(true)
}

func purchaseManager(_ purchaseManager: PurchaseManager, didFailTransactionWithError error: Error?) {
//課金失敗時に呼び出される
/*
TODO: errorを使ってアラート表示

*/
let ac = UIAlertController(title: "purchase fail...", message: error?.localizedDescription, preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
present(ac, animated: true, completion: nil)
}

func purchaseManagerDidFinishRestore(_ purchaseManager: PurchaseManager) {
//リストア終了時に呼び出される(個々のトランザクションは”課金終了”で処理)
/*
TODO: インジケータなどを表示していたら非表示に

*/
let ac = UIAlertController(title: "restore finish!", message: nil, preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
present(ac, animated: true, completion: nil)
}

func purchaseManagerDidDeferred(_ purchaseManager: PurchaseManager) {
//承認待ち状態時に呼び出される(ファミリー共有)
/*
TODO: インジケータなどを表示していたら非表示に

*/
let ac = UIAlertController(title: "purcase defferd.", message: nil, preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
present(ac, animated: true, completion: nil)
}
}



PurchaseManager.swift

import Foundation

import StoreKit

/// 課金エラー
struct PurchaseManagerErrors: OptionSet, Error {
public let rawValue: Int
static let cannotMakePayments = PurchaseManagerErrors(rawValue: 1 << 0)
static let purchasing = PurchaseManagerErrors(rawValue: 1 << 1)
static let restoreing = PurchaseManagerErrors(rawValue: 1 << 2)

public var localizedDescription: String {
var message = ""

if self.contains(.cannotMakePayments) {
message += "設定で購入が無効になっています。"
}

if self.contains(.purchasing) {
message += "課金処理中です。"
}

if self.contains(.restoreing) {
message += "リストア中です。"
}
return message
}
}

/// 課金するためのクラス
open class PurchaseManager : NSObject {

open static var shared = PurchaseManager()

weak var delegate: PurchaseManagerDelegate?

private var productIdentifier: String?
private var isRestore: Bool = false

/// 課金開始
public func purchase(product: SKProduct){

var errors: PurchaseManagerErrors = []

if SKPaymentQueue.canMakePayments() == false {
errors.insert(.cannotMakePayments)
}

if productIdentifier != nil {
errors.insert(.purchasing)
}

if isRestore == true {
errors.insert(.restoreing)
}

//エラーがあれば終了
guard errors.isEmpty else {
delegate?.purchaseManager(self, didFailTransactionWithError: errors)
return
}

//未処理のトランザクションがあればそれを利用
let transactions = SKPaymentQueue.default().transactions
for transaction in transactions {
if transaction.transactionState != .purchased { continue }
if transaction.payment.productIdentifier == product.productIdentifier {
guard let window = UIApplication.shared.delegate?.window else { continue }
let ac = UIAlertController(title: nil, message: "\(product.localizedTitle)は購入処理が中断されていました。\nこのまま無料でダウンロードできます。", preferredStyle: .alert)
let action = UIAlertAction(title: "続行", style: UIAlertActionStyle.default, handler: {[weak self] (action : UIAlertAction!) -> Void in
if let strongSelf = self {
strongSelf.productIdentifier = product.productIdentifier
strongSelf.completeTransaction(transaction)
}
})
ac.addAction(action)
window?.rootViewController?.present(ac, animated: true, completion: nil)
return
}
}

//課金処理開始
let payment = SKMutablePayment(product: product)
SKPaymentQueue.default().add(payment)
productIdentifier = product.productIdentifier
}

/// リストア開始
public func restore(){
if isRestore == false {
isRestore = true
SKPaymentQueue.default().restoreCompletedTransactions()
}else{
delegate?.purchaseManager(self, didFailTransactionWithError: PurchaseManagerErrors.restoreing)
}
}

// MARK: - SKPaymentTransaction process
private func completeTransaction(_ transaction : SKPaymentTransaction) {
if transaction.payment.productIdentifier == self.productIdentifier {
//課金終了
delegate?.purchaseManager(self, didFinishTransaction: transaction, decisionHandler: { (complete) -> Void in
if complete == true {
//トランザクション終了
SKPaymentQueue.default().finishTransaction(transaction)
}
})
productIdentifier = nil
}else{
//課金終了(以前中断された課金処理)
delegate?.purchaseManager(self, didFinishUntreatedTransaction: transaction, decisionHandler: { (complete) -> Void in
if complete == true {
//トランザクション終了
SKPaymentQueue.default().finishTransaction(transaction)
}
})
}
}

private func failedTransaction(_ transaction : SKPaymentTransaction) {
//課金失敗
delegate?.purchaseManager(self, didFailTransactionWithError: transaction.error)
productIdentifier = nil
SKPaymentQueue.default().finishTransaction(transaction)
}

private func restoreTransaction(_ transaction : SKPaymentTransaction) {
//リストア(originalTransactionをdidFinishPurchaseWithTransactionで通知) ※設計に応じて変更
delegate?.purchaseManager(self, didFinishTransaction: transaction, decisionHandler: { (complete) -> Void in
if complete == true {
//トランザクション終了
SKPaymentQueue.default().finishTransaction(transaction)
}
})
}

private func deferredTransaction(_ transaction : SKPaymentTransaction) {
//承認待ち
delegate?.purchaseManagerDidDeferred(self)
productIdentifier = nil
}
}

extension PurchaseManager : SKPaymentTransactionObserver {
// MARK: - SKPaymentTransactionObserver
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
//課金状態が更新されるたびに呼ばれる
for transaction in transactions {
switch transaction.transactionState {
case .purchasing :
//課金中
break
case .purchased :
//課金完了
completeTransaction(transaction)
break
case .failed :
//課金失敗
failedTransaction(transaction)
break
case .restored :
//リストア
restoreTransaction(transaction)
break
case .deferred :
//承認待ち
deferredTransaction(transaction)
break
}
}
}

public func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
//リストア失敗時に呼ばれる
delegate?.purchaseManager(self, didFailTransactionWithError: error)
isRestore = false
}

public func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
//リストア完了時に呼ばれる
delegate?.purchaseManagerDidFinishRestore(self)
isRestore = false
}

}

protocol PurchaseManagerDelegate: NSObjectProtocol {
///課金完了
func purchaseManager(_ purchaseManager: PurchaseManager, didFinishTransaction transaction: SKPaymentTransaction, decisionHandler: (_ complete: Bool) -> Void)
///課金完了(中断していたもの)
func purchaseManager(_ purchaseManager: PurchaseManager, didFinishUntreatedTransaction transaction: SKPaymentTransaction, decisionHandler: (_ complete: Bool) -> Void)
///リストア完了
func purchaseManagerDidFinishRestore(_ purchaseManager: PurchaseManager)
///課金失敗
func purchaseManager(_ purchaseManager: PurchaseManager, didFailTransactionWithError error: Error?)
///承認待ち(ファミリー共有)
func purchaseManagerDidDeferred(_ purchaseManager: PurchaseManager)
}

extension PurchaseManagerDelegate {
///課金完了
func purchaseManager(_ purchaseManager: PurchaseManager, didFinishTransaction transaction: SKPaymentTransaction, decisionHandler: (_ complete: Bool) -> Void){
decisionHandler(false)
}
///課金完了(中断していたもの)
func purchaseManager(_ purchaseManager: PurchaseManager, didFinishUntreatedTransaction transaction: SKPaymentTransaction, decisionHandler: (_ complete: Bool) -> Void){
decisionHandler(false)
}
///リストア完了
func purchaseManagerDidFinishRestore(_ purchaseManager: PurchaseManager){}
///課金失敗
func purchaseManager(_ purchaseManager: PurchaseManager, didFailTransactionWithError error: Error?){}
///承認待ち(ファミリー共有)
func purchaseManagerDidDeferred(_ purchaseManager: PurchaseManager){}

}


課金処理はアプリの設計によって、いろんな実装モデルがあると思います。

私が今まで見たことがあるのは下記のような実装パターンです。

・Appleのサンプルモデル(NSNotificationCenter使用)

・Delegateモデル ←今回紹介したモデル

・Blocksモデル

・継承モデル

・直接実装モデル

・ParseSDKでやるモデル

アプリ設計や、課金で購入するコンテンツの内容、開発者の好み、

に応じて使え分けれればいいかなと思います。

課金によってユーザーが指定した値に対する何かを付与する(占いとかです)、といった可変の動作をするような場合、

今回のモデルは有効と思います。

コンテンツに応じた可変のパラメータを扱うのは、使用側の方が適しているからです。

が、課金によって指定数の仮想通貨を付与する、といった固定の動作しかない場合、

継承、直接実装モデルでいいと思います。

(この方が、AppDelegateが汚れません)

※Objective-Cで書いてたのをSwiftで書き直したのですが、、

間違ってたら、、、優しくご指導いただければ幸いです。。


参考資料

・In-App Purchaseプログラミングガイド

 https://developer.apple.com/jp/documentation/StoreKitGuide.pdf

・In-App Purchase Best Practices

 https://developer.apple.com/library/ios/technotes/tn2387/_index.html

・Optimizing In-App Purchases(WWDC2014)

 https://developer.apple.com/videos/wwdc/2014/?id=303

・StoreKitSuite(Appleサンプルコード)

 https://developer.apple.com/library/prerelease/ios/samplecode/sc1991/Introduction/Intro.html