【Swift5】AppStoreアプリ内課金システムとUserDefaultsで「広告非表示機能(非消耗アイテム)」を識別する仕組みを考える
必要な要件など行なっていきます。
以前有料アプリを導入する際の記事を書きましたのでご覧ください。
広告非表示機能がAppStoreで販売しているとして、アプリ内で広告非表示機能を購入しているかどうかを識別したいと思います。
なぜなら広告非表示機能を利用しているユーザーには必ず広告を表示してはいけないからです。
仕組み
アプリ起動時に常に復元を行うのが本当は良いのかもしれませんが、アプリがオフライン時に動作しなくなる点や都度通信するのは頻度が高くすぎるので、下記設計を考えました。
UserDefaultsはアプリ終了後も次の起動時などで読み込むことが出来るユーザーのデフォルトデータベースへのインターフェイスです。是非活用しましょう。
またリストア処理を行うことで、現在の最新の購入の有無を取得することができます。
またシングルトンクラスでアプリ全体からいつでも購入済みかどうかのBool変数を参照できるようにしておきます。
そこで考えた設計は以下の5点です。
①アプリ起動時はUserDefaultsを読み込み、購入有無をシングルトランクラスに保存する。
②アプリ内で復元(リストア)を選択できるようにする。
③リストアで購入が確認できた時はシングルトンクラスに購入済み(true)を保存して、UserDefaultsにも購入済みを保存する。
④リストアで購入が確認できなかった時はシングルトンクラスに購入済み(true)を保存して、UserDefaultsにも購入済みを保存する。
⑤アプリ内で購入に成功した時はシングルトンクラスに購入済み(true)を保存して、UserDefaultsにも購入済みを保存する。
先に全体のコード書いておきます。
シングルトンクラス(AppStore.Swift)
import Foundation
import StoreKit
import SwiftyStoreKit
final class AppStoreClass {
private init() {}
static let shared = AppStoreClass()
// 購入済みかどうか確認する
var isPurchased = false
// 購入済みを保存する
func savePurchased() {
self.isPurchased = true
UserDefaults.standard.set(self.isPurchased, forKey: "isPurchased")
}
// 購入済みかどうかを読み込んで確認する
func loadPurchased() {
let isPurchased = UserDefaults.standard.bool(forKey: "isPurchased")
self.isPurchased = isPurchased
}
// 販売している有料アイテムを確認する
func getProductInfo() {
SwiftyStoreKit.retrieveProductsInfo([StructConstaints.PRODUCT_ID]) { result in
if let product = result.retrievedProducts.first {
let priceString = product.localizedPrice!
print("Product: \(product.localizedDescription), price: \(priceString)")
}
else if let invalidProductId = result.invalidProductIDs.first {
print("Invalid product identifier: \(invalidProductId)")
}
else {
print("Error: \(result.error)")
}
}
}
// 購入
func purchaseItemFromAppStore(productId: String) {
SwiftyStoreKit.purchaseProduct(productId, quantity: 1, atomically: true) { result in
switch result {
case .success(let purchase):
print("Purchase Success: \(purchase.productId)")
AppStoreClass.shared.savePurchased()
case .error(let error):
switch error.code {
case .unknown: print("Unknown error. Please contact support")
case .clientInvalid: print("Not allowed to make the payment")
case .paymentCancelled: break
case .paymentInvalid: print("The purchase identifier was invalid")
case .paymentNotAllowed: print("The device is not allowed to make the payment")
case .storeProductNotAvailable: print("The product is not available in the current storefront")
case .cloudServicePermissionDenied: print("Access to cloud service information is not allowed")
case .cloudServiceNetworkConnectionFailed: print("Could not connect to the network")
case .cloudServiceRevoked: print("User has revoked permission to use this cloud service")
@unknown default: break
}
}
}
}
// リストア
func restore(isSuceess: @escaping (Bool) -> Void ) {
SwiftyStoreKit.restorePurchases(atomically: true) { result in
for product in result.restoredPurchases {
if product.needsFinishTransaction {
SwiftyStoreKit.finishTransaction(product.transaction)
}
if product.productId == StructConstaints.PRODUCT_ID {
// プロダクトID1のリストア後の処理を記述する
self.isPurchased = true
AppStoreClass.shared.savePurchased()
isSuceess(true)
return
} else {
self.isPurchased = false
isSuceess(false)
return
}
}
isSuceess(false)
}
}
}
それぞれの設計を説明していきます。
アプリ起動時はUserDefaultsを読み込み、購入有無をシングルトランクラスに保存する。
AppDelegate.Swift
loadPurchased()を呼び出して、SwiftyStoreKitを監視するinitSwiftyStorekit()を実行します。
class AppDelegate: UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 読み込む
AppStoreClass.shared.loadPurchased()
initSwiftyStorekit()
return true
}
func initSwiftyStorekit() {
SwiftyStoreKit.completeTransactions(atomically: true) { purchases in
for purchase in purchases {
switch purchase.transaction.transactionState {
case .purchased, .restored:
if purchase.needsFinishTransaction {
SwiftyStoreKit.finishTransaction(purchase.transaction)
}
// Unlock content
case .failed, .purchasing, .deferred:
break // do nothing
}
}
}
}
}
アプリ起動時に以下の処理を行い、UserDefaultsを読み込んでシングルトンクラスに結果を格納します。
AppStore.Swift
// 購入済みかどうかを読み込んで確認する
func loadPurchased() {
let isPurchased = UserDefaults.standard.bool(forKey: "isPurchased")
self.isPurchased = isPurchased
}
これで、アプリ起動時にアプリ内部に保存されたデータからシングルトンクラスへBoolで結果を保存しています。
ちなみに最初にアプリ起動した場合は上記の式はfalseなので、購入していない扱いで問題なく正しい挙動を行います。
②アプリ内で復元(リストア)を選択できるようにする。
どんな感じの実装でもいいんですが、このように書いてみました。
リストアを行う際の処理は以下を呼び出してください。
// リストア
func restore(isSuceess: @escaping (Bool) -> Void ) {
SwiftyStoreKit.restorePurchases(atomically: true) { result in
for product in result.restoredPurchases {
if product.needsFinishTransaction {
SwiftyStoreKit.finishTransaction(product.transaction)
}
if product.productId == StructConstaints.PRODUCT_ID {
// プロダクトID1のリストア後の処理を記述する
self.isPurchased = true
AppStoreClass.shared.savePurchased()
isSuceess(true)
return
} else {
self.isPurchased = false
isSuceess(false)
return
}
}
isSuceess(false)
}
}
呼び出し側はこう書きますです。
AppStoreClass.shared.restore { isSuccess in
if (isSuccess) {
self?.showAlertControllerAutoDelete(massage: "復元に成功しました。")
} else {
self?.showAlertControllerAutoDelete(massage: "復元に失敗しました。広告非表示機能が購入されていません。")
}
}
リストア処理側にリストア成功した場合はUserDefaultsに保存する処理とシングルトンクラスのBoolを変更する処理が入っているので、呼び出し側はアラートビューの結果だけ表示してあげれば良いと思います。
上記の処理で以下も達成しました。
③リストアで購入が確認できた時はシングルトンクラスに購入済み(true)を保存して、UserDefaultsにも購入済みを保存する。
④リストアで購入が確認できなかった時はシングルトンクラスに購入済み(true)を保存して、UserDefaultsにも購入済みを保存する。
⑤アプリ内で購入に成功した時はシングルトンクラスに購入済み(true)を保存して、UserDefaultsにも購入済みを保存する。
AppStoreClass.Swift
// 購入
func purchaseItemFromAppStore(productId: String) {
SwiftyStoreKit.purchaseProduct(productId, quantity: 1, atomically: true) { result in
switch result {
case .success(let purchase):
print("Purchase Success: \(purchase.productId)")
AppStoreClass.shared.savePurchased()
case .error(let error):
switch error.code {
case .unknown: print("Unknown error. Please contact support")
case .clientInvalid: print("Not allowed to make the payment")
case .paymentCancelled: break
case .paymentInvalid: print("The purchase identifier was invalid")
case .paymentNotAllowed: print("The device is not allowed to make the payment")
case .storeProductNotAvailable: print("The product is not available in the current storefront")
case .cloudServicePermissionDenied: print("Access to cloud service information is not allowed")
case .cloudServiceNetworkConnectionFailed: print("Could not connect to the network")
case .cloudServiceRevoked: print("User has revoked permission to use this cloud service")
@unknown default: break
}
}
}
}
こちらも成功した時にUserDefaultsの保存とシングルトンクラスのBoolを保存する処理も行われるので上記記述だけでOKです。
補足
これでアプリ内の広告表示を行う処理の所に、シングルトンクラスのBoolを参照して、もしisPurchased(Bool)がtrueの場合は広告表示を行わないように分岐させてください。
また、クロージャなどを利用して数秒後に広告表示を行うなど行なっている場合はクロージャーの変数のキャプチャ(その時点での変数の値を固定して参照してしまう)があるので気をつけてください。
※わからない人は適切にisPurchased(Bool)が正常値で取得できない場合は疑ってみてください。