Compilar el XCFramework

Para que iOS pueda usar el shared module, hay que compilarlo como un XCFramework — un paquete que contiene los binarios para todas las arquitecturas iOS (dispositivo físico + simulador Intel + simulador Apple Silicon).

# Buildear el XCFramework (desde el directorio raíz del proyecto):
./gradlew :shared:assembleXCFramework

# El framework generado aparece en:
# shared/build/XCFrameworks/debug/Shared.xcframework  (debug)
# shared/build/XCFrameworks/release/Shared.xcframework (release)

# El XCFramework contiene:
# Shared.xcframework/
# ├── ios-arm64/          # dispositivo físico
# │   └── Shared.framework
# ├── ios-arm64-simulator/ # simulador Apple Silicon
# │   └── Shared.framework
# └── ios-x86_64-simulator/ # simulador Intel
#     └── Shared.framework

# Para release (Play Store / App Store):
./gradlew :shared:assembleXCFramework -Pkmp.targets=release

Automatizar con un script de buildEn un proyecto real, el proceso de compilar el XCFramework y copiarlo a la carpeta del proyecto Xcode se automatiza en un script de pre-build o en el CI/CD. No es práctico hacerlo manualmente en cada cambio.

Importar el XCFramework en Xcode

# Opción 1: Direct integration (la más simple para empezar)
# El proyecto Xcode creado por el wizard de KMP ya viene configurado con
# un script de build que genera el XCFramework automáticamente

# El script en Xcode (Build Phases → Run Script):
# cd "$SRCROOT/.."
# ./gradlew :shared:embedAndSignAppleFrameworkForXcode

# Esta tarea de Gradle genera y firma el framework para el target actual
# (simulador o dispositivo) automáticamente

# Opción 2: Agregar manualmente si no usaste el wizard
# Xcode → Project → General → Frameworks, Libraries, Embedded Content
# → Add Files → seleccionar Shared.xcframework
# → Embed & Sign

# Verificar que el import funciona en Swift:
# import Shared  ← si este import no falla, la integración es correcta

Llamar código Kotlin desde Swift

Kotlin compila a Objective-C/Swift cuando el target es iOS. La API está disponible en Swift pero con algunas diferencias de nomenclatura:

// Swift — importar y usar el shared module
import Shared

// Data classes de Kotlin → structs accesibles en Swift
let producto = Producto(
    id: 1,
    nombre: "Mouse",
    precio: 2000.0,
    descripcion: nil,
    imageUrl: "https://..."
)

// Kotlin sealed classes → aparecen como clases con subclases en Swift
// Resultado.Exito, Resultado.Error, Resultado.Cargando
let resultado: ResultadoExito<NSArray> = ...

// Kotlin object (singleton) — acceso via .shared en Swift
// Si tenés: object Logger { fun log(...) }
// En Swift: Logger.shared.log(tag: "iOS", mensaje: "hola")

// Kotlin enum class → enum accesible en Swift
// Kotlin: enum class Estado { CARGANDO, EXITO, ERROR }
// Swift: SharedEstado.cargando, .exito, .error

// Extensiones de String en Kotlin → disponibles en Swift como métodos
// Kotlin: fun String.esEmailValido(): Boolean = ...
// Swift: "[email protected]".esEmailValido()

SKIE — coroutines y Flow nativos en Swift

SKIE (de Touchlab) es la solución más limpia al problema de coroutines en iOS. Con un plugin de Gradle, genera extensiones Swift que hacen que suspend functions y Flow funcionen con la sintaxis nativa de Swift (async/await y AsyncSequence).

// shared/build.gradle.kts — agregar SKIE
plugins {
    id("co.touchlab.skie") version "0.8.0"
}
// No hay más configuración necesaria — SKIE inspecciona el código Kotlin
// y genera las extensiones Swift automáticamente
// Swift — con SKIE, el código Kotlin se siente nativo

// suspend function → async function en Swift
// Kotlin: suspend fun getProducto(id: Int): Producto
// Swift:
let producto = try await repository.getProducto(id: 42)

// Flow → AsyncSequence en Swift
// Kotlin: fun getProductos(): Flow<List<Producto>>
// Swift:
for await productos in repository.getProductos() {
    self.productos = productos
}

// StateFlow → AsyncSequence observable
// Kotlin: val uiState: StateFlow<ProductosUiState>
// Swift:
for await state in viewModel.uiState {
    self.productos = state.productos
    self.isLoading = state.isLoading
}

ViewModel en SwiftUI usando el shared module

// Swift — ViewModel de SwiftUI que consume el shared module
// (requiere SKIE para el soporte de Flow)

import Shared
import SwiftUI

@MainActor
class ProductosViewModelIOS: ObservableObject {
    @Published var productos: [Producto] = []
    @Published var isLoading = true
    @Published var error: String? = nil

    private let getProductos: GetProductosUseCase
    private let sincronizar: SincronizarUseCase

    init() {
        // Obtener los use cases de Koin (configurado en iosMain)
        getProductos = KoinHelper.shared.getProductosUseCase()
        sincronizar = KoinHelper.shared.sincronizarUseCase()
    }

    func cargar() {
        Task {
            // Flow del shared module consumido con async/await (via SKIE)
            for await resultado in getProductos.invoke() {
                self.productos = resultado
                self.isLoading = false
            }
        }
    }

    func pullRefresh() async {
        _ = try? await sincronizar.invoke()  // suspend function via SKIE
    }
}

// Vista SwiftUI
struct ProductosPantalla: View {
    @StateObject private var viewModel = ProductosViewModelIOS()

    var body: some View {
        List(viewModel.productos, id: \.id) { producto in
            VStack(alignment: .leading) {
                Text(producto.nombre).font(.headline)
                Text("$\(producto.precio, specifier: "%.2f")").font(.subheadline)
            }
        }
        .onAppear { viewModel.cargar() }
        .refreshable { await viewModel.pullRefresh() }
    }
}

Limitaciones reales — lo que hay que conocer

# Limitación 1: Generics en Swift
# Los generics de Kotlin con múltiples type parameters pierden información en Swift
# Kotlin: fun <T, E> Result<T, E>.map(...)
# Swift: solo ve el tipo base sin los type parameters
# Solución: usar tipos concretos en lugar de generics en la API pública del shared

# Limitación 2: Coroutines sin SKIE
# Sin SKIE, las suspend functions aparecen en Swift con un completionHandler callback
# Kotlin: suspend fun getProducto(): Producto
# Swift sin SKIE: getProducto(completionHandler: @escaping (Producto?, Error?) -> Void)
# Solución: usar SKIE — es mantenida por Touchlab y es la solución oficial de facto

# Limitación 3: iOS solo puede compilar en Mac
# El XCFramework requiere Xcode y macOS
# Los devs Android sin Mac no pueden compilar para iOS
# En CI/CD se necesita un runner macOS (más caro en GitHub Actions)

# Limitación 4: Kotlin Native es más lento que la JVM
# La primera ejecución en iOS puede ser más lenta que en Android
# Baseline Profiles no existe para iOS — la optimización es diferente
# En la práctica, para código de negocio (no UI) la diferencia no es perceptible

# Lo que funciona muy bien hoy:
# ✓ Modelos de dominio simples
# ✓ Networking con Ktor
# ✓ Lógica de negocio y validaciones
# ✓ Serialización JSON
# ✓ Tests del shared module en ambas plataformas