Maestría en StoreKit 2: Implementando Compras In-App y Suscripciones en iOS con Swift
Guía completa y avanzada para implementar StoreKit 2 en iOS. Descubre cómo gestionar productos, procesar transacciones seguras, manejar estados de suscripción y ofrecer una experiencia de pago fluida en tus apps utilizando SwiftUI.
Introducción a StoreKit 2 en el Ecosistema Apple
La monetización es un pilar fundamental en el ciclo de vida de cualquier aplicación moderna. Con la llegada de StoreKit 2, Apple rediseñó por completo su framework de compras dentro de la aplicación (In-App Purchases o IAP), ofreciendo una API construida desde cero aprovechando las ventajas de Swift Concurrency (async/await) y un manejo de tipos mucho más seguro y moderno.
En este tutorial exhaustivo, exploraremos cómo configurar, implementar y gestionar un flujo completo de compras y suscripciones utilizando StoreKit 2 junto con SwiftUI. Olvídate del código boilerplate heredado de Objective-C y de la complejidad innecesaria del StoreKit clásico.
🛠️ Requisitos Previos y Configuración en App Store Connect
Antes de escribir una sola línea de código en Xcode, debemos preparar nuestro entorno de desarrollo y configurar los productos en el portal de Apple.
1. Creación de Identificadores y Certificados
Asegúrate de tener una cuenta de desarrollador de Apple activa. Los pasos iniciales requieren:
- Un App ID configurado con la capacidad de In-App Purchase habilitada.
- Un entorno de pruebas local configurado (Xcode StoreKit Configuration File) para no depender de la App Store real durante el desarrollo.
2. Estructura de Productos Recomendada
Para una aplicación típica de productividad o contenido, la siguiente tabla resume los tipos de productos que implementaremos:
| Identificador del Producto | Tipo de Producto | Descripción | Precio Sugerido |
|---|---|---|---|
| --- | --- | --- | --- |
com.tuempresa.app.monthly | Suscripción Auto-renovable | Acceso ilimitado mensual | $4.99 / mes |
com.tuempresa.app.yearly | Suscripción Auto-renovable | Acceso ilimitado anual con descuento | $39.99 / año |
| --- | --- | --- | --- |
com.tuempresa.app.lifetime | Compra Única (Non-Consumable) | Acceso vitalicio sin restricciones | $89.99 única vez |
📦 Creación de un Archivo de Configuración Local de StoreKit
Una de las mejores características para desarrolladores en Xcode es la capacidad de simular la App Store mediante un archivo .storekit.
- En Xcode, ve a File > New > File... y busca StoreKit Configuration File.
- Nómbralo
Products.storekit. - Añade tus productos haciendo clic en el botón
+en la esquina inferior izquierda. - Asegúrate de configurar correctamente los IDs para que coincidan con los que usarás en tu código.
🚀 Implementación del StoreManager con Swift Concurrency
Vamos a crear un controlador centralizado que gestione la carga de productos, las compras y el seguimiento de los derechos del usuario (entitlements).
Crea un archivo llamado StoreManager.swift:
import Foundation
import StoreKit
import SwiftUI
@MainActor
class StoreManager: ObservableObject {
@Published private(set) var products: [Product] = []
@Published private(set) var purchasedProductIDs = Set<String>()
@Published var subscriptionGroupStatus: RenewalState?
private var updateListenerTask: Task<Void, Error>? = nil
let productIds = [
"com.tuempresa.app.monthly",
"com.tuempresa.app.yearly",
"com.tuempresa.app.lifetime"
]
init() {
updateListenerTask = listenForTransactions()
Task {
await requestProducts()
await updateCustomerProductStatus()
}
}
deinit {
updateListenerTask?.cancel()
}
func requestProducts() async {
do {
let storeProducts = try await Product.products(for: productIds)
self.products = storeProducts.sorted(by: { $0.price < $1.price })
} catch {
print("Failed to fetch products from App Store: \(error)")
}
}
func purchase(_ product: Product) async throws -> Transaction? {
let result = try await product.purchase()
switch result {
let .success(verification):
let transaction = try checkVerified(verification)
await transaction.finish()
await updateCustomerProductStatus()
return transaction
case .userCancelled:
return nil
case .pending:
return nil
@unknown default:
return nil
}
}
func updateCustomerProductStatus() async {
var purchasedIDs = Set<String>()
for await result in Transaction.currentEntitlements {
do {
let transaction = try checkVerified(result)
if transaction.revocationDate == nil {
purchasedIDs.insert(transaction.productID)
} else {
purchasedIDs.remove(transaction.productID)
}
} catch {
print("Failed to verify transaction: \(error)")
}
}
self.purchasedProductIDs = purchasedIDs
}
private func listenForTransactions() -> Task<Void, Error> {
return Task.detached {
for await result in Transaction.updates {
do {
let transaction = try self.checkVerified(result)
await transaction.finish()
await self.updateCustomerProductStatus()
} catch {
print("Transaction failed verification: \(error)")
}
}
}
}
private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .unverified:
throw StoreError.failedVerification
case .verified(let safe):
return safe
}
}
}
enum StoreError: Error {
case failedVerification
}
🎨 Creación de la Interfaz de Usuario con SwiftUI
Ahora diseñaremos una vista moderna y atractiva para presentar los productos disponibles al usuario, permitiéndole comprar de forma intuitiva.
Crea una vista llamada StoreView.swift:
import SwiftUI
import StoreKit
struct StoreView: View {
@EnvironmentObject var storeManager: StoreManager
@State private var isPurchasing = false
@State private var errorMessage: String?
@State private var showingError = false
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: 20) {
headerView
if storeManager.products.isEmpty {
ProgressView("Cargando ofertas...")
.padding(.top, 50)
} else {
ForEach(storeManager.products) { product in
ProductCardView(product: product, isPurchased: storeManager.purchasedProductIDs.contains(product.id)) {
Task {
await buyProduct(product)
}
}
}
}
restorePurchasesButton
}
.padding()
}
.navigationTitle("Tienda Premium")
.alert("Error en la Compra", isPresented: $showingError, actions: {
Button("OK", role: .cancel) {}
}, message: {
Text(errorMessage ?? "Ocurrió un error desconocido.")
})
}
}
private var headerView: some View {
VStack(spacing: 8) {
Image(systemName: "crown.fill")
.font(.system(size: 50))
.foregroundColor(.yellow)
Text("Desbloquea todo el potencial")
.font(.title2)
.fontWeight(.bold)
Text("Elige el plan que mejor se adapte a tus necesidades y disfruta de contenido exclusivo sin límites.")
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
.padding(.bottom, 10)
}
private var restorePurchasesButton: some View {
Button(action: {
Task {
await storeManager.updateCustomerProductStatus()
}
}) {
Text("Restaurar Compras")
.font(.footnote)
.foregroundColor(.accentColor)
}
.padding(.top, 20)
}
private func buyProduct(_ product: Product) async {
isPurchasing = true
do {
_ = try await storeManager.purchase(product)
} catch {
errorMessage = error.localizedDescription
showingError = true
}
isPurchasing = false
}
}
struct ProductCardView: View {
let product: Product
let isPurchased: Bool
let onPurchase: () -> Void
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(product.displayName)
.font(.headline)
Text(product.description)
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
if isPurchased {
Text("Adquirido")
.font(.subheadline)
.fontWeight(.semibold)
.foregroundColor(.green)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(Color.green.opacity(0.1))
.cornerRadius(8)
} else {
Button(action: onPurchase) {
Text(product.displayPrice)
.font(.subheadline)
.fontWeight(.bold)
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(Color.accentColor)
.foregroundColor(.white)
.cornerRadius(8)
}
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2)
}
}
🔄 Gestión Avanzada de Suscripciones y Renewal States
Para aplicaciones con suscripciones auto-renovables, StoreKit 2 proporciona herramientas muy potentes para verificar el estado de renovación a través de Product.SubscriptionInfo.
Comprobación del Estado de Renovación
Añade la siguiente función a tu StoreManager para consultar el estado actual de las suscripciones y saber si están activas, en período de gracia o canceladas:
extension StoreManager {
func checkSubscriptionStatus(for product: Product) async {
guard let subscription = product.subscription else { return }
do {
let statuses = try await subscription.status
for status in statuses {
switch status.state {
case .subscribed:
print("Suscripción activa y al día.")
case .expired:
print("La suscripción ha expirado.")
case .inBillingRetryPeriod:
print("Error de cobro, en período de reintento.")
case .inGracePeriod:
print("Suscripción en período de gracia.")
case .revoked:
print("Suscripción revocada por Apple o reembolso.")
@unknown default:
break
}
}
} catch {
print("Error al comprobar el estado de la suscripción: \(error)")
}
}
}
🧪 Pruebas Locales y Depuración
Para probar tu implementación de StoreKit 2 sin necesidad de crear cuentas sandbox reales en App Store Connect:
- Selecciona tu esquema en Xcode (Product > Scheme > Edit Scheme...).
- En la pestaña Run, selecciona Options.
- En el campo StoreKit Configuration, selecciona tu archivo
Products.storekitcreado anteriormente. - Ejecuta la aplicación en el simulador. Al realizar una compra, aparecerá la hoja de pago nativa simulada de StoreKit.
Preguntas Frecuentes (FAQ)
¿Es necesario un servidor backend para validar las compras en StoreKit 2?
No es estrictamente necesario. StoreKit 2 utiliza firmas JWS (JSON Web Signatures) que permiten verificar la autenticidad de las transacciones directamente en el dispositivo del cliente. Sin embargo, si tu app requiere sincronización de contenido en la nube o licencias multiplataforma, se recomienda implementar un servidor propio de validación.¿Cómo manejo los reembolsos y cancelaciones de usuarios?
StoreKit 2 emite notificaciones automáticas a través de `Transaction.updates` cuando una transacción es revocada o reembolsada. Tu `StoreManager` debe escuchar estos cambios continuamente para revocar el acceso de forma inmediata.Conclusión y Próximos Pasos
Has construido con éxito un sistema de monetización moderno, robusto y escalable utilizando StoreKit 2 y Swift. Gracias al poder de async/await y las estructuras nativas de SwiftUI, tu código es ahora más limpio, fácil de mantener y preparado para las exigencias de la App Store actual.
Como siguientes pasos recomendados, puedes explorar la integración de ofertas promocionales (Promotional Offers), códigos de oferta (Offer Codes) y la implementación de notificaciones del servidor de App Store (App Store Server Notifications) para mantener tu backend sincronizado en tiempo real.
Intermedio SwiftUI StoreKit 2
Tutoriales relacionados
- Maestría en Live Activities: Integrando Dynamic Island y Pantalla Bloqueada en iOS con Swift y ActivityKitadvanced12 min
- Navegación Avanzada en iOS: Coordinadores y Flow Controllers con SwiftUIintermediate18 min
- Maestría en AudioKit: Creación de Apps de Música y Procesamiento de Audio en iOS con Swiftintermediate25 min
- ¡Domina Swift Concurrency! Asincronía y Paralelismo en iOS con async/await y Actoresintermediate15 min
- Desbloquea ARKit: Realidad Aumentada en iOS con SwiftUI y USDZintermediate20 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!