iOSのアプリ内課金を実装する方法を紹介します。
Xcode8、Swift3の環境で実装しています。

アプリ内課金の条件

アプリ内課金をするには、もちろんiOS Developer Programに登録していないといけませんので、また登録していない方は登録してください。

https://developer.apple.com/programs/jp/

アプリ内課金の種類

まずアプリ内課金には様々な種類があります。種類の説明は下表のとおりです。

名称 説明 備考
消耗型(Consumable)プロダクト アプリケーションの実行に伴って消費されていくアイテムに対しての課金です。 消費アイテム
非消耗型(Non-consumable)プロダクト ユーザのすべてのデバイス上で無制限に使用できるアイテムに対する課金です。ユーザのすべてのデバイスで使用可能になります。例としては、書籍やゲームレベルなどのコンテンツ、およびアプリケーションの追加機能などがあります。 無制限アイテム
自動更新購読(Auto-renewable subscriptions) エピソードで構成されるコンテンツです。消耗型プロダクトと同様に、自動更新購読は、ユーザのすべてのデバイスで無制限に利用可能になります。非消耗型プロダクトと異なるのは、自動更新購読には期限があるという点です。新しいコンテンツは定期的に信され、ユーザは購読が有効な期間中、発行されたコンテンツに対してアクセスできます。自動更新購読の期限が近づいてくると、ユーザに代わってシステムにより購読が自動的に更新されます。 期間で自動課金
非更新購読(Non-renewable subscriptions) エピソードで構成されるコンテンツを含まない購読です。たとえば、歴史的な写真のデータベースに対するアクセス権や、フライトマップのコレクションなどがあります。ユーザのすべてのデバイスで購読を使用可能にし、ユーザの購入を復元するは、アプリケーション側で対応することになります。このプロダクトタイプは、ユーザのアカウントが既にサーバ上に存在し、このアカウントを使用してコンテンツの復元時にユーザを識別できる場合によく使用されます。購読の期限と期間もアプリケーション(またはサーバ)で実装し、実行することになります。 期間で課金(開発者が期間や期限をサーバーで管理)
無料購読(Free subscriptions) Newsstandに無料購読のコンテンツを置くための手段です。サインアップしたユーザは、Apple IDに関連付けられたどのデバイスからでも購読できます。期限切れになることはありません。また、購読にはNewsstand対応アプリケーションが必要です。 無料、Newsstand対応アプリケーション

アプリ内課金手順

アプリ内下記の実装方法の大まかな流れ・手順は下記になります。

準備

  • [iTunes Connect]契約、税金、連絡先、銀行口座情報登録
  • [iTunes Connect]課金アイテムの登録
  • [iTunes Connect]テストユーザ作成

開発

  • アプリ設定でIn-App PurchaseをON
  • 課金用クラスを実装
  • 課金させたい箇所で課金メソッド実行

契約・税金・口座情報登録

iTunes Connectにログインします。

契約 / 税金 / 口座情報のアイコンをクリックします。

Requestボタンをクリックします。

I have read and...の箇所のチェックボックスをクリックしてSubmitボタンをクリックします。

下図のようにContact InfoBank infoTax Infoの箇所がSet Upボタンになります。

連絡先設定

続いて連絡先情報を設定します。Contact infoSet Upボタンをクリックします。

Maage Your Contact Informationの画面でAdd New Contactをクリックします。

連絡先情報を下記のように入力してSaveボタンをクリックします。

Add New Contactの下にあるSenior Management(部長的な人)、Financial(財務担当)、Technical(技術担当)、Legal(法務担当)、Marketing(マーケティング)の連絡先を登録した連絡先を選択しておきます。
担当により連絡先を変更したい場合にはAdd New Contactで追加してください。

これで連絡先登録は完了です。

銀行口座情報設定

続いて銀行口座情報の設定です。Bank infoSet Upボタンをクリックします。

Add Bank Accountのリンクをクリックします。

Bank Countryを選択します。日本の銀行でしたらJapanを選択してください。

Use any combination of the fields...の下の箇所の検索条件を指定してご自身の持っている銀行口座の銀行を探しましょう。(英語です。)
銀行を選択したらNextボタンをクリックします。

次の画面で銀行口座について問題ないか確認して、問題なければNextボタンをクリックします。

Bank Account NumberConfirm Bank Account Numberに口座番号を入力します。(どちらも同じ口座番号を入力します。)
またBank Account TypeSaving(FUSTU)(普通預金)など口座の種類に応じて選択します。
Bank Account Currencyは口座でやりとりしている通貨の種類です。日本円ならJPY - Japanese Yenを選択します。

銀行情報として問題ないか確認して問題なければI certify that the information above...の箇所のチェックを入れてSaveボタンをクリックします。

