StoreKit 2 Interview: Abonnementverwaltung und Beleg-Validierung
Beherrschen Sie iOS-Interview-Fragen zu StoreKit 2, Abonnementverwaltung, Beleg-Validierung und In-App-Kauf-Implementierung mit praktischen Swift-Codebeispielen.

StoreKit 2 stellt einen grundlegenden Wandel in der Handhabung von In-App-Käufen unter iOS dar. Mit iOS 15 eingeführt, vereinfacht dieses moderne Framework den Code für Abonnementverwaltung und Transaktionsvalidierung drastisch. Technische iOS-Interviews behandeln dieses Thema regelmäßig, um die Expertise von Entwicklern in der App-Monetarisierung zu bewerten.
StoreKit 2 nutzt eine native Swift-API mit async/await und macht komplexe Callbacks und clientseitige Beleg-Validierung überflüssig. Interviewer schätzen Kandidaten, die die grundlegenden Unterschiede zu StoreKit 1 klar artikulieren können.
Überblick über die StoreKit 2-Architektur
StoreKit 2 basiert auf einer modernen Architektur, die Swift Concurrency vollständig nutzt. Im Gegensatz zu StoreKit 1 sind alle Operationen asynchron und verwenden native Swift-Typen.
Verfügbare Produkte abrufen
Der erste Schritt besteht darin, die in App Store Connect konfigurierten Produkte zu laden. StoreKit 2 vereinfacht diese Operation mit einer deklarativen API.
import StoreKit
actor ProductService {
// In App Store Connect konfigurierte Produkt-Identifikatoren
private let productIdentifiers: Set<String> = [
"com.app.subscription.monthly",
"com.app.subscription.yearly",
"com.app.premium.lifetime"
]
// Produkt-Cache zur Vermeidung wiederholter Netzwerkaufrufe
private var cachedProducts: [Product] = []
func loadProducts() async throws -> [Product] {
// Cache zurückgeben, falls verfügbar
guard cachedProducts.isEmpty else {
return cachedProducts
}
// Asynchrone Anfrage an den 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 }
}
}Die Verwendung eines actor gewährleistet Thread-Sicherheit bei der Verwaltung des Produkt-Caches.
StoreKit 2-Produkttypen
StoreKit 2 unterscheidet klar zwischen verschiedenen Produkttypen. Das Verständnis dieser Unterscheidung ist für Interviews unerlässlich.
import StoreKit
extension Product.ProductType {
var displayName: String {
switch self {
case .consumable:
// Verbrauchsprodukte (Credits, Leben usw.)
return "Consumable"
case .nonConsumable:
// Permanente Käufe (Funktionsfreischaltung)
return "Non-Consumable"
case .autoRenewable:
// Abonnements mit automatischer Verlängerung
return "Auto-Renewable Subscription"
case .nonRenewable:
// Abonnements ohne Verlängerung (temporärer Pass)
return "Non-Renewable Subscription"
@unknown default:
return "Unknown"
}
}
}Automatisch verlängernde Abonnements stellen das dominante Geschäftsmodell für moderne iOS-Anwendungen dar.
Kauftransaktionen verarbeiten
Der Kaufprozess in StoreKit 2 wird dank async/await linear. Schluss mit Delegates und verschachtelten Callbacks aus StoreKit 1.
Einen Kauf einleiten
import StoreKit
actor PurchaseManager {
enum PurchaseError: Error {
case productNotFound
case purchaseCancelled
case purchasePending
case verificationFailed
case unknown
}
func purchase(_ product: Product) async throws -> Transaction {
// Kauf mit System-Modal einleiten
let result = try await product.purchase()
switch result {
case .success(let verification):
// Transaktionssignatur überprüfen
let transaction = try checkVerification(verification)
// Transaktion als abgeschlossen markieren
await transaction.finish()
return transaction
case .userCancelled:
// Benutzer hat das Kauf-Modal abgebrochen
throw PurchaseError.purchaseCancelled
case .pending:
// Kauf ausstehend (Elternfreigabe usw.)
throw PurchaseError.purchasePending
@unknown default:
throw PurchaseError.unknown
}
}
private func checkVerification<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .verified(let safe):
// Transaktion vom App Store signiert und überprüft
return safe
case .unverified(_, let error):
// Ungültige oder beschädigte Signatur
throw PurchaseError.verificationFailed
}
}
}Die Methode finish() ist entscheidend: Sie signalisiert dem App Store, dass der Inhalt an den Benutzer ausgeliefert wurde.
Vergessen Sie niemals, transaction.finish() nach der Inhaltsauslieferung aufzurufen. Eine nicht abgeschlossene Transaktion wird beim nächsten App-Start wiederhergestellt und verursacht unerwartetes Verhalten.
Hintergrundtransaktionen abhören
StoreKit 2 führt Transaction.updates ein, einen asynchronen Stream, der Transaktionen ausgibt, die außerhalb der App stattfinden (Verlängerungen, Family-Sharing-Käufe usw.).
import StoreKit
actor TransactionObserver {
private var updateTask: Task<Void, Never>?
func startObserving() {
// Vorherige Beobachtung abbrechen
updateTask?.cancel()
updateTask = Task(priority: .background) {
// Unendlicher Stream von Transaktionsaktualisierungen
for await result in Transaction.updates {
do {
let transaction = try self.checkVerification(result)
// Transaktion entsprechend ihrem Typ verarbeiten
await self.handleTransaction(transaction)
// Transaktion immer abschließen
await transaction.finish()
} catch {
// Fehler für Debugging protokollieren
print("Transaction verification failed: \(error)")
}
}
}
}
func stopObserving() {
updateTask?.cancel()
updateTask = nil
}
private func handleTransaction(_ transaction: Transaction) async {
switch transaction.productType {
case .autoRenewable:
// Abonnementstatus aktualisieren
await updateSubscriptionStatus(transaction)
case .nonConsumable:
// Premium-Funktion freischalten
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 {
// Implementierung der Statusaktualisierung
}
private func unlockFeature(_ productID: String) async {
// Implementierung der Funktionsfreischaltung
}
enum PurchaseError: Error {
case verificationFailed
}
}Die Transaktionsbeobachtung sollte unmittelbar beim App-Start beginnen, um keine Aktualisierungen zu verpassen.
Häufige Interview-Fragen zu StoreKit 2-Abonnements
iOS-Interviews enthalten oft spezifische Fragen zur Abonnementverwaltung. Hier sind die häufigsten mit detaillierten Antworten.
Frage 1: Wie überprüft man den aktuellen Abonnementstatus?
StoreKit 2 bietet direkten Zugriff auf aktive Berechtigungen über Transaction.currentEntitlements.
import StoreKit
struct SubscriptionStatusChecker {
struct SubscriptionStatus {
let isActive: Bool
let expirationDate: Date?
let willRenew: Bool
let productID: String?
}
static func checkCurrentStatus() async -> SubscriptionStatus {
// Alle aktiven Transaktionen abrufen
var latestTransaction: Transaction?
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result {
// Nur Abonnements filtern
if transaction.productType == .autoRenewable {
// Aktuellste Transaktion behalten
if latestTransaction == nil ||
transaction.purchaseDate > latestTransaction!.purchaseDate {
latestTransaction = transaction
}
}
}
}
guard let transaction = latestTransaction else {
return SubscriptionStatus(
isActive: false,
expirationDate: nil,
willRenew: false,
productID: nil
)
}
// Prüfen, ob das Abonnement noch aktiv ist
let isActive = transaction.expirationDate ?? Date.distantPast > Date()
// Verlängerungsinformationen abrufen
let renewalInfo = await getRenewalInfo(for: transaction.productID)
return SubscriptionStatus(
isActive: isActive,
expirationDate: transaction.expirationDate,
willRenew: renewalInfo?.willAutoRenew ?? false,
productID: transaction.productID
)
}
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
}
// Verlängerungsstatus abrufen
let status = try? await subscription.status.first
return status?.renewalInfo
}
}Dieser Ansatz liefert den genauen Abonnementstatus ohne Serveraufrufe.
Bereit für deine iOS-Interviews?
Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.
Frage 2: Wie verarbeitet man kostenlose Testphasen und Werbeangebote?
StoreKit 2 vereinfacht den Zugriff auf Angebotsinformationen. Interviewer testen häufig das Verständnis der verschiedenen Angebotstypen.
import StoreKit
struct OfferManager {
enum OfferType {
case freeTrial(duration: String)
case payAsYouGo(periods: Int, price: Decimal)
case payUpFront(duration: String, price: Decimal)
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:
// Kostenloser Testzeitraum
return .freeTrial(duration: formatPeriod(offer.period))
case .payAsYouGo:
// Reduzierter Preis über mehrere Perioden
return .payAsYouGo(
periods: offer.periodCount,
price: offer.price
)
case .payUpFront:
// Einmalzahlung für einen Zeitraum
return .payUpFront(
duration: formatPeriod(offer.period),
price: offer.price
)
@unknown default:
return .none
}
}
static func checkEligibility(for product: Product) async -> Bool {
// Prüfen, ob der Benutzer vom Angebot profitieren kann
guard let subscription = product.subscription else {
return false
}
// isEligibleForIntroOffer zeigt an, ob ein neuer Abonnent vorliegt
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 ""
}
}
}Die Methode isEligibleForIntroOffer ist unerlässlich, um die Anzeige nicht verfügbarer Angebote zu vermeiden.
Frage 3: Wie stellt man frühere Käufe wieder her?
Die Wiederherstellung von Käufen ist gemäß den App Store-Richtlinien eine obligatorische Funktion.
import StoreKit
actor RestoreManager {
struct RestoreResult {
let restoredProducts: [String]
let activeSubscriptions: [String]
}
func restorePurchases() async throws -> RestoreResult {
var restoredProducts: [String] = []
var activeSubscriptions: [String] = []
// Mit dem App Store synchronisieren
try await AppStore.sync()
// Alle Benutzertransaktionen durchlaufen
for await result in Transaction.currentEntitlements {
guard case .verified(let transaction) = result else {
continue
}
switch transaction.productType {
case .nonConsumable:
// Permanente Käufe wiederherstellen
restoredProducts.append(transaction.productID)
case .autoRenewable:
// Prüfen, ob Abonnement aktiv ist
if let expiration = transaction.expirationDate,
expiration > Date() {
activeSubscriptions.append(transaction.productID)
}
default:
// Verbrauchsprodukte sind nicht wiederherstellbar
break
}
}
return RestoreResult(
restoredProducts: restoredProducts,
activeSubscriptions: activeSubscriptions
)
}
}AppStore.sync() erzwingt die Synchronisation mit Apples Servern, nützlich nach einer Neuinstallation.
Serverseitige Beleg-Validierung
Die serverseitige Validierung bleibt für Anwendungen mit kritischen Abonnements empfohlen. StoreKit 2 führt JWS (JSON Web Signatures) für moderne Validierung ein.
Transaktionsdaten für den Server extrahieren
import StoreKit
struct TransactionPayload: Codable {
let transactionID: String
let originalTransactionID: String
let productID: String
let purchaseDate: Date
let expirationDate: Date?
let jwsRepresentation: String
}
actor ServerValidationService {
private let apiEndpoint = "https://api.example.com/verify"
func validateWithServer(_ transaction: Transaction) async throws -> Bool {
// Payload für den Server erstellen
let payload = TransactionPayload(
transactionID: String(transaction.id),
originalTransactionID: String(transaction.originalID),
productID: transaction.productID,
purchaseDate: transaction.purchaseDate,
expirationDate: transaction.expirationDate,
jwsRepresentation: transaction.jwsRepresentation ?? ""
)
// In JSON kodieren
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let jsonData = try encoder.encode(payload)
// An Server senden
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
}
// Serverantwort dekodieren
let validationResponse = try JSONDecoder().decode(
ValidationResponse.self,
from: data
)
return validationResponse.isValid
}
}
struct ValidationResponse: Codable {
let isValid: Bool
let subscriptionStatus: String?
}Die jwsRepresentation enthält die kryptografische Signatur, die vom Server überprüfbar ist.
Apple stellt jetzt die App Store Server API für serverseitige Validierung bereit. Diese moderne API ersetzt den veralteten verifyReceipt-Endpunkt und bietet erweiterte Funktionen wie Server-zu-Server-Benachrichtigungen.
Server-Benachrichtigungen konfigurieren
Server Notifications V2 ermöglichen den Empfang von Ereignissen in Echtzeit.
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?
}
}Diese Benachrichtigungen ermöglichen die Synchronisation des Abonnementstatus mit dem Backend.
Fehlerbehandlung und Randfälle
Interviews testen häufig das Verständnis von Fehlerszenarien und deren ordnungsgemäße Behandlung.
Häufige StoreKit 2-Fehler
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 {
// Prüfen, ob es ein StoreKit-Fehler ist
if let storeKitError = error as? StoreKitError {
switch storeKitError {
case .networkError:
return .networkError
case .userCancelled:
// Keine Fehleranzeige, Benutzer hat abgebrochen
return .unknown
case .notAvailableInStorefront:
return .productNotAvailable
case .notEntitled:
return .notAuthorized
default:
return .unknown
}
}
// Kauffehler
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
}
}Klare Fehlerbehandlung verbessert die Benutzererfahrung und vereinfacht das Debugging.
Offline-Modus-Unterstützung
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
}
}
// Lokal speichern
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 [:]
}
// Abgelaufene Berechtigungen filtern
let now = Date()
return entitlements.filter { _, expiration in
expiration > now
}
}
func hasValidEntitlement(for productID: String) -> Bool {
let cached = getCachedEntitlements()
return cached[productID] != nil
}
}Lokales Caching erhält den Zugriff auf Premium-Funktionen auch ohne Konnektivität.
Best Practices für die Produktion
Empfohlene Architektur
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() {
// Transaktionsbeobachtung starten
startTransactionObserver()
// Anfangszustand laden
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()
}
}Diese zentralisierte Architektur vereinfacht die Verwaltung des Kaufstatus in der gesamten Anwendung.
Fazit
StoreKit 2 verwandelt die Verwaltung von In-App-Käufen in eine moderne und sichere Entwicklungserfahrung. Wichtige Punkte für ein Interview:
✅ Native async/await-API: Schluss mit komplexen Delegates und Callbacks aus StoreKit 1
✅ Automatische Verifizierung: VerificationResult übernimmt die kryptografische Validierung von Transaktionen
✅ Transaction.updates: asynchroner Stream für Hintergrundtransaktionen
✅ Transaction.currentEntitlements: direkter Zugriff auf aktive Berechtigungen des Benutzers
✅ Werbeangebote: isEligibleForIntroOffer zur Berechtigungsprüfung
✅ Server-Validierung: JWS und App Store Server API für erhöhte Sicherheit
✅ transaction.finish(): obligatorisch nach der Inhaltsauslieferung
✅ Offline-Handling: lokales Caching von Berechtigungen für reibungsloses Erlebnis
Fang an zu üben!
Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.
Tags
Teilen
Verwandte Artikel

Swift Testing Framework Interview 2026: Makros #expect und #require vs XCTest
Beherrsche das neue Swift Testing Framework für iOS-Interviews: Makros #expect und #require, XCTest-Migration, fortgeschrittene Muster und häufige Fallstricke.

iOS Push Notifications Interview 2026: APNs, Tokens und Troubleshooting
Umfassender Leitfaden zur Vorbereitung auf iOS-Interviews zu Push Notifications, APNs, Token-Verwaltung und Troubleshooting. Häufige Fragen mit ausführlichen Antworten.

Die 25 wichtigsten Swift-Interviewfragen für iOS-Entwickler
Vorbereitung auf iOS-Vorstellungsgespräche mit den 25 häufigsten Swift-Fragen: Optionals, Closures, ARC, Protokolle, async/await und fortgeschrittene Muster.