Desarrollar una aplicación increíble es solo la mitad de la batalla. La otra mitad, a menudo la más compleja para los desarrolladores independientes y pequeñas startups, es convertir ese código en ingresos sostenibles.
Durante años, integrar compras dentro de la aplicación (In-App Purchases o IAP) en iOS fue una tarea desalentadora. Implicaba lidiar con SKPaymentQueue, delegados complejos, validación de recibos con OpenSSL y servidores backend propios. Afortunadamente, esos días han quedado atrás.
Con la llegada de StoreKit 2 y su integración profunda con la concurrencia de Swift (async/await), implementar un sistema de monetización robusto en SwiftUI es ahora una experiencia fluida, segura y casi agradable.
En este tutorial de “clase magistral”, construiremos desde cero un sistema de compras para una aplicación multiplataforma (iOS, macOS, watchOS), cubriendo desde la configuración en App Store Connect hasta la interfaz de usuario de la “Paywall”.
Parte 1: Conceptos Fundamentales y Estrategia
Antes de escribir una sola línea de código, debemos entender qué estamos vendiendo. Apple clasifica las compras en cuatro tipos:
- Consumibles: Se usan una vez y se agotan (ej. monedas en un juego).
- No Consumibles: Se compran una vez y duran para siempre (ej. desbloquear “Modo Pro”, filtros de fotos). Nos centraremos en este para el tutorial.
- Suscripciones Renovables: Cobro recurrente (ej. Netflix, Spotify).
- Suscripciones No Renovables: Acceso por tiempo limitado (ej. pase de temporada).
Para este tutorial, simularemos una aplicación de productividad donde el usuario puede comprar una “Versión Premium” (Lifetime) que desbloquea funcionalidades ilimitadas.
Parte 2: Configuración del Entorno (Sin Código Aún)
El error número uno de los desarrolladores es saltar a Xcode sin configurar los metadatos.
1. App Store Connect
Para probar compras reales (incluso en modo Sandbox), necesitas definir los productos en los servidores de Apple.
- Entra a App Store Connect.
- Ve a “Mis Apps” y selecciona tu aplicación.
- En la barra lateral, busca Monetización > Compras dentro de la app.
- Haz clic en el botón
+y selecciona No consumible. - ID del producto: Esto es crucial. Usa la notación inversa de dominio. Ejemplo:
com.tuempresa.tuapp.premium_lifetime. - Rellena el nombre de referencia y el precio (ej. Tier 5 – $4.99).
- Importante: Añade una localización (Nombre y Descripción visibles para el usuario) y una captura de pantalla (puedes usar una imagen vacía temporalmente para pruebas), o el estado quedará en “Metadatos faltantes” y no podrás probarlo.
2. Configuración en Xcode
- Abre tu proyecto en Xcode.
- Selecciona el target de tu app -> Pestaña Signing & Capabilities.
- Haz clic en
+ Capabilityy busca In-App Purchase. Esto añade el framework al proyecto.
Parte 3: El Archivo de Configuración de StoreKit (El Secreto de la Productividad)
Antiguamente, para probar compras necesitabas un dispositivo físico y cuentas de Sandbox, lo cual era lento y propenso a fallos. Xcode ahora permite pruebas locales.
- En Xcode,
File > New > File... - Busca StoreKit Configuration File.
- Nómbralo
Products.storekity guárdalo en tu proyecto. - Abre el archivo. Verás una interfaz visual.
- Haz clic en
+abajo a la izquierda y selecciona Add Non-Consumable In-App Purchase. - Introduce el mismo Product ID que creaste en App Store Connect (
com.tuempresa.tuapp.premium_lifetime). - Ponle un precio y una descripción para pruebas.
Paso Crítico: Para que Xcode use este archivo en lugar de conectar a los servidores de Apple:
- Haz clic en el esquema de tu app (arriba, junto al simulador).
- Edit Scheme…
- En la pestaña Run, ve a Options.
- En StoreKit Configuration, selecciona tu archivo
Products.storekit.
Ahora puedes simular compras en el simulador de iOS sin conexión a internet y con respuestas instantáneas.
Parte 4: La Lógica de Negocio (StoreManager)
Vamos a crear una clase StoreManager que será el cerebro de nuestras operaciones. Usaremos el patrón ObservableObjectpara que nuestras vistas de SwiftUI reaccionen a los cambios.
Crea un archivo llamado StoreManager.swift:
import Foundation
import StoreKit
// Alias para simplificar
typealias Transaction = StoreKit.Transaction
typealias RenewalInfo = StoreKit.Product.SubscriptionInfo.RenewalInfo
typealias RenewalState = StoreKit.Product.SubscriptionInfo.RenewalState
public enum StoreError: Error {
case failedVerification
}
@MainActor
class StoreManager: ObservableObject {
// Los productos disponibles para comprar
@Published private(set) var products: [Product] = []
// Estado de si el usuario ha comprado la versión Pro
@Published private(set) var hasUnlockedPro: Bool = false
// Los IDs de tus productos (deben coincidir con App Store Connect y .storekit)
private let productIds: [String] = ["com.tuempresa.tuapp.premium_lifetime"]
// Tarea para escuchar actualizaciones de transacciones en segundo plano
var updateListenerTask: Task<Void, Error>? = nil
init() {
// Al iniciar, empezamos a escuchar transacciones
updateListenerTask = listenForTransactions()
// Iniciamos la carga de productos y verificamos compras previas
Task {
await requestProducts()
await updateCustomerProductStatus()
}
}
deinit {
updateListenerTask?.cancel()
}
// MARK: - 1. Obtener Productos
func requestProducts() async {
do {
// Llamada asíncrona a Apple para obtener detalles (precio, moneda, etc.)
let products = try await Product.products(for: productIds)
self.products = products.sorted(by: { $0.price < $1.price })
} catch {
print("Error al obtener productos: \(error)")
}
}
// MARK: - 2. Comprar Producto
func purchase(_ product: Product) async throws {
let result = try await product.purchase()
switch result {
case .success(let verification):
// La compra fue exitosa, ahora verificamos la firma criptográfica
let transaction = try checkVerified(verification)
// Entregamos el contenido al usuario
await updateCustomerProductStatus()
// Le decimos a StoreKit que hemos terminado de procesar
await transaction.finish()
case .userCancelled, .pending:
break
default:
break
}
}
// MARK: - 3. Verificar Estado del Usuario (Restore)
func updateCustomerProductStatus() async {
// Iteramos sobre los derechos (entitlements) actuales del usuario
for await result in Transaction.currentEntitlements {
do {
let transaction = try checkVerified(result)
// Si encontramos nuestro producto ID, activamos la versión Pro
if transaction.productID == "com.tuempresa.tuapp.premium_lifetime" {
hasUnlockedPro = true
}
} catch {
print("Fallo en verificación")
}
}
}
// MARK: - 4. Escucha Activa de Transacciones
/* Esto es vital. Si una compra se aprueba fuera de la app (ej. Control Parental,
o se renueva una suscripción), este listener lo capturará.
*/
func listenForTransactions() -> Task<Void, Error> {
return Task.detached {
for await result in Transaction.updates {
do {
let transaction = try self.checkVerified(result)
// Actualizamos la UI en el hilo principal
await self.updateCustomerProductStatus()
await transaction.finish()
} catch {
print("Error en transacción verificada")
}
}
}
}
// Función auxiliar para verificar la firma criptográfica de Apple
func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .unverified:
throw StoreError.failedVerification
case .verified(let safe):
return safe
}
}
}Análisis del Código
- @MainActor: Asegura que todas las actualizaciones de
@Publishedocurran en el hilo principal, evitando problemas de UI. - Transaction.currentEntitlements: Esta es la magia de StoreKit 2. Ya no necesitas un botón explícito de “Restaurar Compras” que llame a una función compleja. Simplemente iterando sobre esta secuencia asíncrona, sabes qué posee el usuario.
- Transaction.updates: Un loop infinito que escucha eventos del App Store. Es obligatorio implementarlo para manejar casos extremos como compras interrumpidas o aprobaciones parentales diferidas.
Parte 5: Creando la Interfaz de Usuario (Paywall)
Ahora que tenemos la lógica, necesitamos una vista atractiva para vender. SwiftUI hace que esto sea trivial.
Crea PaywallView.swift:
import SwiftUI
import StoreKit
struct PaywallView: View {
@EnvironmentObject var storeManager: StoreManager
@Environment(\.dismiss) var dismiss
var body: some View {
ScrollView {
VStack(spacing: 20) {
// Header
Image(systemName: "crown.fill")
.resizable()
.scaledToFit()
.frame(width: 80, height: 80)
.foregroundColor(.yellow)
.padding(.top, 40)
Text("Desbloquea Premium")
.font(.largeTitle.bold())
Text("Obtén acceso ilimitado a todas las funcionalidades, sincronización en la nube y soporte prioritario.")
.multilineTextAlignment(.center)
.padding(.horizontal)
.foregroundColor(.secondary)
// Lista de Beneficios
VStack(alignment: .leading, spacing: 15) {
FeatureRow(icon: "icloud", text: "Sincronización iCloud")
FeatureRow(icon: "chart.bar", text: "Estadísticas Avanzadas")
FeatureRow(icon: "lock.open", text: "Sin Anuncios")
}
.padding(.vertical)
// Tarjetas de Productos
if storeManager.products.isEmpty {
ProgressView("Cargando precios...")
} else {
ForEach(storeManager.products) { product in
ProductButton(product: product) {
Task {
try? await storeManager.purchase(product)
}
}
}
}
// Botón Restaurar (Aunque StoreKit 2 lo hace automático, Apple recomienda ponerlo)
Button("Restaurar Compras") {
Task {
await storeManager.updateCustomerProductStatus()
}
}
.font(.footnote)
.foregroundColor(.secondary)
.padding(.top)
}
.padding()
}
.onChange(of: storeManager.hasUnlockedPro) { newValue in
if newValue {
// Cerrar la paywall si la compra es exitosa
dismiss()
}
}
}
}
// Subvista para el botón de compra
struct ProductButton: View {
let product: Product
let action: () -> Void
var body: some View {
Button(action: action) {
HStack {
VStack(alignment: .leading) {
Text(product.displayName)
.font(.headline)
Text(product.description)
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
Text(product.displayPrice)
.padding(8)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
.padding()
.background(Color("CardBackground")) // Define este color en tus Assets
.cornerRadius(12)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.blue, lineWidth: 2)
)
}
.buttonStyle(.plain)
}
}
struct FeatureRow: View {
let icon: String
let text: String
var body: some View {
HStack {
Image(systemName: icon)
.foregroundColor(.blue)
.frame(width: 30)
Text(text)
}
}
}Parte 6: Integración en el Flujo de la App
Finalmente, debemos conectar todo en nuestro punto de entrada de la aplicación (App.swift).
import SwiftUI
@main
struct MiAppIncreible: App {
// Inicializamos el StoreManager como StateObject para que viva toda la sesión
@StateObject private var storeManager = StoreManager()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(storeManager) // Inyectamos al entorno
}
}
}Y en tu ContentView, puedes controlar el acceso a las funciones premium:
struct ContentView: View {
@EnvironmentObject var storeManager: StoreManager
@State private var showPaywall = false
var body: some View {
NavigationStack {
VStack {
if storeManager.hasUnlockedPro {
Text("¡Bienvenido Usuario Premium!")
.font(.title)
.foregroundColor(.green)
// Contenido exclusivo aquí
} else {
Text("Modo Gratuito")
Button("Hazte Premium") {
showPaywall = true
}
.buttonStyle(.borderedProminent)
}
}
.sheet(isPresented: $showPaywall) {
PaywallView()
}
}
}
}Parte 7: Pruebas y Depuración (Transaction Manager)
Una de las mejores características de desarrollar con Xcode y archivos .storekit es el Transaction Manager.
- Ejecuta la app en el simulador.
- Abre la
PaywallViewy compra el producto. - Verás que la UI se actualiza instantáneamente.
- Ahora, en Xcode, ve al menú Debug > StoreKit > Manage Transactions.
Aquí verás una ventana flotante con todas las compras realizadas en el simulador. Puedes:
- Borrar transacciones (para simular un usuario nuevo).
- Reembolsar transacciones (para probar cómo reacciona tu app si alguien devuelve el producto).
- Simular errores de facturación.
- Aprobar/Rechazar compras “Ask to Buy” (control parental).
Esta herramienta es invaluable para asegurarte de que tu listenForTransactions() maneja las revocaciones (refunds) correctamente, bloqueando el acceso al contenido premium si la compra desaparece.
Parte 8: Adaptación Multiplataforma (macOS y watchOS)
La belleza de SwiftUI y StoreKit 2 es que el código lógico (StoreManager.swift) es 100% reutilizable en macOS y watchOS. No necesitas cambiar ni una coma en la lógica.
Sin embargo, la UI (PaywallView) podría necesitar ajustes:
- macOS: En lugar de
.sheet, quizás prefieras una ventana modal o una vista lateral. Los botones y el espaciado deben adaptarse al puntero del ratón. - watchOS: La pantalla es pequeña. Simplifica la descripción. Muestra solo el botón grande de “Comprar” y quizás una sola frase de beneficio.
Ejemplo de adaptación condicional en PaywallView:
#if os(watchOS)
ScrollView {
VStack {
Text("Hazte Pro").font(.headline)
// Botón simplificado
}
}
#else
// Tu diseño complejo para iOS/macOS
#endifConsideraciones Finales y Mejores Prácticas
1. Manejo de Errores
En el código de ejemplo, usamos try? o bloques catch simples. En una app de producción, deberías mostrar alertas al usuario si la compra falla por problemas de red o si el ID de Apple no está configurado.
2. StoreKit Views (Novedad iOS 17+)
Apple introdujo StoreView y SubscriptionStoreView en iOS 17. Estas son vistas prefabricadas que renderizan la Paywall automáticamente basándose en tus metadatos de App Store Connect. Si buscas la integración más rápida posible y solo soportas iOS 17+, puedes reemplazar toda nuestra PaywallView con:
import StoreKit
struct SimplePaywall: View {
var body: some View {
SubscriptionStoreView(groupID: "tu_grupo_de_suscripcion")
}
}Nota: Esto funciona mejor para suscripciones, pero tiene opciones para no consumibles.
3. Validación del lado del servidor
Para aplicaciones de alta seguridad (ej. juegos con monedas o apps bancarias), la validación local (checkVerified en nuestro código) es muy segura, pero no invulnerable a dispositivos con Jailbreak avanzado. Si el riesgo de fraude es alto, deberías enviar el transaction.jwsRepresentation a tu propio servidor y validarlo con la API de Apple directamente.
Conclusión
Integrar compras en SwiftUI ha pasado de ser una pesadilla de código “spaghetti” a una arquitectura limpia y moderna gracias a StoreKit 2. Al seguir este tutorial, has creado una base sólida:
- Un StoreManager reactivo y seguro en hilos.
- Un entorno de pruebas local rápido con archivos .storekit.
- Una UI declarativa que responde al estado de la compra.
Ahora tienes las herramientas no solo para crear software, sino para construir un negocio sostenible en el ecosistema de Apple. El siguiente paso es experimentar con Suscripciones Renovables y ofrecer periodos de prueba para maximizar tu conversión.
Si tienes cualquier duda sobre este artículo, contacta conmigo y estaré encantado de ayudarte 🙂. Puedes contactar conmigo en mi perfil de X o en mi perfil de Instagram










