[iOS]「アプリ内課金(非消費型)」

iOSアプリのアプリ内課金を実装したコードを紹介します。

iOSアプリの課金方法には、消費型、非消費型、サブスクリプションなどがありますが、非消費型のコードを紹介します。

目次

概要

この講座で学習する内容
  • アプリ内課金を実装したコードの書き方

参考にした講座】iOS & Swift – The Complete iOS App Development Bootcamp (Udemy)
 (Section20) In-App Purchases and Apple StoreKit

この講座で作成するアプリの概要
  1. このアプリは、起動すると無料コンテンツが1〜6まで表示されます。
  2. 「有料コンテンツはこちら」から課金手続きを完了すると、有料コンテンツが表示されます。
アプリの開発環境(2023年7月17日現在)
  • Xcode: Version 14.3.1
  • macOS: Venture: Version 13.4.1
  • iOS: 16.5.1
Xcodeのインストールが未了の方はこちら
GitHubからプロジェクトを入手したい方はこちら

iOSアプリの課金方法について

iOSアプリの課金方法には、消費型(Consumable)、非消費型(Non-Consumable)、サブスクリプション(Subscription)などがあります。以下にそれぞれの課金方法について説明します。

消費型(Consumable)

消費型アイテムは、一度使用すると消耗するコンテンツや機能です。

例えば、ゲーム内の仮想通貨やエネルギーが該当します。ユーザーはこれらのアイテムを購入し、使用することでアプリ内で特定のアクションを実行したり、利益を得たりします。消費型アイテムは再購入が可能であり、ユーザーは必要に応じて何度でも購入することができます。

非消費型(Non-Consumable)

非消費型アイテムは、一度購入すると永久的に利用可能なコンテンツや機能です。

例えば、アプリ内の追加の機能やコンテンツ広告の非表示化などが該当します。ユーザーは一度購入すると、そのアイテムを永久に利用することができます。非消費型アイテムは再購入が不要であり、ユーザーに一度の支払いだけで利益や特典を提供します。

サブスクリプション(Subscription)

サブスクリプションモデルでは、ユーザーは定期的な支払いを行い、一定期間にわたってコンテンツや機能へのアクセスや特典を享受します。

例えば、音楽ストリーミングサービスや雑誌の定期購読などが該当します。サブスクリプションは月額や年額などの定期的な支払いが必要であり、ユーザーは有料のサービスやコンテンツを継続的に利用することができます。

アプリ内課金を実装したコードの紹介

アプリ内課金を実装したコード(ここでは、非消費型(Non-Consumable)のコード)を紹介します。

先に、全体のコードを紹介し、後で、コードを区切って、その内容について紹介していきます。

コード紹介(全体)

コード全文かこちらから
import UIKit
import StoreKit

