Programación en Swift y SwiftUI para iOS Developers

Sendable Protocol en Swift

El ecosistema de desarrollo de Apple ha evolucionado drásticamente en los últimos años. Con la llegada de la concurrencia estructurada (async/await) y los Actors, cualquier iOS Developer moderno necesita dominar cómo los datos se mueven de forma segura entre diferentes hilos de ejecución. Aquí es donde entra en juego una de las piezas más críticas de la programación Swift moderna: el Sendable Protocol en Swift.

Ya sea que estés construyendo aplicaciones para iOS, macOS o watchOS utilizando Xcode y SwiftUI, comprender Sendable no es opcional si quieres escribir código libre de data races (condiciones de carrera) y prepararte para el modo de concurrencia estricta de Swift 6.

En este tutorial exhaustivo, desglosaremos qué es el protocolo Sendable, por qué existe y cómo puedes implementarlo en tus proyectos del día a día.


1. El Problema: Concurrencia y Data Races

Antes de hablar de la solución, debemos entender el problema. En la programación Swift, cuando múltiples hilos de ejecución intentan acceder y modificar el mismo fragmento de memoria simultáneamente, ocurre un data race.

Los data races son notorios por causar bloqueos impredecibles (crashes), corrupción de datos y comportamientos extraños que son increíblemente difíciles de depurar en Xcode.

Para resolver esto, Apple introdujo los Actors y el modelo de concurrencia estructurada. Los actores protegen su estado interno permitiendo que solo una tarea acceda a ellos a la vez. Sin embargo, ¿qué pasa con los datos que pasamos hacia y desde un Actor o un Task concurrente? Si pasamos una clase mutable (un tipo de referencia) de un hilo a otro, seguimos siendo vulnerables a los data races.

Aquí es donde entra la magia del Sendable Protocol en Swift.


2. ¿Qué es el Sendable Protocol en Swift?

En términos simples, Sendable es un protocolo “marcador”. Si observas su definición en la biblioteca estándar de Swift, verás que no tiene métodos ni propiedades requeridas.

Su único propósito es comunicarse con el compilador. Cuando un tipo (ya sea un struct, enum, class o actor) conforma el protocolo Sendable, le estás diciendo al compilador de Swift: “Es seguro compartir instancias de este tipo a través de diferentes dominios concurrentes (hilos o tareas)”.

Si intentas pasar un tipo que no es Sendable a un Task o a través del límite de un Actor, el compilador de Xcode emitirá una advertencia (o un error en Swift 6), protegiéndote antes de que el código llegue a producción.


3. ¿Por qué es crucial para un iOS Developer?

Como iOS Developer, tu objetivo principal es crear aplicaciones fluidas, rápidas y que no se cierren inesperadamente. Al utilizar SwiftUI, la interfaz de usuario se actualiza en el hilo principal (MainActor), pero el trabajo pesado (llamadas a red, procesamiento de imágenes, acceso a bases de datos) ocurre en hilos de fondo.

Cruzar esta frontera entre el fondo y la interfaz de usuario requiere mover datos. Si dominas el uso de Sendable, el compilador se convertirá en tu mejor amigo, garantizando matemáticamente que tus aplicaciones estén libres de vulnerabilidades de concurrencia a nivel de datos.


4. Tipos que son Implícitamente Sendable

Afortunadamente, no tienes que marcar cada tipo de tu aplicación como Sendable de forma manual. Swift es lo suficientemente inteligente como para inferir esta conformidad en muchos casos.

Tipos de Valor (Value Types)

Los structs y enums son el pan de cada día en la programación Swift. Dado que los tipos de valor se copian cuando se pasan de un lugar a otro, no hay memoria compartida que pueda sufrir un data race.

Por lo tanto, un struct es implícitamente Sendable si todas sus propiedades también lo son.

// Swift infiere automáticamente que este struct es Sendable
// porque String e Int (tipos primitivos) son Sendable.
struct UserProfile {
    let username: String
    let age: Int
}

Tipos Primitivos y Colecciones

Los tipos como Int, String, Bool, y las colecciones como Array o Dictionary (siempre que contengan elementos Sendable) son seguros por defecto.

