Swift4.0 非消耗型課金を簡単に行うIAPマネージャクラスを作ってみた

# はじめに
こんにちは:leaves:
**アプリ内の広告を削除する**などiOSの非消耗型課金IAPを実装する機会があり、悩んだので書いてみたいと思います。`Xcode9.2 Swift4.0.3`

# Non-Consumable(非消耗型)課金について
<img width="416" alt="スクリーンショット 2018-03-26 0.48.25.png" src="https://qiita-image-store.s3.amazonaws.com/0/128250/724d0cb7-3bb5-235e-8e14-0efc06c82f1d.png">
ユーザがアプリ内で**1度だけ**購入するもの。無効になったり減ったりせず、アプリをアンインストールしても、**リストア処理**を行うことで、購入を復元することができるものです。

# 実装手順
## 1. iTunes Connectにて課金プロダクトを作成
**iTunes Connect**->**マイApp**->**機能** へ進み、
<img width="165" alt="スクリーンショット 2018-03-26 0.48.03.png" src="https://qiita-image-store.s3.amazonaws.com/0/128250/7fc345da-18c6-530a-cb83-df10ac1d5c88.png">

+ボタンから、**参照名**、**製品ID**、**価格**、**スクリーンショット**、**審査スクリーンショット**を入力する。(:warning:これらを入力しないと、提出準備中になりません)

## 2. マネージャークラスの準備
クラス名や関数名などご自由に変更してください。

```IAPManager.swift
import UIKit
import StoreKit

@objc protocol IAPManagerDelegate {
    //購入が完了した時
    @objc optional func iapManagerDidFinishPurchased()
    //ログインや購入確認のアラートが出た時
    @objc optional func iapManagerDidFinishItemLoad()
    //リストアが完了した時
    @objc optional func iapManagerDidFinishRestore(_ productIdentifiers: [String])
    //リストアに失敗した時
    @objc optional func iapManagerDidFailedRestore()
    //1度もアイテム購入したことがなく、リストアを実行した時
    @objc optional func iapManagerDidFailedRestoreNeverPurchase()
    //購入に失敗した時
    @objc optional func iapManagerDidFailedPurchased()
    //特殊な購入時の延期の時
    @objc optional func iapManagerDidDeferredPurchased()
}

class IAPManager: NSObject {
    
    fileprivate var isBuying  = false
    fileprivate var isRestoring = false
    fileprivate var completionForProductidentifiers : (([SKProduct]) -> Void)?
    fileprivate let paymentQueue = SKPaymentQueue.default()
    
    weak var delegate: IAPManagerDelegate?
    
    class var shared : IAPManager {
        struct Static {
            static let instance : IAPManager = IAPManager()
        }
        return Static.instance
    }
    
    private override init() {}
    //ユーザーが課金可能かどうか
    class var canMakePayments: Bool {
        get { return SKPaymentQueue.canMakePayments() }
    }
    //Product情報をApp Storeから取得
    func validateProductIdentifiers(productIdentifiers:[String], completion:(([SKProduct]) -> Void)?) {
        let request = SKProductsRequest(productIdentifiers: Set<String>(productIdentifiers))
        self.completionForProductidentifiers = completion
        request.delegate = self
        request.start()
    }
    //IDで指定したIAPプロダクトの購入を行います
    func buy(productIdentifier: String) {
        guard !self.isBuying else { print("購入処理中"); return }
        validateProductIdentifiers(productIdentifiers: [productIdentifier]) { [unowned self] products in
            let buyProduct: SKProduct? = {
                return products.filter { $0.productIdentifier == productIdentifier }.first
            }()
            
            //購入処理開始
            guard let product = buyProduct else { return }
            
            self.isBuying = true
            let payment = SKMutablePayment(product: product)
            self.paymentQueue.add(payment)
        }
    }
    //リストアを行います
    func restore() {
        guard !isRestoring else { print("リストア処理中"); return }
        self.isRestoring = true
        paymentQueue.restoreCompletedTransactions()
    }
}

extension IAPManager: SKPaymentTransactionObserver {
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction in transactions {
            switch transaction.transactionState {
            case .purchased:
                print("-----purchased-----")
                self.isBuying = false
                delegate?.iapManagerDidFinishPurchased?()
                queue.finishTransaction(transaction)
            case .purchasing:
                print("-----purchasing-----")
                delegate?.iapManagerDidFinishItemLoad?()
            case .restored:
                print("-----restored-----")
                queue.finishTransaction(transaction)
            case .failed:
                print("-----purchaseFailed-----")
                self.isBuying = false
                delegate?.iapManagerDidFailedPurchased?()
                queue.finishTransaction(transaction)
            case .deferred:
                print("-----purchaseDeferred-----")
                self.isBuying = false
                delegate?.iapManagerDidDeferredPurchased?()
                queue.finishTransaction(transaction)
            }
        }
    }
    //リストアの問い合わせが完了した時
    func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
        self.isRestoring = false
        
        guard !queue.transactions.isEmpty else {
            delegate?.iapManagerDidFailedRestoreNeverPurchase?()
            return
        }
        self.isRestoring = false

        let productIdentifiers: [String] = {
            var identifiers: [String] = []
            queue.transactions.forEach {
                identifiers.append($0.payment.productIdentifier)
            }
            return identifiers
        }()

        delegate?.iapManagerDidFinishRestore?(productIdentifiers)
    }
    //リストアの問い合わせが失敗した時
    func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
        print("-----restoreFailed-----")
        self.isRestoring = false
        delegate?.iapManagerDidFailedRestore?()
    }
    //AppStoreで必須
    func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool {
        return true
    }
}

extension IAPManager: SKProductsRequestDelegate {
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        if !response.products.isEmpty {
            self.completionForProductidentifiers?(response.products)
        }
    }
}
```

