Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

はじめに

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

Non-Consumable(非消耗型)課金について

スクリーンショット 2018-03-26 0.48.25.png
ユーザがアプリ内で1度だけ購入するもの。無効になったり減ったりせず、アプリをアンインストールしても、リストア処理を行うことで、購入を復元することができるものです。

実装手順

1. iTunes Connectにて課金プロダクトを作成

iTunes Connect->マイApp->機能 へ進み、
スクリーンショット 2018-03-26 0.48.03.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を作成します。(例)

スクリーンショット 2018-03-26 1.15.00.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テスター へ進み、

スクリーンショット 2018-03-26 1.31.36.png

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

7. iPhone側の準備

実機端末やiPhoneシミュレータから
スクリーンショット 2018-03-26 1.31.36.png
を開き、
スクリーンショット 2018-03-26 1.37.09.png
を選択、

スクリーンショット 2018-03-26 1.37.19.png

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

IMG_3166.PNG

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

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

IMG_3161.PNG

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

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away