Actores

Los actor son tipos de referencia, pero por diseño, sincronizan el acceso a su estado mutable. Por lo tanto, cualquier actor es inherentemente Sendable.


5. El Reto: Haciendo que las Clases sean Sendable

Las clases (class) son tipos de referencia. Si pasas una instancia de una clase a dos tareas diferentes, ambas tareas apuntan al mismo espacio en memoria. Esto es una receta para el desastre en concurrencia. Por ello, las clases no son implícitamente Sendable.

Sin embargo, hay situaciones donde necesitas que una clase conforme al protocolo. ¿Cómo lo logramos en Xcode?

Opción A: Clases Inmutables (El camino seguro)

Si una clase no puede cambiar su estado después de ser inicializada, es segura para compartir. Para que el compilador acepte una clase como Sendable, debe ser final y todas sus propiedades deben ser constantes (let) y de tipos que también sean Sendable.

final class AppConfiguration: Sendable {
    let apiEndpoint: String
    let maxRetryCount: Int
    
    init(apiEndpoint: String, maxRetryCount: Int) {
        self.apiEndpoint = apiEndpoint
        self.maxRetryCount = maxRetryCount
    }
}

Si quitas la palabra final o cambias un let por un var, Xcode te lanzará un error.

Opción B: @unchecked Sendable (Bajo tu propio riesgo)

A veces, estás trabajando con código heredado o clases que gestionan su propia sincronización interna (por ejemplo, usando NSLock o colas de despacho en C). Sabes que la clase es segura para hilos, pero el compilador de Swift no tiene forma de verificarlo.

En estos casos, puedes usar @unchecked Sendable. Esto desactiva las comprobaciones del compilador para ese tipo en particular. Como iOS Developer, debes usar esto con extrema precaución.

import Foundation

class ImageCache: @unchecked Sendable {
    private var cache: [String: Data] = [:]
    private let lock = NSLock()
    
    func save(data: Data, forKey key: String) {
        lock.lock()
        cache[key] = data
        lock.unlock()
    }
    
    func retrieve(forKey key: String) -> Data? {
        lock.lock()
        defer { lock.unlock() }
        return cache[key]
    }
}

Aquí asumimos la responsabilidad. Si olvidamos el lock, introduciremos un data race que el compilador ya no atrapará.


6. Funciones y Closures: El Atributo @Sendable

No solo los datos viajan entre hilos; el código también lo hace en forma de closures. Cuando creas un Task, le pasas un closure que se ejecutará de forma concurrente. Ese closure debe ser seguro de compartir.

Para esto existe el atributo @Sendable. Un closure @Sendable restringe lo que puedes capturar dentro de él:

  1. Solo puede capturar variables por valor o tipos que conformen a Sendable.
  2. No puede capturar variables locales mutables (var).

Veamos un ejemplo de cómo se aplica al pasar funciones como parámetros:

// Definimos una función que acepta un closure Sendable
func performConcurrentWork(action: @escaping @Sendable () -> Void) {
    Task {
        // La ejecución ocurre en un dominio concurrente
        action()
    }
}

class ViewController {
    var counter = 0
    
    func doWork() {
        performConcurrentWork {
            // ERROR: "Capture of 'self' with non-sendable type 'ViewController' in a `@Sendable` closure"
            // self.counter += 1 
        }
    }
}

Para solucionar el error de arriba, ViewController necesitaría ser un Actor o gestionar el estado de manera segura.


7. Tutorial Práctico: Sendable en SwiftUI y Xcode

Ahora que conocemos la teoría, veamos cómo todo este ecosistema de programación Swift se une en una aplicación real usando SwiftUI en Xcode. Crearemos una vista simple que descarga información del clima y veremos cómo fluyen los datos Sendable.

Paso 1: Definir nuestros modelos (Sendable implícito)

Creamos un modelo de datos. Al ser un struct con propiedades simples, es automáticamente Sendable.

struct WeatherData: Codable, Sendable {
    let temperature: Double
    let condition: String
    let cityName: String
}

Paso 2: Crear el servicio de red (Actor)