class QuoteTableViewController: UITableViewController, 
    SKPaymentTransactionObserver {

    let productID = "com.ios-pro"
    
    var quotesToShow = [
        "無料コンテンツ-1",
        "無料コンテンツ-2",
        "無料コンテンツ-3",
        "無料コンテンツ-4",
        "無料コンテンツ-5",
        "無料コンテンツ-6"
    ]
    
    let premiumQuotes = [
        "有料コンテンツ-1",
        "有料コンテンツ-2",
        "有料コンテンツ-3",
        "有料コンテンツ-4",
        "有料コンテンツ-5",
        "有料コンテンツ-6"
    ]

    override func viewDidLoad() {
        super.viewDidLoad()
        
        SKPaymentQueue.default().add(self)

        if isPurchased() {
            showPremiumQuotes()
        }
    }

    // MARK: - Table view data source

    override func tableView(_ tableView: UITableView, 
        numberOfRowsInSection section: Int) -> Int {
        if isPurchased() {
            return quotesToShow.count
        } else {
        return quotesToShow.count + 1
        }
    }

 
    override func tableView(_ tableView: UITableView, 
        cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(
            withIdentifier: "QuoteCell", for: indexPath)

        if indexPath.row < quotesToShow.count{
            cell.textLabel?.text = quotesToShow[indexPath.row]
            cell.textLabel?.numberOfLines = 0
            cell.textLabel?.textColor = 
                #colorLiteral(red: 0, green: 0, blue: 0, alpha: 1)
            cell.accessoryType = .none
        } else {
            cell.textLabel?.text = "有料コンテンツはこちら"
            cell.textLabel?.textColor = 
                #colorLiteral(red: 0.1568627451, 
                              green: 0.6666666667, 
                              blue: 0.7529411765, alpha: 1)
            cell.accessoryType = .disclosureIndicator
        }
        return cell
    }
   

    // MARK: - Table view delegate methods
    
    override func tableView(_ tableView: UITableView, 
        didSelectRowAt indexPath: IndexPath) {
        if indexPath.row == quotesToShow.count {
            buyPremiumQuotes()
        }
        
        tableView.deselectRow(at: indexPath, animated: true)
    }
    
    
    // MARK: - In-App Purchase Methods
    
    func buyPremiumQuotes() {
        if SKPaymentQueue.canMakePayments() {
            //Can make payments
            
           let paymentRequest = SKMutablePayment()
            paymentRequest.productIdentifier = productID
            SKPaymentQueue.default().add(paymentRequest)
                  
        } else {
            //Can't make payments
            print("User can't make payments")
        }
    }
    
    func paymentQueue(_ queue: SKPaymentQueue, 
        updatedTransactions transactions: [SKPaymentTransaction]) {
        
        for transaction in transactions {
            if transaction.transactionState == .purchased {
                
                //User payment successful
                print("Transaction successful!")
                
                showPremiumQuotes()
                            
                SKPaymentQueue.default().finishTransaction(transaction)
                
            } else if transaction.transactionState == .failed {
                
                //Payment failed
                if let error = transaction.error {
                    let errorDescription = error.localizedDescription
                    print("Transaction failed due to error: \(errorDescription)")
                }
                
                 SKPaymentQueue.default().finishTransaction(transaction)
                
            } else if transaction.transactionState == .restored {
                
                showPremiumQuotes()
                
                print("Transaction restored")
                
                navigationItem.setRightBarButton(nil, animated: true)
                
                SKPaymentQueue.default().finishTransaction(transaction)
            }
        }     
    }
    
    func showPremiumQuotes() {
        
        UserDefaults.standard.set(true, forKey: productID)
        
        quotesToShow.append(contentsOf: premiumQuotes)
        tableView.reloadData()
        
    }
    
    func isPurchased() -> Bool {
        let purchaseStatus = UserDefaults.standard.bool(forKey: productID)
        
        if purchaseStatus {
            print("Previously purchased")
            return true
        } else {
            print("Never purchased")
            return false
        }
    }
       
    @IBAction func restorePressed(_ sender: UIBarButtonItem) {
        SKPaymentQueue.default().restoreCompletedTransactions()
    }
}

コード紹介(分割)

STEP
class QuoteTableViewController
import StoreKit

class QuoteTableViewController: UITableViewController, 
    SKPaymentTransactionObserver {
}
  • import StoreKitは、StoreKitフレームワークをインポートしていることを示しています。
    StoreKitフレームワークは、アプリ内課金機能を実装するために使用されます。
  • SKPaymentTransactionObserverプロトコルを適用しているため、アプリ内課金のトランザクションの監視を行うことができますSKPaymentTransactionObserverは、アプリ内課金の処理が行われる際に通知を受け取るメソッドを提供します。
STEP
override func viewDidLoad()
override func viewDidLoad() {
    super.viewDidLoad()
        
    SKPaymentQueue.default().add(self)

    if isPurchased() {
        showPremiumQuotes()
    }
}
  • SKPaymentQueue.default().add(self)が実行されています。
  • これは、SKPaymentQueueオブジェクトに現在のビューコントローラ自体を追加しています。
  • SKPaymentQueueは、アプリ内課金の処理を管理するキューであり、selfを追加することで、アプリ内課金のトランザクションの監視を開始します
  • その後、isPurchased()メソッドが呼び出され、ユーザーが既に有料アイテムを購入しているかどうかを確認します。
  • isPurchased()メソッドの返り値がtrueの場合、showPremiumQuotes()メソッドが呼び出され、有料の引用文を表示する処理が行われます。
STEP
セルの行数tableView(_:numberOfRowsInSection:)
override func tableView(_ tableView: UITableView, 
    numberOfRowsInSection section: Int) -> Int {
    if isPurchased() {
        return quotesToShow.count
    } else {
    return quotesToShow.count + 1
    }
}
  • 指定されたセクション内のセルの数を返すために使用されます。
  • isPurchased()メソッドを呼び出して、ユーザーが有料アイテムを購入済みかどうかを確認します。
  • もし購入済みであれば、quotesToShow配列の要素数を返します。
  • 購入していない場合は、quotesToShow配列の要素数に1を加えた値を返します。

これは、最後のセルに「有料コンテンツはこちら」という購入ボタンを表示するためです。