Banking Informationの最初の画面で登録した銀行口座情報が選択できるようになっていますので選択してSaveボタンをクリックします。

これで銀行口座登録は完了です。

税金情報設定

次に税金情報設定を行います。Tax InfoSet Upボタンをクリックします。

U.S. Tax FormsSet Upボタンをクリックします。
※ ご自身の環境によりAustralis TaxCanada Taxのほうが良い場合がありますので、気になる方はAppleサポートセンターで確認ください。

Are you a U.S. citizen, U.S. resident, U.S. partnership, or U.S. corporation?とアメリカ国民かどうかを訪ねていますので日本人ならNoを選択してSubmitボタンをクリックします。

Do you have any U.S. Bussiness Activities?と、アメリアでビジネスをしていますか?と聞いています。正直に答えてSubmitボタンをクリックします。

アメリカ人か、またはアメリカでビジネスをしていないかでNoを選択した場合は下図のCertificatie of Foreign Status of Benefical Ownerの画面ができてきます。下図のとおりに入力してSubmitボタンをクリックします。

再度同じ画面がでてきてOnce you submit this form, you will not be able to mark change via iTunes Connect. Please make sure this information is correct before you click Submitというふうに再度入力した内容が問題ないかの確認を求められます。確認して問題なければSubmitボタンをクリックしましょう。

これで税金関連の設定は完了です。

アプリ内課金アイテム追加

次にアプリ内課金のアイテムを追加します。マイAppを選択します。

新規でアプリを作ってプラットフォーム名前プライマリ言語バンドルIDSKUを入力します。

機能をクリックしてApp内課金をクリックし、新規追加の+ボタンをクリックします。

App内課金の種類を選択します。お好みのタイプを選んで作成ボタンをクリックします。

次の画面で課金アイテムの設定を行います。

入力項目に関する説明は下表のとおりです。

入力項目 説明
① 参照名 参照名は iTunes Connect および「売上とトレンド」のレポートで使用されます。App Store には表示されません。名前は最大で64文字です。
② 価格 この価格がApp Storeでの表示価格や収益を決定します。App内課金でコンテンツを販売するには有料App契約が必要です。
③ ローカリゼーション – 表示名 App 内課金コンテンツの名前(App Store に表示)。
④ ローカリゼーション – 説明 審査に使用する、App 内課金に関する情報。App のテストに必要な情報(この App に特有の設定事項など)を記載してください。App 内課金コンテンツによっては、この説明はカスタマーの目にも触れることがあります。自動更新登録の場合、説明文に期間についての記述を含めないでください。
⑤ 審査に関する情報 – スクリーンショット スクリーンショットは審査の目的においてのみ使用します。App Store に表示されることはありません。スクリーンショットは、App のプラットホームで有効な寸法である必要があります。
⑥ 審査に関する情報 – 審査メモ 審査に役立つ App 内課金に関する追加情報(ユーザ名、パスワードなどを含むテストアカウント情報など)。審査メモは4000文字以内で入力してください。

これで課金アイテムが追加されました。

テストユーザの作成

iTunes Connectのホーム画面に戻りユーザと役割をクリックします。

Sandboxテスターのタブを開いて+ボタンをクリックします。

下図の画面が表示されますので下記項目を入力して保存ボタンを作成します。

  • 姓・名
  • メールアドレス
  • パスワード・確認
  • セキュリティ質問・答え
  • 生年月日
  • App Store販売地域

これでテストユーザが作成できました。

アプリ側の課金設定

まだiOSのXcodeプロジェクトを作成していない場合には新規で作成します。

新規プロジェクトの作成

アプリのBundle IDはiTunes Connectで作成したIDにしてください。

プロジェクトエディタのCapabilitiesタブをクリックしてIn-App PurchaseONにします。

課金アイテム管理クラス作成

ProductManager.swift

import Foundation
import StoreKit
private var productManagers : Set<ProductManager> = Set()

class ProductManager: NSObject, SKProductsRequestDelegate {

    private var completionForProductidentifiers : (([SKProduct]?,NSError?) -> Void)?

    /// 課金アイテム情報を取得
    class func productsWithProductIdentifiers(productIdentifiers : [String]!,completion:(([SKProduct]?,NSError?) -> Void)?){
        let productManager = ProductManager()
        productManager.completionForProductidentifiers = completion 
        let productRequest = SKProductsRequest(productIdentifiers: Set(productIdentifiers))
        productRequest.delegate = productManager
        productRequest.start()
        productManagers.insert(productManager)
    }