## 3. AppDelegate.swiftを書き換える
AppDelegate.swiftのコードの一部を以下のように書き換えます。

```AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    //以下を追加
    SKPaymentQueue.default().add(IAPManager.shared)
    return true
}
```
## 4. UIを作成する
課金を行うボタンなどの**UI**を作成します。(例)

<img width="310" alt="スクリーンショット 2018-03-26 1.15.00.png" src="https://qiita-image-store.s3.amazonaws.com/0/128250/369dcd82-7330-99c9-c96d-e6a6bb6389d5.png">

(:warning:画像のように大げさに配置する必要はありませんが、リストアボタンを分かりやすい位置に置かないとリジェクトされる可能性があります。)
アプリ内で課金を行いたいタイミングで(購入ボタンを押した時など）IAPManagerの以下メソッドを呼びます。
また、このタイミングで**Indicatorを表示する**などの実装をすると良いです。

```MainViewController.swift
//購入
IAPManager.shared.buy(productIdentifier: "手順1で記入した製品ID")
//リストア
IAPManager.shared.restore()
```

## 5. デリゲートメソッドを実装する

```MainViewController.swift
override func viewDidLoad() {
    super.viewDidLoad()
    IAPManager.shared.delegate = self
}
```

ボタンなどを配置している**UIViewController**にて、IAPManagerのデリゲートメソッドを記述します。
ここで、各タイミングで行うべき処理を書いてみました。参考にしてみてください:bow_and_arrow:

```MainViewController.swift
extension TitleViewController: IAPManagerDelegate {
    //購入が完了した時
    func iapManagerDidFinishPurchased() {
        //購入完了をユーザに知らせるアラートを表示
        //UserDefaultにBool値を保存する(例:isPurchased = true)
        //Indicatorを隠す処理
        //広告を消す処理など
    }
    //購入に失敗した時
    func iapManagerDidFailedPurchased() {
        //購入失敗をユーザに知らせるアラートなど
        //Indicatorを隠す処理
    }
    //リストアが完了した時
    func iapManagerDidFinishRestore(_ productIdentifiers: [String]) {
        for identifier in productIdentifiers {
            if identifier == App.iapID {
                //リストア完了をユーザに知らせるアラートを表示
                //UserDefaultにBool値を保存する(例:isPurchased = true)
                //広告を消す処理など
            }
        }
        //Indicatorを隠す処理
    }
    //1度もアイテム購入したことがなく、リストアを実行した時
    func iapManagerDidFailedRestoreNeverPurchase() {
        //先に購入をお願いするアラートを表示
        //Indicatorを隠す処理
    }
    //リストアに失敗した時
    func iapManagerDidFailedRestore() {
        //リストア失敗をユーザに知らせるアラートを表示
        //Indicatorを隠す処理
    }
    //特殊な購入時の延期の時
    func iapManagerDidDeferredPurchased() {
        //購入失敗をユーザに知らせるアラートを表示
        //Indicatorを隠す処理
    }
}
```

## 6. テストを行う
ここで、実際に購入ができるかどうかテストをします。
**iTunes Connect** -> **ユーザと役割** -> **Sandboxテスター** へ進み、

<img width="149" alt="スクリーンショット 2018-03-26 1.31.36.png" src="https://qiita-image-store.s3.amazonaws.com/0/128250/bd497f62-c730-dd2f-0842-2195eaa44a9f.png">

+ボタンからテストユーザを作成します。

## 7. iPhone側の準備
実機端末やiPhoneシミュレータから
<img width="50" alt="スクリーンショット 2018-03-26 1.31.36.png" src="https://applech2.com/wp-content/uploads/2017/03/iOS-10-Settings-and-Preference-logo-icon.jpg">
を開き、
<img width="300" alt="スクリーンショット 2018-03-26 1.37.09.png" src="https://qiita-image-store.s3.amazonaws.com/0/128250/4027a616-a79b-089e-6257-abef78d2d4c8.png">
を選択、

<img width="200" alt="スクリーンショット 2018-03-26 1.37.19.png" src="https://qiita-image-store.s3.amazonaws.com/0/128250/efe74d9d-7931-5324-73eb-7864e80dddd2.png">

サインアウトから、普段利用しているAppleアカウントからログアウトしておきます。(ここでテストアカウントでログインする必要は無いみたいです。)
課金テストをするアプリを開き、課金するところまで操作します。すると、以下のようなアラートが表示されるので、

<img width="200" alt="IMG_3166.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/128250/b3f9a96d-9f77-44f1-43d6-ac03464dffcd.png">

**手順6**で作成したユーザの**AppleID**と**パスワード**を入力します。

成功すると以下のアラートが出るはずです。

<img width="230" alt="IMG_3161.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/128250/603cecdb-63f1-4c96-4a61-1e1353b5a7a0.png">

# 参考にさせていただいた記事

- [iOS 課金まとめ Qiita @kayo_h様](https://qiita.com/kayo_h/items/4adeed1048064d9f41f2)
- [iOS(swift)におけるアプリ内課金（IAP）まとめ Qiita @Iwark
様](https://qiita.com/Iwark/items/5f121974c2b8c4905fa6)

見て頂いてありがとうございます。