STEP
セルを表示tableView(_:cellForRowAt:)
override func tableView(_ tableView: UITableView, 
    cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(
        withIdentifier: "QuoteCell", for: indexPath)

    if indexPath.row < quotesToShow.count{
        cell.textLabel?.text = quotesToShow[indexPath.row]
        cell.textLabel?.numberOfLines = 0
        cell.textLabel?.textColor = 
            #colorLiteral(red: 0, green: 0, blue: 0, alpha: 1)
        cell.accessoryType = .none
    } else {
        cell.textLabel?.text = "有料コンテンツはこちら"
        cell.textLabel?.textColor = 
            #colorLiteral(red: 0.1568627451, 
            green: 0.6666666667, blue: 0.7529411765, alpha: 1)
        cell.accessoryType = .disclosureIndicator
    }

    return cell
}
  1. 指定された位置に表示するセルを返すために使用されます。
  2. このメソッドの実装では、まず再利用可能なセルを取得します(dequeueReusableCell(withIdentifier:for:)メソッドを使用)。

indexPath.row < quotesToShow.countの時

  1. 該当する引用文をセルのテキストに設定します。
  2. numberOfLinesプロパティを設定して複数行表示可能にし、テキストの色を黒色に設定します。
  3. セルのアクセサリータイプを.noneに設定します。

