StoreKit 2面接対策:サブスクリプション管理とレシート検証

StoreKit 2、サブスクリプション管理、レシート検証、アプリ内課金実装に関するiOS面接の質問を、実用的なSwiftコード例とともにマスターしましょう。

iOS StoreKit 2サブスクリプションアーキテクチャとレシート検証

StoreKit 2は、iOSにおけるアプリ内課金の処理方法における根本的な変革を表します。iOS 15で導入されたこの最新フレームワークは、サブスクリプション管理とトランザクション検証に必要なコードを大幅に簡素化します。iOS 18でStoreKit 1は正式に非推奨となり、StoreKit 2が唯一のサポート対象となりました。iOSの技術面接では、アプリ収益化に関する開発者の専門知識を評価するため、このトピックが定期的に取り上げられます。

面接における重要なポイント

StoreKit 2は、async/awaitを使用したネイティブSwift APIを活用し、複雑なコールバックやクライアントサイドのレシート検証を不要にします。面接官は、StoreKit 1との根本的な違いや、appTransactionIDやSubscriptionOfferViewなどのiOS 18.4の新機能を明確に説明できる候補者を高く評価します。

StoreKit 2アーキテクチャの概要

StoreKit 2は、Swift Concurrencyを最大限に活用するモダンなアーキテクチャの上に構築されています。StoreKit 1とは異なり、すべての操作は非同期であり、ネイティブSwift型を使用します。

利用可能な製品の取得

最初のステップは、App Store Connectで設定された製品を読み込むことです。StoreKit 2は宣言的なAPIでこの操作を簡素化します。

ProductService.swiftswift
import StoreKit

actor ProductService {

    // App Store Connectで設定された製品識別子
    private let productIdentifiers: Set<String> = [
        "com.app.subscription.monthly",
        "com.app.subscription.yearly",
        "com.app.premium.lifetime"
    ]

    // 繰り返しのネットワーク呼び出しを避けるための製品キャッシュ
    private var cachedProducts: [Product] = []

    func loadProducts() async throws -> [Product] {
        // キャッシュが利用可能な場合は返す
        guard cachedProducts.isEmpty else {
            return cachedProducts
        }

        // App Storeへの非同期リクエスト
        let products = try await Product.products(for: productIdentifiers)
        cachedProducts = products
        return products
    }

    func product(for identifier: String) async throws -> Product? {
        let products = try await loadProducts()
        return products.first { $0.id == identifier }
    }
}

actorを使用することで、製品キャッシュの管理時にスレッド安全性が保証されます。

StoreKit 2の製品タイプ

StoreKit 2は、異なる製品タイプを明確に区別します。この区別を理解することは面接で重要です。

ProductType+Extensions.swiftswift
import StoreKit

extension Product.ProductType {

    var displayName: String {
        switch self {
        case .consumable:
            // 消耗型製品(クレジット、ライフなど)
            return "Consumable"
        case .nonConsumable:
            // 永続的な購入(機能のアンロック)
            return "Non-Consumable"
        case .autoRenewable:
            // 自動更新サブスクリプション
            return "Auto-Renewable Subscription"
        case .nonRenewable:
            // 更新なしのサブスクリプション(一時的なパス)
            return "Non-Renewable Subscription"
        @unknown default:
            return "Unknown"
        }
    }
}

自動更新サブスクリプションは、現代のiOSアプリケーションにとって主流のビジネスモデルです。

購入トランザクションの処理

StoreKit 2における購入プロセスは、async/awaitのおかげで線形的になります。StoreKit 1のデリゲートやネストされたコールバックはもう必要ありません。

購入の開始

iOS 18.2以降、購入にはUIコンテキストパラメータが必要です。この変更により、購入シートがアプリのビュー階層内で正しく表示されます。

PurchaseManager.swiftswift
import StoreKit
import UIKit

actor PurchaseManager {

    enum PurchaseError: Error {
        case productNotFound
        case purchaseCancelled
        case purchasePending
        case verificationFailed
        case unknown
    }

    func purchase(_ product: Product, in scene: UIWindowScene) async throws -> Transaction {
        // UIコンテキスト付きで購入を開始(iOS 18.2以降)
        let result = try await product.purchase(confirmIn: scene)

        switch result {
        case .success(let verification):
            // トランザクション署名の検証
            let transaction = try checkVerification(verification)

            // トランザクションを完了としてマーク
            await transaction.finish()

            return transaction

        case .userCancelled:
            // ユーザーが購入モーダルをキャンセル
            throw PurchaseError.purchaseCancelled

        case .pending:
            // 保留中の購入(保護者の承認など)
            throw PurchaseError.purchasePending

        @unknown default:
            throw PurchaseError.unknown
        }
    }

    private func checkVerification<T>(_ result: VerificationResult<T>) throws -> T {
        switch result {
        case .verified(let safe):
            // App Storeによって署名・検証されたトランザクション
            return safe
        case .unverified(_, let error):
            // 無効または破損した署名
            throw PurchaseError.verificationFailed
        }
    }
}

