LoginSignup
136
143

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-04-13

前回は、課金情報を取得するまででしたが、
今回は、実際に課金処理をします。

サンプルはこちらに置いています。
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

136
143
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
136
143