CULTIVA IA · IA Ingeniería & MLOps

Swift 6.2 Approachable Concurrency

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.

Swift 6.2 Xcode 26 Avanzado iOS / macOS
4 errores data-race
resueltos
3 patrones Swift 6.2
aplicados
1 función @concurrent
para Vision
0 anotaciones @MainActor
manuales necesarias
🧵
Patrón 1 — Async permanece en el actor llamante

En Swift 6.1, generateContent() era enviado implícitamente a un background thread, causando data-race al acceder a self.imageProcessor.

ANTES · Swift 6.1 — ERROR
DESPUÉS · Swift 6.2 — OK
// 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)
        )
    }
}
⚠️
error: Sending 'self.imageProcessor' risks causing data races — concurrencia implícita en Swift 6.1
// 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)
        )
    }
}
Async functions permanecen en el MainActor. Zero data races. Zero annotations adicionales con SE-0466 habilitado.
🏃
Patrón 2 — @concurrent para trabajo CPU-intensivo

El análisis de imágenes con Vision framework sí necesita background. Usamos @concurrent de forma explícita solo donde importa el rendimiento.

ANTES · Dispatch manual — problemático
DESPUÉS · @concurrent — explícito y seguro
// 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 */ }
}
⚠️ Cache mutation races con DispatchQueue. Difícil de auditar para el compilador.
// 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 */ }
}
✓ Solo runVisionAnalysis usa threads paralelos. La cache es gestionada por MainActor. Explícito e intencional.
🔒
Patrón 3 — Isolated Conformances

ContentGeneratorModel necesita conformar el protocolo Exportable sin perder aislamiento de MainActor.

ANTES — workaround frágil
DESPUÉS — isolated conformance
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
    }
}
⚠️ La exportación es nonisolated y vacía. No puede acceder a datos del modelo.
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
    }
}
✓ La conformance está aislada a MainActor. Acceso completo al estado. El compilador verifica que sólo se use desde el mismo actor.
🗂️
Patrón 4 — Global/Static State con MainActor

ContentLibrary.shared era un singleton sin aislamiento. Swift 6.2 lo resuelve con inferencia automática de MainActor.

ANTES — singleton sin aislamiento
DESPUÉS — inferencia automática
// 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)
        }
    }
}
⚠️ error: static stored property 'shared' of non-Sendable type 'ContentLibrary' can cause data races
// 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)
✓ El compilador infiere @MainActor en toda la clase. El singleton es data-race free sin anotaciones manuales.
⚙️
Build Settings — Xcode 26

Habilitar Approachable Concurrency en el target app de CultivaGen:

📁 CultivaGen.xcodeproj → Target CultivaGen → Build Settings → Swift Compiler - Concurrency
SWIFT_CONCURRENCY_NONISOLATED_NONSENDING_BY_DEFAULT YES
SWIFT_DEFAULT_ACTOR_ISOLATION MainActor
SWIFT_STRICT_CONCURRENCY complete
SWIFT_VERSION 6.2
Package.swift
// Para Swift Package Manager:
.target(
    name: "CultivaGen",
    swiftSettings: [
        // SE-0461: nonisolated non-sending by default
        .enableExperimentalFeature("NonisolatedNonsendingByDefault"),
        // SE-0466: MainActor default isolation
        .enableExperimentalFeature("GlobalActorIsolatedTypesUsability"),
    ]
)
📊
Tabla de Decisión — ¿Cuándo usar qué?

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
Checklist de Migración — CultivaGen a Xcode 26

Pasos verificados para completar la migración:

Actualizar Xcode a versión 26 Requerido para Swift 6.2 y Approachable Concurrency
Habilitar NonisolatedNonsendingByDefault SE-0461 — async no se va a background implícitamente
Habilitar MainActor default isolation SE-0466 — inferencia automática en app target
Migrar ContentGeneratorModel Eliminar @MainActor explícito, funciona por inferencia
Migrar ImageProcessor con @concurrent Solo runVisionAnalysis() en thread pool
Isolated conformance en Exportable extension ContentGeneratorModel: @MainActor Exportable
ContentLibrary.shared sin @MainActor Inferencia automática elimina el error de static
Perfilar con Instruments Verificar que @concurrent mejora latencia de Vision
Eliminar DispatchQueue.global() legacy Reemplazar por @concurrent en funciones apropiadas
Revisar dependencias de terceros SPM packages pueden necesitar actualización para SE-0461
⚠️
Anti-patrones a Evitar

Errores comunes durante la migración:

🚫 @concurrent en todo
Aplicar @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 como silenciador
Usar nonisolated para quitar errores del compilador sin entender el aislamiento. Enmascara data races reales que aparecerán en producción.
🚫 DispatchQueue legacy
Mantener DispatchQueue.global() junto a @concurrent. Son equivalentes; mezclarlos crea confusión y dificulta auditorías de concurrencia.
🚫 Ignorar warnings del compilador
Asumir que un warning de data race es falso positivo. En Swift 6.2, si el compilador reporta una carrera de datos, existe un problema real — investigar siempre.
CULTIVA IA · Skill: concurrencia-asequible-swift-6-2 · Swift 6.2 / Xcode 26 · SE-0461 + SE-0466 · MIT License