finish()メソッドは重要です:コンテンツがユーザーに配信されたことをApp Storeに通知します。

面接の警告

コンテンツ配信後にtransaction.finish()を呼び出すことを決して忘れないでください。完了していないトランザクションは次のアプリ起動時に復元され、予期しない動作を引き起こします。

バックグラウンドトランザクションの監視

StoreKit 2はTransaction.updatesを導入します。これは、アプリの外で発生するトランザクション(更新、ファミリー共有購入など)を発行する非同期ストリームです。

TransactionObserver.swiftswift
import StoreKit

actor TransactionObserver {

    private var updateTask: Task<Void, Never>?

    func startObserving() {
        // 以前の監視をキャンセル
        updateTask?.cancel()

        updateTask = Task(priority: .background) {
            // トランザクション更新の無限ストリーム
            for await result in Transaction.updates {
                do {
                    let transaction = try self.checkVerification(result)

                    // タイプに応じてトランザクションを処理
                    await self.handleTransaction(transaction)

                    // 常にトランザクションを完了
                    await transaction.finish()
                } catch {
                    // デバッグのためにエラーをログ記録
                    print("Transaction verification failed: (error)")
                }
            }
        }
    }

    func stopObserving() {
        updateTask?.cancel()
        updateTask = nil
    }

    private func handleTransaction(_ transaction: Transaction) async {
        switch transaction.productType {
        case .autoRenewable:
            // サブスクリプションステータスを更新
            await updateSubscriptionStatus(transaction)
        case .nonConsumable:
            // プレミアム機能をアンロック
            await unlockFeature(transaction.productID)
        default:
            break
        }
    }

    private func checkVerification<T>(_ result: VerificationResult<T>) throws -> T {
        switch result {
        case .verified(let safe):
            return safe
        case .unverified:
            throw PurchaseError.verificationFailed
        }
    }

    private func updateSubscriptionStatus(_ transaction: Transaction) async {
        // ステータス更新の実装
    }

    private func unlockFeature(_ productID: String) async {
        // 機能アンロックの実装
    }

    enum PurchaseError: Error {
        case verificationFailed
    }
}

トランザクションの監視は、更新を見逃さないようにアプリ起動と同時に開始する必要があります。

StoreKit 2サブスクリプションに関するよくある面接質問

iOS面接では、サブスクリプション管理に関する具体的な質問がよく含まれます。以下は最も一般的なものと、その詳細な回答です。その他のSwift面接トピックについては、Swift構造化並行処理ガイドを参照してください。

質問1:現在のサブスクリプションステータスを確認する方法は?

StoreKit 2は、アクティブなエンタイトルメントへの直接アクセスを提供します。Transaction.currentEntitlement(for:)はiOS 18.4で非推奨となりました。新しいcurrentEntitlements(for:)は、複数のエンタイトルメント(例:サブスクリプションとファミリー共有アクセス)を処理するための非同期シーケンスを返します。

SubscriptionStatusChecker.swiftswift
import StoreKit

struct SubscriptionStatusChecker {

    struct SubscriptionStatus {
        let isActive: Bool
        let expirationDate: Date?
        let willRenew: Bool
        let productID: String?
        let appTransactionID: UInt64?  // iOS 18.4以降
    }

    static func checkCurrentStatus(for productID: String) async -> SubscriptionStatus {
        // 新しいcurrentEntitlements(for:) APIを使用(iOS 18.4以降)
        var latestTransaction: Transaction?

        for await result in Transaction.currentEntitlements(for: productID) {
            if case .verified(let transaction) = result {
                if transaction.productType == .autoRenewable {
                    if latestTransaction == nil ||
                       transaction.purchaseDate > latestTransaction!.purchaseDate {
                        latestTransaction = transaction
                    }
                }
            }
        }

        guard let transaction = latestTransaction else {
            return SubscriptionStatus(
                isActive: false,
                expirationDate: nil,
                willRenew: false,
                productID: nil,
                appTransactionID: nil
            )
        }

        // サブスクリプションがまだアクティブかチェック
        let isActive = transaction.expirationDate ?? Date.distantPast > Date()

        // 更新情報を取得
        let renewalInfo = await getRenewalInfo(for: transaction.productID)

        return SubscriptionStatus(
            isActive: isActive,
            expirationDate: transaction.expirationDate,
            willRenew: renewalInfo?.willAutoRenew ?? false,
            productID: transaction.productID,
            appTransactionID: transaction.appTransactionID
        )
    }