    // MARK: - SKProducts Request Delegate
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        var error : NSError? = nil
        if response.products.count == 0 {
            error = NSError(domain: "ProductsRequestErrorDomain", code: 0, userInfo: [NSLocalizedDescriptionKey:"プロダクトを取得できませんでした。"])
        }
        completionForProductidentifiers?(response.products, error)
    }

    func request(_ request: SKRequest, didFailWithError error: Error) {
        let error = NSError(domain: "ProductsRequestErrorDomain", code: 0, userInfo: [NSLocalizedDescriptionKey:"プロダクトを取得できませんでした。"])
        completionForProductidentifiers?(nil,error)
        productManagers.remove(self)
    }

    func requestDidFinish(_ request: SKRequest) {
        productManagers.remove(self)
    }

    // MARK: - Utility
    // 価格情報を抽出
    class func priceStringFromProduct(product: SKProduct!) -> String {
        let numberFormatter = NumberFormatter()
        numberFormatter.formatterBehavior = .behavior10_4
        numberFormatter.numberStyle = .currency
        numberFormatter.locale = product.priceLocale
        return numberFormatter.string(from: product.price)!
    }
}

購入処理クラス作成

PurchaseManager.swift

import Foundation
import StoreKit

private let purchaseManagerSharedManager = PurchaseManager()

class PurchaseManager : NSObject,SKPaymentTransactionObserver {

    var delegate : PurchaseManagerDelegate?

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

    /// シングルトン
    class func sharedManager() -> PurchaseManager{
        return purchaseManagerSharedManager;
    }

    /// 課金開始
    func startWithProduct(_ product : SKProduct){
        var errorCount = 0
        var errorMessage = ""

        if SKPaymentQueue.canMakePayments() == false {
            errorCount += 1
            errorMessage = "設定で購入が無効になっています。"
        }

        if self.productIdentifier != nil {
            errorCount += 10
            errorMessage = "課金処理中です。"
        }

        if self.isRestore == true {
            errorCount += 100
            errorMessage = "リストア中です。"
        }

        //エラーがあれば終了
        if errorCount > 0 {
            let error = NSError(domain: "PurchaseErrorDomain", code: errorCount, userInfo: [NSLocalizedDescriptionKey:errorMessage + "(\(errorCount))"])
            self.delegate?.purchaseManager?(self, didFailWithError: error)
            return
        }

        //未処理のトランザクションがあればそれを利用
        let transactions = SKPaymentQueue.default().transactions
        if transactions.count > 0 {
            for transaction in transactions {
                if transaction.transactionState != .purchased {
                    continue
                }

                if transaction.payment.productIdentifier == product.productIdentifier {
                    if let window = UIApplication.shared.delegate?.window {
                        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 weakSelf = self {
                                weakSelf.productIdentifier = product.productIdentifier
                                weakSelf.completeTransaction(transaction)
                            }
                            })
                        ac.addAction(action)
                        window!.rootViewController?.present(ac, animated: true, completion: nil)
                        return
                    }
                }
            }
        }

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

    /// リストア開始
    func startRestore(){
        if self.isRestore == false {
            self.isRestore = true
            SKPaymentQueue.default().restoreCompletedTransactions()
        }else{
            let error = NSError(domain: "PurchaseErrorDomain", code: 0, userInfo: [NSLocalizedDescriptionKey:"リストア処理中です。"])
            self.delegate?.purchaseManager?(self, didFailWithError: error)
        }
    }

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

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

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



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

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

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

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


@objc protocol PurchaseManagerDelegate {
    //課金完了
    @objc optional func purchaseManager(_ purchaseManager: PurchaseManager!, didFinishPurchaseWithTransaction transaction: SKPaymentTransaction!, decisionHandler: ((_ complete : Bool) -> Void)!)
    //課金完了(中断していたもの)
    @objc optional func purchaseManager(_ purchaseManager: PurchaseManager!, didFinishUntreatedPurchaseWithTransaction transaction: SKPaymentTransaction!, decisionHandler: ((_ complete : Bool) -> Void)!)
    //リストア完了
    @objc optional func purchaseManagerDidFinishRestore(_ purchaseManager: PurchaseManager!)
    //課金失敗
    @objc optional func purchaseManager(_ purchaseManager: PurchaseManager!, didFailWithError error: NSError!)
    //承認待ち(ファミリー共有)
    @objc optional func purchaseManagerDidDeferred(_ purchaseManager: PurchaseManager!)
}

購入処理実装

AppDelegate.swift

