Guía de migración para CultivaGen — app iOS de generación de contenido con IA. De Swift 6.1 con data-race errors a Swift 6.2 limpio en 4 patrones.
En Swift 6.1, generateContent() era enviado implícitamente a un background thread, causando data-race al acceder a self.imageProcessor.
// ContentGeneratorModel.swift // Swift 6.1 — data-race error @MainActor final class ContentGeneratorModel { let imageProcessor = ImageProcessor() var generatedContent: [Content] = [] func generateContent( prompt: String, style: ContentStyle ) async throws { let text = try await callLLM(prompt) // ❌ ERROR: Sending 'self.imageProcessor' // risks causing data races let image = await imageProcessor .generateThumbnail( text: text, style: style ) generatedContent.append( Content(text: text, image: image) ) } }
// ContentGeneratorModel.swift // Swift 6.2 — sin cambios, funciona // Con MainActor default inference // la clase es @MainActor implícitamente final class ContentGeneratorModel { let imageProcessor = ImageProcessor() var generatedContent: [Content] = [] func generateContent( prompt: String, style: ContentStyle ) async throws { let text = try await callLLM(prompt) // ✅ OK: async stays on MainActor // No implicit background offloading let image = await imageProcessor .generateThumbnail( text: text, style: style ) generatedContent.append( Content(text: text, image: image) ) } }
El análisis de imágenes con Vision framework sí necesita background. Usamos @concurrent de forma explícita solo donde importa el rendimiento.
// ImageProcessor.swift — Swift 6.1 nonisolated final class ImageProcessor { private var cache: [String: UIImage] = [:] func generateThumbnail( text: String, style: ContentStyle ) async -> UIImage { if let cached = cache[text] { return cached } // ❌ Legacy: DispatchQueue manual return await withCheckedContinuation { cont in DispatchQueue.global(qos: .userInitiated) .async { let img = Self.runVisionAnalysis(text) // ❌ data race: cache write from // background thread cont.resume(returning: img) } } } static func runVisionAnalysis( _ text: String ) -> UIImage { /* Vision + Core ML */ } }
// ImageProcessor.swift — Swift 6.2 nonisolated final class ImageProcessor { // Cache es safe: sólo se escribe // desde el actor llamante (MainActor) private var cache: [String: UIImage] = [:] func generateThumbnail( text: String, style: ContentStyle ) async -> UIImage { if let cached = cache[text] { return cached // ✅ MainActor read } // ✅ @concurrent: offload explícito // a thread pool para Vision/CoreML let img = await Self .runVisionAnalysis(text) cache[text] = img // ✅ MainActor write return img } // Solo ésta corre en background @concurrent static func runVisionAnalysis( _ text: String ) async -> UIImage { /* Vision + Core ML */ } }
runVisionAnalysis usa threads paralelos. La cache es gestionada por MainActor. Explícito e intencional.ContentGeneratorModel necesita conformar el protocolo Exportable sin perder aislamiento de MainActor.
protocol Exportable { func exportToCSV() -> String func exportToPDF() -> Data } // ❌ Swift 6.1: ERROR // extension ContentGeneratorModel: Exportable { // func exportToCSV() -> String { ... } // } // Workaround: nonisolated (PELIGROSO) extension ContentGeneratorModel: Exportable { nonisolated func exportToCSV() -> String { // ❌ No puede acceder a generatedContent // desde contexto nonisolated return "" // placeholder vacío } nonisolated func exportToPDF() -> Data { Data() // placeholder vacío } }
protocol Exportable { func exportToCSV() -> String func exportToPDF() -> Data } // ✅ Swift 6.2: isolated conformance extension ContentGeneratorModel: @MainActor Exportable { func exportToCSV() -> String { // ✅ Acceso completo a @MainActor state generatedContent .map { "\($0.id),\($0.text)" } .joined(separator: "\n") } func exportToPDF() -> Data { // ✅ Acceso a generatedContent completo PDFBuilder.build(generatedContent) } } // ExportManager también @MainActor: @MainActor struct ExportManager { var exportables: [any Exportable] = [] func register(_ m: ContentGeneratorModel) { exportables.append(m) // ✅ mismo actor } }
ContentLibrary.shared era un singleton sin aislamiento. Swift 6.2 lo resuelve con inferencia automática de MainActor.
// ContentLibrary.swift — Swift 6.1 final class ContentLibrary { // ❌ ERROR: static stored property // of non-Sendable type static let shared = ContentLibrary() var items: [Content] = [] var tags: [String: [Content]] = [:] func save(_ content: Content) { items.append(content) for tag in content.tags { tags[tag, default: []].append(content) } } }
// ContentLibrary.swift — Swift 6.2 // Con MainActor default inference ON // ✅ Sin @MainActor explícito — // la inferencia lo aplica automáticamente final class ContentLibrary { // ✅ OK: implicitly @MainActor static let shared = ContentLibrary() var items: [Content] = [] var tags: [String: [Content]] = [:] func save(_ content: Content) { items.append(content) for tag in content.tags { tags[tag, default: []].append(content) } } } // Uso: await ContentLibrary.shared.save(newContent)
Habilitar Approachable Concurrency en el target app de CultivaGen:
// Para Swift Package Manager: .target( name: "CultivaGen", swiftSettings: [ // SE-0461: nonisolated non-sending by default .enableExperimentalFeature("NonisolatedNonsendingByDefault"), // SE-0466: MainActor default isolation .enableExperimentalFeature("GlobalActorIsolatedTypesUsability"), ] )
Guía rápida para el equipo de CultivaGen al diseñar nuevas funciones:
| Patrón | Cuándo usarlo | En CultivaGen |
|---|---|---|
| async en MainActor | La mayoría de funciones async — llamadas a API, UI updates, state management | callLLM(), saveContent(), fetchHistory() |
| @concurrent | CPU-bound: procesamiento imagen, compresión video, ML inference pesada (>50ms) | runVisionAnalysis(), encodeVideo(), runCoreMLBatch() |
| @MainActor conformance | Tipos @MainActor que implementan protocolos de framework (no aislados) | ContentModel: Exportable, View: Identifiable |
| MainActor inference | App targets: view models, controllers, coordinators, singletons | ContentLibrary, NavigationCoordinator, SettingsStore |
| Actor explícito | Estado compartido entre múltiples actores (no solo MainActor) | NetworkSession, DatabaseActor, CacheActor |
Pasos verificados para completar la migración:
Errores comunes durante la migración:
@concurrent a cada función async porque "es más rápido". La mayoría de código no necesita parallelism — añade overhead y complejidad sin beneficio.nonisolated para quitar errores del compilador sin entender el aislamiento. Enmascara data races reales que aparecerán en producción.DispatchQueue.global() junto a @concurrent. Son equivalentes; mezclarlos crea confusión y dificulta auditorías de concurrencia.