    private static func getRenewalInfo(for productID: String) async -> Product.SubscriptionInfo.RenewalInfo? {
        guard let product = try? await Product.products(for: [productID]).first,
              let subscription = product.subscription else {
            return nil
        }

        // 更新ステータスを取得
        let status = try? await subscription.status.first
        return status?.renewalInfo
    }
}

このアプローチは、サーバー呼び出しなしで正確なサブスクリプション状態を提供します。iOS 18.4で導入されたappTransactionIDフィールドにより、サーバー間通信なしでトランザクション間で顧客を追跡できます。

iOSの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

質問2:無料トライアルとプロモーションオファーの処理方法は?

StoreKit 2はオファー情報へのアクセスを簡素化します。面接官はしばしば異なるオファータイプの理解をテストします。iOS 18.4以降、App Store Server Libraryで利用可能な新しいPromotionOfferV2SignatureCreatorを使用して、JWSでプロモーションオファーに署名できます。

OfferManager.swiftswift
import StoreKit

struct OfferManager {

    enum OfferType {
        case freeTrial(duration: String)
        case payAsYouGo(periods: Int, price: Decimal)
        case payUpFront(duration: String, price: Decimal)
        case oneTime(price: Decimal)  // iOS 17.2以降:すべての製品タイプのオファーコード
        case none
    }

    static func getIntroductoryOffer(for product: Product) -> OfferType {
        guard let subscription = product.subscription,
              let offer = subscription.introductoryOffer else {
            return .none
        }

        switch offer.paymentMode {
        case .freeTrial:
            // 無料トライアル期間
            return .freeTrial(duration: formatPeriod(offer.period))

        case .payAsYouGo:
            // 複数期間にわたる割引価格
            return .payAsYouGo(
                periods: offer.periodCount,
                price: offer.price
            )

        case .payUpFront:
            // 期間に対する一括支払い
            return .payUpFront(
                duration: formatPeriod(offer.period),
                price: offer.price
            )

        case .oneTime:
            // 一回限りのオファーコード(消耗型、非消耗型、非更新型)
            return .oneTime(price: offer.price)

        @unknown default:
            return .none
        }
    }

    static func checkEligibility(for product: Product) async -> Bool {
        // ユーザーがオファーを利用できるかチェック
        guard let subscription = product.subscription else {
            return false
        }

        // isEligibleForIntroOfferは新規サブスクライバーかを示す
        return await subscription.isEligibleForIntroOffer
    }

    private static func formatPeriod(_ period: Product.SubscriptionPeriod) -> String {
        let value = period.value
        switch period.unit {
        case .day:
            return "(value) day(value > 1 ? "s" : "")"
        case .week:
            return "(value) week(value > 1 ? "s" : "")"
        case .month:
            return "(value) month(value > 1 ? "s" : "")"
        case .year:
            return "(value) year(value > 1 ? "s" : "")"
        @unknown default:
            return ""
        }
    }
}

isEligibleForIntroOfferメソッドは、利用できないオファーの表示を回避するために重要です。以前は自動更新サブスクリプションに限定されていたオファーコードは、iOS 16.3以降、消耗型、非消耗型、非更新型サブスクリプションもサポートしています。

質問3:過去の購入を復元する方法は?

購入の復元は、App Storeレビューガイドラインに従って必須の機能です。

RestoreManager.swiftswift
import StoreKit

actor RestoreManager {

    struct RestoreResult {
        let restoredProducts: [String]
        let activeSubscriptions: [String]
    }

    func restorePurchases() async throws -> RestoreResult {
        var restoredProducts: [String] = []
        var activeSubscriptions: [String] = []

        // App Storeと同期
        try await AppStore.sync()

        // すべてのユーザートランザクションを反復
        for await result in Transaction.currentEntitlements {
            guard case .verified(let transaction) = result else {
                continue
            }

            switch transaction.productType {
            case .nonConsumable:
                // 永続的な購入を復元
                restoredProducts.append(transaction.productID)

            case .autoRenewable:
                // サブスクリプションがアクティブかチェック
                if let expiration = transaction.expirationDate,
                   expiration > Date() {
                    activeSubscriptions.append(transaction.productID)
                }

            default:
                // 消耗型は復元不可
                break
            }
        }

        return RestoreResult(
            restoredProducts: restoredProducts,
            activeSubscriptions: activeSubscriptions
        )
    }
}

