Tutorial: WebSockets en Swift para apps de chat en tiempo real

En la era de la inmediatez, los usuarios esperan que sus aplicaciones respondan al instante. Ya sea una aplicación de trading financiero, un marcador deportivo en vivo o, el caso más común, una aplicación de chat, la latencia es el enemigo número uno. Aquí es donde los WebSockets entran en juego.

En este artículo, exploraremos a fondo cómo implementar WebSockets en Swift utilizando las herramientas nativas que Apple nos proporciona. Olvida las peticiones HTTP constantes (polling); es hora de construir una comunicación bidireccional y eficiente.


¿Qué son los WebSockets y por qué usarlos en Swift?

Tradicionalmente, las aplicaciones móviles se comunican con los servidores mediante el protocolo HTTP. En este modelo, el cliente (tu app) solicita información y el servidor responde. Sin embargo, para un chat, este modelo es ineficiente: la app tendría que preguntar cada segundo «¿hay mensajes nuevos?», consumiendo batería y datos innecesariamente.

Los WebSockets proporcionan un canal de comunicación full-duplex a través de una única conexión TCP de larga duración. Esto significa que:

  1. Comunicación Bidireccional: Tanto el cliente como el servidor pueden enviar mensajes en cualquier momento.
  2. Baja Latencia: Una vez establecida la conexión, no hay necesidad de repetir el apretón de manos (handshake) de HTTP.
  3. Eficiencia: Menos encabezados y menos sobrecarga de red en comparación con las peticiones REST.

Desde iOS 13, Apple introdujo URLSessionWebSocketTask, eliminando la dependencia casi obligatoria de librerías de terceros como Starscream o Socket.io en proyectos sencillos.


Configuración inicial de la conexión

Para empezar a trabajar con WebSockets en Swift, utilizaremos la API nativa de URLSession. El primer paso es definir la URL del servidor (que debe empezar por ws:// o wss:// para conexiones seguras) y crear la tarea.

import Foundation

class ChatManager: NSObject {
    private var webSocketTask: URLSessionWebSocketTask?
    private let urlSession = URLSession(configuration: .default)

    func connect() {
        let url = URL(string: "wss://tu-servidor-de-chat.com/socket")!
        webSocketTask = urlSession.webSocketTask(with: url)
        webSocketTask?.resume()

        print("Conexión iniciada...")
        receiveMessage() // Empezamos a escuchar inmediatamente
    }
}

Es importante llamar a .resume() para que la conexión se abra realmente. Además, como los WebSockets son asíncronos por naturaleza, debemos preparar nuestra lógica para recibir datos de forma continua.


Cómo recibir mensajes: El reto de la recursividad

A diferencia de otras APIs, URLSessionWebSocketTask.receive solo escucha el próximo mensaje. Una vez que recibe uno, deja de escuchar. Para solucionar esto y crear un flujo constante de chat, debemos implementar una función recursiva.

Implementación del receptor

func receiveMessage() {
    webSocketTask?.receive { [weak self] result in
        switch result {
        case .failure(let error):
            print("Error al recibir mensaje: \(error)")
            // Aquí podrías implementar una lógica de reconexión
        case .success(let message):
            switch message {
            case .string(let text):
                print("Mensaje de texto recibido: \(text)")
                // Aquí actualizarías tu UI de chat
            case .data(let data):
                print("Datos binarios recibidos: \(data.count) bytes")
            @unknown default:
                break
            }

            // Volvemos a llamar a la función para seguir escuchando
            self?.receiveMessage()
        }
    }
}

Puntos clave:

  • Usamos [weak self] para evitar ciclos de retención de memoria.
  • Manejamos tanto string (texto plano o JSON) como data (archivos o imágenes).
  • La llamada recursiva al final asegura que la app esté siempre lista para el siguiente mensaje.

Enviando mensajes al servidor

Enviar un mensaje es mucho más directo. El servidor de chat esperará normalmente un string en formato JSON que contenga el ID del remitente, el cuerpo del mensaje y el ID de la sala.

func sendMessage(_ text: String) {
    let message = URLSessionWebSocketMessage.string(text)

    webSocketTask?.send(message) { error in
        if let error = error {
            print("Error al enviar mensaje: \(error.localizedDescription)")
        } else {
            print("Mensaje enviado con éxito")
        }
    }
}

Si estás construyendo una app profesional, lo ideal es codificar un objeto Codable a una cadena JSON antes de enviarlo:

struct ChatMessage: Codable {
    let senderId: String
    let content: String
    let timestamp: Date
}

// Uso:
let chatMsg = ChatMessage(senderId: "user123", content: "Hola a todos!", timestamp: Date())
if let data = try? JSONEncoder().encode(chatMsg), let jsonString = String(data: data, encoding: .utf8) {
    sendMessage(jsonString)
}

Gestión del ciclo de vida y mantenimiento de la conexión

Las conexiones móviles son inestables por naturaleza. Tu app de chat debe ser capaz de manejar desconexiones, cambios de Wi-Fi a 5G y estados de suspensión.

1. El Ping/Pong (Heartbeat)

Para evitar que el servidor cierre la conexión por inactividad, es buena práctica enviar un «ping».

func sendPing() {
    webSocketTask?.sendPing { error in
        if let error = error {
            print("El servidor no respondió al ping: \(error)")
        } else {
            // El servidor respondió, la conexión sigue viva
            DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
                self.sendPing()
            }
        }
    }
}

