【Swift】今日から始める、神速で実装する有料非消耗型アイテム販売【AppStore】
AppStoreで有料アイテムの販売することはトライしたことない人からすると、かなり難しそうに思えますし、手順なども不明瞭なのかなと思い、記事を書きました。
SwiftyStoreKitライブラリを使えばかなり簡単に記述できるのでぜひ挑戦してみてください。
手順
・Apple Program Developerで有料アイテムを売るために必要な口座・税金などの情報をすべて入力する。
・マイAppに有料アイテムを登録する。
・ローカルで課金できる開発環境を整える。
・Signing&Capobilitiesから「In-App Purchase」を追加する。
・SwiftyStoreKitライブラリを導入して購入・リストアのコードを書く。
上記手順で開発を進めることができます。
Apple Program Developerで有料アイテムを売るために必要な口座・税金などの情報をすべて入力する
上記の欄を入力してください。
普通に読めば入力できると内容だと思いますが、私も見過ごしてしまった点ですが、「アメリカの納税入力フォーム」は日本在住でも入力してください。
こちらがわかりやすいですね。
https://kacchanblog.com/apple/appstoreconnect-registerbank/2#i
マイAppに有料アイテムを登録する
マイAppで有料アイテムを選択して以下の「管理」へ
App内課金アイテムを追加します。
今回は非消耗型を選択します。
イメージとしては広告非表示機能です。
(実際私も広告非表示機能を実装しました。)
参照名、製品ID、価格、表示名、説明、審査に関する情報(スクショ、審査メモ)が入力できます。
参照名→ 管理画面で集計などで使う値です。好きな名前をつけましょう。
製品ID→ アプリ内でアイテムを特定するためのIdとして利用します。
表示名→ アプリ内で表示されるアイテム名です。
審査関連:→ 審査の為に提出しましょう。
審査が終わって配信が問題なくなったら、・アクティブと表示されます。
ローカルで課金できる開発環境を整える
AppStoreで課金アイテムを設定しても本番環境で試すことも出来ませんし、また審査通るまで開発できないので、Xcode内のローカル環境で課金テストをできる環境を整える必要があります。
https://dev.classmethod.jp/articles/xcode12_storekit_test/
こちらのローカル課金の環境構築をご参考ください。
とりあえずadBlockという製品IDで1個アイテムを作成しました。
・Signing&Capobilitiesから「In-App Purchase」を追加する。
プロジェクトファイルを選択し、Generalの隣のSigning&Capobilitiesを押し、+Capabilityから「In-App Purchase」します。
SwiftyStoreKitライブラリを導入して購入・リストアのコードを書く
SwiftyStoreKitを利用すると用意されたメソッドで簡単に記述できるので是非やってみましょう。私は利用しない場合が複雑で書くのしんどかったので今後はSwiftyStoreKitに依存します。
Podfileを編集し、 pod 'SwiftyStoreKit'を記入し、プロジェクトファイルのあるディレクトリでpod installします。
AppDelete.swiftに以下の記述を追加します。
import UIKit
import StoreKit
import SwiftyStoreKit
@main
class AppDelegate: UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 読み込む
AppStoreClass.shared.loadPurchased()
initSwiftyStorekit()
return true
}
func applicationWillTerminate(_ application: UIApplication) {
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
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
}
}
}
}
}
私はSwiftyStoreKitを利用する上でAppStoreClassというシングルトンクラスを要してみました。
//
// AppStoreClass.swift
// monpokiKit
//
// Created by tanukidevelop on 2021/06/03.
//
import Foundation
import StoreKit
import SwiftyStoreKit
final class AppStoreClass {
private init() {}
static let shared = AppStoreClass()
// 購入済みかどうか確認する
var isPurchased = false
// アプリ起動時にネットに繋いでAppStoreで購入済みか確認する(1件のみ有料アイテムを登録)
func isPurchasedWhenAppStart() {
restore { isSuccess in
if (isSuccess) {
self.isPurchased = true
} else {
self.isPurchased = false
}
}
}
// 購入
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.isPurchased = true
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 == "adBlock" {
// プロダクトID1のリストア後の処理を記述する
self.isPurchased = true
UserDefaults.standard.set(true, forKey:"isPurchased")
isSuceess(true)
return
} else {
self.isPurchased = false
isSuceess(false)
return
}
}
isSuceess(false)
}
}
}
UserDefaultsでアプリが終了しても課金した情報を保存して、AppDelegateで都度起動時に取り出す設計も考えました(Apple的にはこちらが推奨だそうです)
しかしながら、1つのAppleIDをiPad/iPhoneで使っている場合、iPadで購入した場合、iPhoneで認識できないということになるので、アプリ起動時にネットワーク接続して購入済みかどうかを取得するようにしました。
利用者がオフラインの場合は困りますが、利用者の99%はWi-fiなりオンライン環境にいるとは思っていますので問題ないと思います。
この処理によって、シングルトンクラスのプロパティisPurchasedに購入済みかどうかセットされます。
アプリ内で購入・非購入の判定を行う際は、アプリ内で「シングルトンクラスのプロパティisPurchased」を参照してください。
・アイテムを購入する場合
purchaseItemFromAppStore(productId:)を呼び出してください。
第一引数はAppStore/ローカル課金環境に登録した製品ID(productID)をセットしてください。
// 購入
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.isPurchased = true
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 == "adBlock" {
// プロダクトID1のリストア後の処理を記述する
self.isPurchased = true
isSuceess(true)
return
} else {
self.isPurchased = false
isSuceess(false)
return
}
}
isSuceess(false)
}
}
もし購入されている場合は販売アイテム型で列挙されます。
今回は1件だけ登録していますが、ProductID"adBlock"に一致するかどうかの判定を入れております。
購入していた場合はシングルトンクラスのプロパティisPurchasedにtrueをセットします。
これで購入状況をアプリ内に反映できます。