AppStore.sync()はAppleサーバーとの同期を強制し、再インストール後に役立ちます。

SubscriptionOfferView:モダンなサブスクリプションマーチャンダイジング

iOS 18.4で導入されたSubscriptionOfferViewは、製品メタデータの自動読み込みを備えたサブスクリプションオプションを表示するSwiftUIコンポーネントです。サブスクリプションベースのアプリを構築する開発者にとって、よくある面接トピックです。

SubscriptionOfferViewExample.swiftswift
import SwiftUI
import StoreKit

struct SubscriptionSettingsView: View {
    let subscriptionGroupID = "com.app.subscription.group"
    
    var body: some View {
        VStack(spacing: 16) {
            // 現在の購読者にアップグレードオプションを表示
            SubscriptionOfferView(groupID: subscriptionGroupID)
                .visibleRelationship(.upgrade)
                .prefersPromotionalIcon(true)
                .subscriptionOfferViewDetailAction { path in
                    path.append("subscriptionDetail")
                }
            
            // 現在のプラン詳細を表示
            SubscriptionOfferView(groupID: subscriptionGroupID)
                .visibleRelationship(.current)
        }
        .padding()
    }
}

visibleRelationshipパラメータは、表示されるサブスクリプションティアを制御します:.upgradeは1つ上のティアのプランを表示し、.downgradeは下のティアを、.crossgradeはより価値のある同等ティアを、.allはグループ内のすべてのプランを表示します。

サーバーサイドのレシート検証

サーバーサイド検証は、重要なサブスクリプションを持つアプリケーションには引き続き推奨されます。StoreKit 2は、最新の検証のためにJWS (JSON Web Signatures) を使用します。非推奨のverifyReceiptエンドポイントは、App Store Server APIに置き換えられました。

サーバー用のトランザクションデータの抽出

ServerValidation.swiftswift
import StoreKit

struct TransactionPayload: Codable {
    let transactionID: String
    let originalTransactionID: String
    let productID: String
    let purchaseDate: Date
    let expirationDate: Date?
    let jwsRepresentation: String
    let appTransactionID: String?  // iOS 18.4以降
}

actor ServerValidationService {

    private let apiEndpoint = "https://api.example.com/verify"

    func validateWithServer(_ transaction: Transaction) async throws -> Bool {
        // サーバー用のペイロードを作成
        let payload = TransactionPayload(
            transactionID: String(transaction.id),
            originalTransactionID: String(transaction.originalID),
            productID: transaction.productID,
            purchaseDate: transaction.purchaseDate,
            expirationDate: transaction.expirationDate,
            jwsRepresentation: transaction.jwsRepresentation ?? "",
            appTransactionID: transaction.appTransactionID.map { String($0) }
        )

        // JSONにエンコード
        let encoder = JSONEncoder()
        encoder.dateEncodingStrategy = .iso8601
        let jsonData = try encoder.encode(payload)

        // サーバーへ送信
        var request = URLRequest(url: URL(string: apiEndpoint)!)
        request.httpMethod = "POST"
        request.httpBody = jsonData
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")

        let (data, response) = try await URLSession.shared.data(for: request)

        guard let httpResponse = response as? HTTPURLResponse,
              httpResponse.statusCode == 200 else {
            return false
        }

        // サーバー応答をデコード
        let validationResponse = try JSONDecoder().decode(
            ValidationResponse.self,
            from: data
        )

        return validationResponse.isValid
    }
}

struct ValidationResponse: Codable {
    let isValid: Bool
    let subscriptionStatus: String?
}

jwsRepresentationにはサーバーで検証可能な暗号化署名が含まれています。新しいappTransactionIDにより、追加のサーバーラウンドトリップなしで正確な顧客追跡が可能です。

App Store Server API

AppleのApp Store Server APIは、JWSトークンによるサーバーサイド検証、リアルタイムサブスクリプションイベント用のServer Notifications V2、そしてステータスを直接クエリするためのトランザクションIDを受け入れる新しいSubscriptionStatus API(iOS 18.4以降)を提供します。