Para evitar condiciones de carrera si múltiples vistas intentan acceder al servicio a la vez, usamos un actor. Los actores, como vimos, son Sendable.

actor WeatherService {
    func fetchWeather(for city: String) async throws -> WeatherData {
        // Simulamos una llamada de red concurrente
        try await Task.sleep(nanoseconds: 1_000_000_000)
        
        // Retornamos nuestro struct Sendable
        return WeatherData(temperature: 24.5, condition: "Soleado", cityName: city)
    }
}

Paso 3: Integración en SwiftUI (El MainActor)

En SwiftUI, las vistas que actualizan la interfaz deben operar en el hilo principal. Usamos @MainActor en el ViewModel para garantizar esto. El cruce de datos entre WeatherService (un actor de fondo) y WeatherViewModel (MainActor) es seguro porque WeatherData es Sendable.

import SwiftUI

@MainActor
class WeatherViewModel: ObservableObject {
    @Published var weather: WeatherData?
    @Published var isLoading = false
    
    private let service = WeatherService()
    
    func loadData() {
        isLoading = true
        
        // Creamos un Task. El closure que pasamos es @Sendable por defecto en este contexto.
        Task {
            do {
                // Cruzamos el límite del actor. 
                // Esto es seguro porque WeatherData es Sendable.
                let fetchedData = try await service.fetchWeather(for: "Madrid")
                self.weather = fetchedData
                self.isLoading = false
            } catch {
                print("Error cargando datos")
                self.isLoading = false
            }
        }
    }
}

struct WeatherView: View {
    @StateObject private var viewModel = WeatherViewModel()
    
    var body: some View {
        VStack(spacing: 20) {
            if viewModel.isLoading {
                ProgressView("Cargando clima...")
            } else if let weather = viewModel.weather {
                Text(weather.cityName)
                    .font(.largeTitle)
                Text("\(weather.temperature, specifier: "%.1f")°C")
                    .font(.system(size: 60, weight: .bold))
                Text(weather.condition)
                    .font(.title2)
            } else {
                Text("No hay datos disponibles")
            }
            
            Button("Actualizar") {
                viewModel.loadData()
            }
            .buttonStyle(.borderedProminent)
        }
        .padding()
    }
}

Este código compilará sin una sola advertencia de concurrencia en Xcode, incluso con el modo estricto activado, gracias al uso correcto del Sendable Protocol en Swift.


8. Mejores prácticas para el iOS Developer moderno

Para dominar verdaderamente la concurrencia en la programación Swift, aquí tienes algunas reglas de oro:

  1. Prioriza los Tipos de Valor: Siempre que sea posible, usa structs y enums para tus modelos de datos. Al hacerlo, obtienes la conformidad con Sendable “gratis” y evitas dolores de cabeza.
  2. Activa la comprobación estricta de concurrencia en Xcode: Ve a los Build Settings de tu proyecto en Xcode, busca Strict Concurrency Checking y configúralo en Complete. Esto te obligará a adoptar Sendable correctamente y preparará tu código para Swift 6.
  3. Evita @unchecked Sendable a menos que sea el último recurso: Si te encuentras usándolo con frecuencia, es probable que haya un fallo arquitectónico en cómo estás gestionando el estado de tu aplicación.
  4. Usa Actores para el estado mutable compartido: Si realmente necesitas pasar un estado mutable entre diferentes partes de tu aplicación, encapsúlalo dentro de un actor en lugar de una clase tradicional.

9. Conclusión

El Sendable Protocol en Swift no es solo una característica más del lenguaje; es el pilar fundamental que hace que la concurrencia moderna en el ecosistema de Apple sea segura y robusta.

Como iOS Developer, aprender a estructurar tus datos para que sean transmitibles de manera segura te ahorrará innumerables horas de depuración de crashes misteriosos en producción. Ya sea que estés construyendo para iOS, macOS o watchOS utilizando SwiftUI, abrazar Sendable y dejar que el compilador de Xcode te guíe es la marca de un profesional de la programación Swift.

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

Transferable Protocol en Swift

Next Article

Equatable protocol en Swift

Related Posts