indexPath.row =< quotesToShow.countの時→(最後のセルである場合

  1. セルのテキストに「有料コンテンツはこちら」を設定します。
  2. テキストの色は青色に設定し、セルのアクセサリータイプを.disclosureIndicatorに設定します。
  3. これにより、ユーザーが「有料コンテンツはこちら」セルをタップすると、追加の引用文を購入するための画面に遷移することができます。
STEP
セルをタップした時tableView(_:didSelectRowAt:)
override func tableView(_ tableView: UITableView, 
    didSelectRowAt indexPath: IndexPath) {
    if indexPath.row == quotesToShow.count {
        buyPremiumQuotes()
    }
        
    tableView.deselectRow(at: indexPath, animated: true)
}
  • tableView(_:didSelectRowAt:)メソッドは、ユーザーがテーブルビューのセルをタップしたときに実行される処理を記述するために使用されます。
  • ユーザーが最後のセル(「有料コンテンツはこちら」セル)をタップした場合、buyPremiumQuotes()メソッドを呼び出して有料の引用文を購入する処理を開始します。
  • その後、tableView.deselectRow(at:animated:)メソッドを使用して、セルの選択状態を解除します。この処理により、セルをタップした後に選択状態が維持されないようになります。
STEP
アプリ内課金を利用できるか?func buyPremiumQuotes()
func buyPremiumQuotes() {
    if SKPaymentQueue.canMakePayments() {
        //Can make payments
           
       let paymentRequest = SKMutablePayment()
        paymentRequest.productIdentifier = productID
        SKPaymentQueue.default().add(paymentRequest)
       
    } else {
        //Can't make payments
        print("User can't make payments")
    }
}
  • SKPaymentQueue.canMakePayments()を使用して、ユーザーがアプリ内課金を利用できるかどうかを確認しています。

ユーザーがアプリ内課金を利用できる場合

  1. SKMutablePaymentオブジェクトを作成します。SKMutablePaymentは、アプリ内課金の支払い情報を表すオブジェクトです。
  2. paymentRequest.productIdentifierproductID(製品のID)を設定します。これにより、購入するアイテムの識別子を指定します。
  3. SKPaymentQueue.default().add(paymentRequest)を使用して、作成した支払い情報をSKPaymentQueueに追加します。これにより、実際のアプリ内課金の処理が開始されます

ユーザーがアプリ内課金を利用できない場合

  • elseブロックが実行され、”User can't make payments“というメッセージがコンソールに出力されます。
STEP
課金状況を確認paymentQueue(_:updatedTransactions:)
func paymentQueue(_ queue: SKPaymentQueue, 
    updatedTransactions transactions: [SKPaymentTransaction]) {
        
    for transaction in transactions {
        if transaction.transactionState == .purchased {
                
            //User payment successful
            print("Transaction successful!")   
            showPremiumQuotes()         
            SKPaymentQueue.default().finishTransaction(transaction)
                
        } else if transaction.transactionState == .failed {
                
            //Payment failed
            if let error = transaction.error {
                let errorDescription = error.localizedDescription
                print("Transaction failed due to error: \(errorDescription)")
            }        
             SKPaymentQueue.default().finishTransaction(transaction)
                
        } else if transaction.transactionState == .restored {
                
            showPremiumQuotes()              
            print("Transaction restored")            
            navigationItem.setRightBarButton(nil, animated: true)           
            SKPaymentQueue.default().finishTransaction(transaction)
        }
    }
}
  • このコードは、SKPaymentTransactionObserverプロトコルのメソッドであるpaymentQueue(_:updatedTransactions:)を実装しています。このメソッドは、アプリ内課金のトランザクションの状態変化を受け取るために使用されます。
  • 引数のtransactionsは、トランザクションの配列です。
  • このメソッドの実装では、transactions配列を反復処理して各トランザクションの状態をチェックしています。

transaction.transactionState == .purchasedの場合

  1. トランザクションの状態が「購入済み」であることをチェックしています。
  2. この場合、支払いが正常に完了したことを示します。コンソールに “Transaction successful!” というメッセージを表示し、showPremiumQuotes()メソッドを呼び出して有料の引用文を表示します。
  3. 最後に、SKPaymentQueue.default().finishTransaction(transaction)を使用してトランザクションの処理を完了させます。

transaction.transactionState == .failedの場合

  1. トランザクションの状態が「失敗」であることをチェックしています。
  2. エラーメッセージを出力し、SKPaymentQueue.default().finishTransaction(transaction)を使用してトランザクションの処理を完了させます。

transaction.transactionState == .restoredの場合

  • トランザクションの状態が「復元」であることをチェックしています。
  • showPremiumQuotes()メソッドを呼び出して有料の引用文を表示し、コンソールに “Transaction restored” というメッセージを表示します。
  • また、navigationItem.setRightBarButton(nil, animated: true)を使用して、復元が完了したことを示すために右上のバーボタンアイテムを非表示にします。
  • 最後に、SKPaymentQueue.default().finishTransaction(transaction)を使用してトランザクションの処理を完了させます。
STEP
有料アイテムを表示func showPremiumQuotes()
func showPremiumQuotes() {
        
    UserDefaults.standard.set(true, forKey: productID)
        
    quotesToShow.append(contentsOf: premiumQuotes)
    tableView.reloadData()
        
}
  • このコードは、showPremiumQuotes()というメソッドを定義しています。このメソッドは、有料の引用文を表示するための処理を行います。
  • まず、UserDefaults.standard.set(true, forKey: productID)を使用して、ユーザーが有料の引用文を購入したことを示すフラグをUserDefaultsに保存します。
  • UserDefaultsは、アプリ内でデータを永続的に保存するためのインターフェースです。この場合、trueの値をproductIDをキーとしてUserDefaultsに保存し、有料の引用文を購入済みであることを示します。
  • 次に、quotesToShow配列にpremiumQuotes配列の要素を追加します。
  • premiumQuotesは有料の引用文の配列であり、これをquotesToShow配列に追加することで、有料の引用文が表示されるようになります。
  • 最後に、tableView.reloadData()を使用して、テーブルビューを再読み込みします。これにより、新しい引用文が表示されるようになります。
STEP
有料アイテムを購入済みか?func isPurchased()
func isPurchased() -> Bool {
    let purchaseStatus = UserDefaults.standard.bool(forKey: productID)
        
    if purchaseStatus {
        print("Previously purchased")
        return true
    } else {
        print("Never purchased")
        return false
    }
}
  • このコードは、isPurchased()というメソッドを定義しています。このメソッドは、ユーザーが有料の引用文を購入済みかどうかを判定するための処理を行います。
  • まず、UserDefaults.standard.bool(forKey: productID)を使用して、UserDefaultsに保存されたproductIDキーに関連付けられたブール値(trueまたはfalse)を取得します。この値は、ユーザーが有料の引用文を購入したかどうかを示すフラグです。
  • 次に、取得したフラグの値をチェックします。もしフラグがtrueであれば、ユーザーは以前に有料の引用文を購入していることを示します。この場合、”Previously purchased”というメッセージをコンソールに出力し、trueを返します。
  • もしフラグがfalseであれば、ユーザーは有料の引用文をまだ購入していないことを示します。この場合、”Never purchased”というメッセージをコンソールに出力し、falseを返します。
STEP
復元ボタンを押した時@IBAction func restorePressed
@IBAction func restorePressed(_ sender: UIBarButtonItem) {
    SKPaymentQueue.default().restoreCompletedTransactions()
}
  • このコードは、restorePressed(_:)というアクションメソッドを定義しています。このメソッドは、ユーザーが「Restore」ボタン(UIBarButtonItem)をタップしたときに呼び出される処理を行います。
  • SKPaymentQueue.default().restoreCompletedTransactions()は、アプリ内課金のトランザクションをリストア(復元)するためのメソッドです。リストアは、ユーザーが以前に購入したアイテムを再度利用可能にするための処理です。通常、ユーザーが新しいデバイスにアプリをインストールした場合や、アプリを削除して再インストールした場合などに使用されます。
  • restoreCompletedTransactions()メソッドを呼び出すことで、SKPaymentQueue(アプリ内課金キュー)に対してトランザクションのリストアを要求します。この要求がキューに追加されると、アプリ内課金フレームワークが適切な処理を行い、ユーザーが以前に購入したアイテムが復元されます

この記事が気に入ったら
いいね または フォローしてね!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次