import UIKit
//////////////// ▼▼ 追加 ▼▼ ////////////////
import StoreKit
//////////////// ▲▲ 追加 ▲▲ ////////////////

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate,
//////////////// ▼▼ 追加 ▼▼ ////////////////
PurchaseManagerDelegate
//////////////// ▲▲ 追加 ▲▲ ////////////////
{
    :
    :
    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        //////////////// ▼▼ 追加 ▼▼ ////////////////
        //---------------------------------------
        // アプリ内課金設定
        //---------------------------------------
        // デリゲート設定
        PurchaseManager.sharedManager().delegate = self
        // オブザーバー登録
        SKPaymentQueue.default().add(PurchaseManager.sharedManager())
        //////////////// ▲▲ 追加 ▲▲ ////////////////
        return true
    }
    //////////////// ▼▼ 追加 ▼▼ ////////////////
    // 課金終了(前回アプリ起動時課金処理が中断されていた場合呼ばれる)
    func purchaseManager(_ purchaseManager: PurchaseManager!, didFinishUntreatedPurchaseWithTransaction transaction: SKPaymentTransaction!, decisionHandler: ((_ complete: Bool) -> Void)!) {
        print("#### didFinishUntreatedPurchaseWithTransaction ####")
        // TODO: コンテンツ解放処理
        //コンテンツ解放が終了したら、この処理を実行(true: 課金処理全部完了, false 課金処理中断)
        decisionHandler(true)
    }
    //////////////// ▲▲ 追加 ▲▲ ////////////////
    :
    :
    func applicationWillTerminate(application: UIApplication) {
        // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
        //////////////// ▼▼ 追加 ▼▼ ////////////////
        // オブザーバー登録解除
        SKPaymentQueue.default().remove(PurchaseManager.sharedManager());
        //////////////// ▲▲ 追加 ▲▲ ////////////////
    }
}

Main.storyboard

ViewController.swift

StoreKitをインポート、PurchaseManagerDelegateプロトコルを追加して下記のように実装します。

import UIKit
import StoreKit

class ViewController: UIViewController,PurchaseManagerDelegate {

    @IBOutlet weak var priceLabel: UILabel!
    @IBOutlet weak var purchaseButton: UIButton!

    let productIdentifiers : [String] = ["com.imagepit.official.appitem1"]

    override func viewDidLoad() {
        super.viewDidLoad()

        // プロダクト情報取得
        fetchProductInformationForIds(productIdentifiers)

    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction func purchaseButtonTapped(_ sender: AnyObject) {
        startPurchase(productIdentifier: productIdentifiers[0])
    }


    //------------------------------------
    // 課金処理開始
    //------------------------------------
    func startPurchase(productIdentifier : String) {
        print("課金処理開始!!")
        //デリゲード設定
        PurchaseManager.sharedManager().delegate = self
        //プロダクト情報を取得
        ProductManager.productsWithProductIdentifiers(productIdentifiers: [productIdentifier], completion: { (products, error) -> Void in
            if (products?.count)! > 0 {
                //課金処理開始
                PurchaseManager.sharedManager().startWithProduct((products?[0])!)
            }
            if (error != nil) {
                print(error)
            }
        })
    }

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

    //------------------------------------
    // MARK: - PurchaseManager Delegate
    //------------------------------------
    //課金終了時に呼び出される
    func purchaseManager(_ purchaseManager: PurchaseManager!, didFinishPurchaseWithTransaction transaction: SKPaymentTransaction!, decisionHandler: ((_ complete: Bool) -> Void)!) {
        print("課金終了!!")
        //---------------------------
        // コンテンツ解放処理
        //---------------------------
        // TODO UserDefault更新

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

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


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

    //課金失敗時に呼び出される
    func purchaseManager(_ purchaseManager: PurchaseManager!, didFailWithError error: NSError!) {
        print("課金失敗!!")
        // TODO errorを使ってアラート表示
    }

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

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

    }

    // プロダクト情報取得
    fileprivate func fetchProductInformationForIds(_ productIds:[String]) {
        ProductManager.productsWithProductIdentifiers(productIdentifiers: productIds,completion: {[weak self] (products : [SKProduct]?, error : NSError?) -> Void in
            if error != nil {
                if self != nil {
                }
                print(error?.localizedDescription)
                return
            }

            for product in products! {
                let priceString = ProductManager.priceStringFromProduct(product: product)
                if self != nil {
                    print(product.localizedTitle + ":\(priceString)")
                    self?.priceLabel.text = product.localizedTitle + ":\(priceString)"
                }
                print(product.localizedTitle + ":\(priceString)" )
            }
            })
    }
}

実行してみましょう。下記のように設定した課金アイテムの名称、価格が表示されればOKです。

テストユーザで購入するには

テストユーザで購入するには一度iOS端末のApple IDをサインアウトしておきましょう。

1.iOS端末のiTunes StoreのAppleIDをサインアウト

iOS端末で設定-> iTunes StoreとApp Storeをタップ

Apple IDの箇所をタップ

サインアウトをタップ

これでサインアウトされます。

2.アプリを再インストール

それからアプリを再インストールしてください。

3.購入テスト

そうすればアプリ内課金がエラーなく実施されます。