2. Desconexión limpia

Cuando el usuario sale del chat o cierra la app, debemos cerrar la conexión indicando un motivo.

func disconnect() {
    webSocketTask?.cancel(with: .goingAway, reason: "Usuario salió del chat".data(using: .utf8))
}

Integración con la interfaz de usuario (SwiftUI)

Para una app de chat real, necesitaremos que nuestra lógica de WebSocket se comunique con la vista. El uso de ObservableObject en SwiftUI es la mejor opción.

class ChatViewModel: ObservableObject {
    @Published var messages: [String] = []
    private var service = ChatManager() // Nuestra clase anterior

    init() {
        // En una implementación real, usarías delegados o cierres
        // para pasar los mensajes recibidos a este array
    }
}

struct ChatView: View {
    @StateObject var viewModel = ChatViewModel()
    @State private var newMessage = ""

    var body: some View {
        VStack {
            List(viewModel.messages, id: \.self) { msg in
                Text(msg)
            }
            HStack {
                TextField("Escribe algo...", text: $newMessage)
                Button("Enviar") {
                    // Acción de enviar
                }
            }
        }
    }
}

Buenas prácticas para apps de chat en producción

Si bien URLSessionWebSocketTask es potente, escalar una app de chat requiere considerar aspectos avanzados:

  • Reconexión Exponencial: Si la conexión falla, no intentes reconectar cada segundo. Usa un «backoff exponencial» (esperar 1s, luego 2s, luego 4s…) para no saturar el servidor ni agotar la batería.
  • Seguridad (TLS): Utiliza siempre wss:// en lugar de ws://. Apple exige conexiones seguras a través de App Transport Security (ATS).
  • Manejo de Background: iOS suspende las conexiones de red poco después de que la app entra en segundo plano. Considera usar Notificaciones Push (APNs) para avisar al usuario de nuevos mensajes cuando la app no esté activa.
  • Librerías externas: Si tu backend usa Socket.io (que es un protocolo sobre WebSockets, no WebSocket puro), la API nativa de Swift no funcionará directamente. En ese caso, deberás usar la librería socket.io-client-swift.

Conclusión

Implementar WebSockets en Swift ha pasado de ser una tarea compleja que requería frameworks externos a ser una funcionalidad nativa elegante y robusta gracias a URLSessionWebSocketTask.

Resumen de pasos clave:

  1. Crear la tarea con una URL wss://.
  2. Usar recursividad en la función receive para mantener la escucha activa.
  3. Implementar un sistema de Ping para mantener viva la conexión.
  4. Codificar y decodificar mensajes usando el protocolo Codable.

Al dominar esta tecnología, no solo mejoras la experiencia de usuario de tu app de chat, sino que abres la puerta a un mundo de aplicaciones en tiempo real, desde herramientas colaborativas hasta juegos multijugador. ¡Es hora de empezar a programar!

Deja una respuesta