サーバー通知の設定

Server Notifications V2はリアルタイムイベントの受信を可能にします。

ServerNotificationTypes.swiftswift
enum NotificationType: String, Codable {
    case subscribed = "SUBSCRIBED"
    case didRenew = "DID_RENEW"
    case didFailToRenew = "DID_FAIL_TO_RENEW"
    case didChangeRenewalStatus = "DID_CHANGE_RENEWAL_STATUS"
    case expired = "EXPIRED"
    case gracePeriodExpired = "GRACE_PERIOD_EXPIRED"
    case offerRedeemed = "OFFER_REDEEMED"
    case priceIncrease = "PRICE_INCREASE"
    case refund = "REFUND"
    case renewalExtended = "RENEWAL_EXTENDED"
    case revoke = "REVOKE"
}

struct ServerNotification: Codable {
    let notificationType: NotificationType
    let subtype: String?
    let data: NotificationData

    struct NotificationData: Codable {
        let bundleId: String
        let environment: String
        let signedTransactionInfo: String
        let signedRenewalInfo: String?
    }
}

これらの通知により、サブスクリプション状態をバックエンドと同期して保つことができます。

エラー処理とエッジケース

面接ではしばしば、エラーシナリオの理解とその適切な処理がテストされます。iOSアーキテクチャパターンの詳細については、iOSシニア面接アーキテクチャガイドを参照してください。

StoreKit 2の一般的なエラー

PurchaseErrorHandler.swiftswift
import StoreKit

struct PurchaseErrorHandler {

    enum UserFacingError {
        case networkError
        case paymentFailed
        case notAuthorized
        case productNotAvailable
        case unknown

        var message: String {
            switch self {
            case .networkError:
                return "Connection failed. Please check your internet connection."
            case .paymentFailed:
                return "Payment failed. Please verify your payment information."
            case .notAuthorized:
                return "In-app purchases are disabled on this device."
            case .productNotAvailable:
                return "This product is not currently available."
            case .unknown:
                return "An unexpected error occurred."
            }
        }
    }

    static func handle(_ error: Error) -> UserFacingError {
        // StoreKitエラーかチェック
        if let storeKitError = error as? StoreKitError {
            switch storeKitError {
            case .networkError:
                return .networkError
            case .userCancelled:
                // エラーを表示しない、ユーザーがキャンセル
                return .unknown
            case .notAvailableInStorefront:
                return .productNotAvailable
            case .notEntitled:
                return .notAuthorized
            default:
                return .unknown
            }
        }

        // 購入エラー
        if let purchaseError = error as? Product.PurchaseError {
            switch purchaseError {
            case .invalidQuantity:
                return .unknown
            case .productUnavailable:
                return .productNotAvailable
            case .purchaseNotAllowed:
                return .notAuthorized
            default:
                return .unknown
            }
        }

        return .unknown
    }
}

明確なエラー処理は、ユーザー体験を向上させ、デバッグを簡素化します。

オフラインモードのサポート

OfflineCapabilityManager.swiftswift
import StoreKit

actor OfflineCapabilityManager {

    private let userDefaults = UserDefaults.standard
    private let entitlementsKey = "cached_entitlements"

    func cacheCurrentEntitlements() async {
        var entitlements: [String: Date] = [:]

        for await result in Transaction.currentEntitlements {
            if case .verified(let transaction) = result {
                entitlements[transaction.productID] = transaction.expirationDate
            }
        }

        // ローカルに保存
        if let data = try? JSONEncoder().encode(entitlements) {
            userDefaults.set(data, forKey: entitlementsKey)
        }
    }

    func getCachedEntitlements() -> [String: Date] {
        guard let data = userDefaults.data(forKey: entitlementsKey),
              let entitlements = try? JSONDecoder().decode(
                  [String: Date].self,
                  from: data
              ) else {
            return [:]
        }

        // 期限切れのエンタイトルメントをフィルタ
        let now = Date()
        return entitlements.filter { _, expiration in
            expiration > now
        }
    }

    func hasValidEntitlement(for productID: String) -> Bool {
        let cached = getCachedEntitlements()
        return cached[productID] != nil
    }
}

ローカルキャッシングにより、接続がなくてもプレミアム機能へのアクセスが維持されます。

本番環境のベストプラクティス

推奨アーキテクチャ

SubscriptionService.swiftswift
import StoreKit
import Combine

@MainActor
final class SubscriptionService: ObservableObject {

    static let shared = SubscriptionService()

