前回は、課金情報を取得するまででしたが、
今回は、実際に課金処理をします。
サンプルはこちらに置いています。
https://github.com/Shin-ch/PurchaseSample
#処理の流れ概要
https://developer.apple.com/jp/documentation/StoreKitGuide.pdf
から要約抜粋です。
- iTunes Connectでプロダクトを作成し、設定します。
- プロダクトIDでSKProductsRequestのインスタンスを使用して、そのリストをApp Storeに送信します。
App Storeから返されたSKProductのインスタンスを使用して、App Store向けのユーザインタフェー
スを実装します。 - SKPaymentQueueのaddPayment:メソッドを使用してSKPaymentのインスタンスをトランザクショ
ンキューに追加することで、支払いを要求します。 - paymentQueue:updatedTransactions:メソッドからトランザクションキューのオブザーバを実
装します。 - 購入したプロダクトを配信するには、アプリケーションの今後の起動に備えて購入記録を持続
し、関連するコンテンツをダウンロードした後、SKPaymentQueueのfinishTransaction:メソッ
ドを呼び出します。
2. はProductManagerを使用すればSKProductを取得することが可能です
3. 4. 5. は今回紹介する PurchaseManagerで処理をします。
#実装例
長いですが、下記3つの構成です。
・AppDelegate.swift ← 使用側(準備)
・ViewController.swift ← 使用側(課金画面)
・PurchaseManager.swift ← 課金処理
課金完了などの通知はdelegateメソッドで行う方式になっています。
また、コンテンツ解放処理を使用側で行うことを前提にしています。
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も使用します。
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)
}
}
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