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

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

サンプルはこちらに置いています。
https://github.com/Shin-ch/PurchaseSample


#処理の流れ概要
https://developer.apple.com/jp/documentation/StoreKitGuide.pdf
から要約抜粋です。

>1. iTunes Connectでプロダクトを作成し、設定します。
* プロダクトIDでSKProductsRequestのインスタンスを使用して、そのリストをApp Storeに送信します。
App Storeから返されたSKProductのインスタンスを使用して、App Store向けのユーザインタフェー
スを実装します。
* SKPaymentQueueのaddPayment:メソッドを使用してSKPaymentのインスタンスをトランザクショ
ンキューに追加することで、支払いを要求します。
* paymentQueue:updatedTransactions:メソッドからトランザクションキューのオブザーバを実
装します。
* 購入したプロダクトを配信するには、アプリケーションの今後の起動に備えて購入記録を持続
し、関連するコンテンツをダウンロードした後、SKPaymentQueueのfinishTransaction:メソッ
ドを呼び出します。

2.　は[ProductManager](http://qiita.com/sora/items/9116a12dcaed27ba27e1)を使用すればSKProductを取得することが可能です

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


#実装例

長いですが、下記３つの構成です。
・AppDelegate.swift ←　使用側(準備) 
・ViewController.swift　←　使用側(課金画面)
・PurchaseManager.swift　←　課金処理


課金完了などの通知はdelegateメソッドで行う方式になっています。
また、コンテンツ解放処理を使用側で行うことを前提にしています。

```swift: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](http://qiita.com/sora/items/9116a12dcaed27ba27e1)も使用します。

```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)
    }
}
```

```swift: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