    @Published private(set) var products: [Product] = []
    @Published private(set) var purchasedProductIDs: Set<String> = []
    @Published private(set) var subscriptionStatus: SubscriptionStatus = .none

    private var transactionObserver: Task<Void, Never>?

    enum SubscriptionStatus {
        case none
        case active(expirationDate: Date)
        case expired
        case inGracePeriod
    }

    private init() {
        // トランザクション監視を開始
        startTransactionObserver()

        // 初期状態をロード
        Task {
            await loadProducts()
            await updateSubscriptionStatus()
        }
    }

    private func startTransactionObserver() {
        transactionObserver = Task.detached(priority: .background) {
            for await result in Transaction.updates {
                if case .verified(let transaction) = result {
                    await transaction.finish()
                    await self.updateSubscriptionStatus()
                }
            }
        }
    }

    func loadProducts() async {
        do {
            let productIDs = [
                "com.app.subscription.monthly",
                "com.app.subscription.yearly"
            ]
            products = try await Product.products(for: productIDs)
        } catch {
            print("Failed to load products: (error)")
        }
    }

    func purchase(_ product: Product) async throws {
        let result = try await product.purchase()

        switch result {
        case .success(let verification):
            if case .verified(let transaction) = verification {
                await transaction.finish()
                await updateSubscriptionStatus()
            }
        case .userCancelled:
            break
        case .pending:
            break
        @unknown default:
            break
        }
    }

    func updateSubscriptionStatus() async {
        var purchasedIDs: Set<String> = []
        var latestSubscription: Transaction?

        for await result in Transaction.currentEntitlements {
            if case .verified(let transaction) = result {
                purchasedIDs.insert(transaction.productID)

                if transaction.productType == .autoRenewable {
                    if latestSubscription == nil ||
                       transaction.purchaseDate > latestSubscription!.purchaseDate {
                        latestSubscription = transaction
                    }
                }
            }
        }

        purchasedProductIDs = purchasedIDs

        if let subscription = latestSubscription,
           let expiration = subscription.expirationDate {
            if expiration > Date() {
                subscriptionStatus = .active(expirationDate: expiration)
            } else {
                subscriptionStatus = .expired
            }
        } else {
            subscriptionStatus = .none
        }
    }

    deinit {
        transactionObserver?.cancel()
    }
}

この集中化されたアーキテクチャは、アプリケーション全体での購入状態管理を簡素化します。

Sources

StoreKit 2面接の重要ポイント

StoreKit 2は、アプリ内課金管理をモダンで安全な開発体験に変革します。面接で覚えておくべき重要なポイント:

  • ネイティブasync/await APIにより、iOS 18で非推奨となったStoreKit 1の複雑なデリゲートやコールバックが不要に
  • VerificationResultによる自動検証がトランザクションの暗号化検証を処理
  • Transaction.updatesは、更新やファミリー共有を含むバックグラウンドトランザクション用の非同期ストリームを提供
  • Transaction.currentEntitlements(for:)は、複数のエンタイトルメントを処理するために非推奨のcurrentEntitlement(for:)を置き換え
  • appTransactionIDフィールド(iOS 18.4以降)により、サーバー間通信なしで顧客追跡が可能
  • SubscriptionOfferViewは、アップグレード、ダウングレード、クロスグレードオプション用のvisibleRelationshipを備えたサブスクリプションマーチャンダイジング用の組み込みUIを提供
  • プロモーションオファーはPromotionOfferV2SignatureCreatorを使用してJWSで署名可能
  • オファーコードは消耗型、非消耗型、非更新型サブスクリプションをサポート(iOS 16.3以降)
  • サーバー検証は、非推奨のverifyReceiptエンドポイントに代わり、JWSトークンを使用したApp Store Server APIを使用
  • コンテンツ配信後は常にtransaction.finish()を呼び出し、予期しない復元動作を防止
  • 接続がない状態でもシームレスなユーザー体験のため、オフラインエンタイトルメントキャッシングを実装

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

今日のチャレンジ

iOS のバグを見つけられますか

実際のコード、隠れたバグ、1日1回。アカウントなしで試せます。

Anthony Fillion-Maillet

執筆

Anthony Fillion-Maillet

SharpSkill 創業者

10 年以上フルスタック開発に携わっています。SharpSkill を運営し、ここで公開される内容に責任を負っています。

2026年10月20日 更新

タグ

#ios
#storekit
#in-app-purchase
#swift
#interview

共有

関連記事