Programación en Swift y SwiftUI para iOS Developers

Cómo crear un clon de WhatsApp en SwiftUI

Si eres un iOS Developer buscando elevar tus habilidades al siguiente nivel, no hay mejor proyecto para tu portafolio que construir aplicaciones del mundo real. Hoy nos sumergiremos en uno de los retos más completos y gratificantes: crear un WhatsApp Clon en SwiftUI.

La programación Swift ha evolucionado drásticamente en los últimos años, y con la madurez de SwiftUI, ahora podemos construir interfaces de usuario fluidas, reactivas y multiplataforma con una fracción del código que requeríamos en UIKit. En este mega tutorial paso a paso, aprenderás a utilizar Swift y el entorno de desarrollo Xcode para construir una aplicación de mensajería funcional que no solo corra en el iPhone, sino que comparta código para funcionar de manera nativa en iPadOS, macOS y watchOS.


1. Requisitos Previos para el iOS Developer

Antes de escribir nuestra primera línea de código en Swift, asegúrate de tener:

  • Xcode 15 o superior (necesario para las últimas características de SwiftUI).
  • Conocimientos intermedios de programación Swift.
  • Familiaridad con los conceptos básicos de SwiftUI (VStack, HStack, List, NavigationStack).
  • Un espíritu de aprendizaje continuo, la cualidad más importante de cualquier iOS Developer.

2. Paso 1: Configuración del Proyecto Multiplataforma en Xcode

El verdadero poder de SwiftUI radica en su capacidad de “Aprender una vez, aplicar en cualquier lugar”. Vamos a configurar nuestro proyecto para soportar todo el ecosistema de Apple.

  1. Abre Xcode y selecciona Create a new Xcode project.
  2. En la pestaña de selección de plantillas, dirígete a la sección Multiplatform y selecciona App. Esto configurará nuestro proyecto de Swift para compartir el código base entre iOS y macOS.
  3. Nombra tu proyecto: WhatsAppClone.
  4. Asegúrate de que Interface esté configurado en SwiftUI y Language en Swift.
  5. Para watchOS, iremos a File > New > Target, seleccionaremos la pestaña watchOS y añadiremos un App Target a nuestro proyecto existente.

¡Felicidades! Ya tienes la base de tu WhatsApp Clon en SwiftUI.


3. Paso 2: Modelado de Datos y Lógica de Negocio (Programación Swift)

Un buen iOS Developer sabe que la interfaz es solo la punta del iceberg. Necesitamos una base de datos sólida. Crearemos modelos de datos utilizando las características modernas de la programación Swift.

Crea un nuevo archivo llamado Models.swift.

import Foundation

// Modelo para el Usuario
struct User: Identifiable, Hashable {
    let id: UUID
    let name: String
    let profileImageName: String
    let status: String
}

// Modelo para el Mensaje
struct Message: Identifiable, Hashable {
    let id = UUID()
    let text: String
    let timestamp: Date
    let isCurrentUser: Bool
}

// Modelo para el Chat
struct Chat: Identifiable, Hashable {
    let id = UUID()
    let participant: User
    var messages: [Message]
    var unreadCount: Int
    
    var lastMessage: Message? {
        messages.last
    }
}

Estos modelos son ligeros y conforman al protocolo Identifiable, lo que los hace perfectos para iterar sobre ellos en las listas de SwiftUI.

A continuación, crearemos nuestro ViewModel. En el paradigma MVVM, este archivo manejará el estado de nuestra app. Crea ChatViewModel.swift:

import Foundation
import Combine

class ChatViewModel: ObservableObject {
    @Published var chats: [Chat] = []
    
    init() {
        loadMockData()
    }
    
    private func loadMockData() {
        // Simulando datos para nuestro WhatsApp Clon en SwiftUI
        let user1 = User(id: UUID(), name: "Elena Gómez", profileImageName: "person.circle.fill", status: "Disponible")
        let user2 = User(id: UUID(), name: "Carlos Dev", profileImageName: "person.circle", status: "En el trabajo")
        
        let msg1 = Message(text: "¡Hola! ¿Viste la nueva actualización de Xcode?", timestamp: Date().addingTimeInterval(-3600), isCurrentUser: false)
        let msg2 = Message(text: "Sí, SwiftUI está increíble este año.", timestamp: Date().addingTimeInterval(-1800), isCurrentUser: true)
        
        let chat1 = Chat(participant: user1, messages: [msg1, msg2], unreadCount: 0)
        let chat2 = Chat(participant: user2, messages: [Message(text: "¿Subimos la app a TestFlight hoy?", timestamp: Date(), isCurrentUser: false)], unreadCount: 1)
        
        self.chats = [chat1, chat2]
    }
    
    func sendMessage(_ text: String, to chatID: UUID) {
        if let index = chats.firstIndex(where: { $0.id == chatID }) {
            let newMessage = Message(text: text, timestamp: Date(), isCurrentUser: true)
            chats[index].messages.append(newMessage)
        }
    }
}

4. Paso 3: Construyendo la Interfaz para iOS con SwiftUI

Ahora viene la parte visual. En nuestro WhatsApp Clon en SwiftUI, comenzaremos construyendo la vista principal de listas de chats, la pantalla que ves nada más abrir la app.

La Vista de Fila del Chat (ChatRowView)

Esta vista representará cada celda en nuestra lista de chats. Crea un archivo ChatRowView.swift:

import SwiftUI

struct ChatRowView: View {
    let chat: Chat
    
    var body: some View {
        HStack(spacing: 15) {
            Image(systemName: chat.participant.profileImageName)
                .resizable()
                .scaledToFill()
                .frame(width: 50, height: 50)
                .foregroundColor(.gray)
                .clipShape(Circle())
            
            VStack(alignment: .leading, spacing: 5) {
                HStack {
                    Text(chat.participant.name)
                        .font(.headline)
                        .fontWeight(.semibold)
                    
                    Spacer()
                    
                    if let lastMsg = chat.lastMessage {
                        Text(lastMsg.timestamp, style: .time)
                            .font(.caption)
                            .foregroundColor(.gray)
                    }
                }
                
                HStack {
                    if let lastMsg = chat.lastMessage {
                        Text(lastMsg.text)
                            .font(.subheadline)
                            .foregroundColor(.gray)
                            .lineLimit(1)
                    }
                    
                    Spacer()
                    
                    if chat.unreadCount > 0 {
                        Text("\(chat.unreadCount)")
                            .font(.caption2)
                            .padding(6)
                            .foregroundColor(.white)
                            .background(Color.blue)
                            .clipShape(Circle())
                    }
                }
            }
        }
        .padding(.vertical, 4)
    }
}

La Lista de Chats Principal (ChatListView)

Aprovechando NavigationStack de SwiftUI (o NavigationView en versiones antiguas, aunque todo buen iOS Developer debería actualizarse), crearemos la estructura principal de la app en iOS.

import SwiftUI

struct ChatListView: View {
    @StateObject private var viewModel = ChatViewModel()
    
    var body: some View {
        NavigationStack {
            List(viewModel.chats) { chat in
                NavigationLink(value: chat) {
                    ChatRowView(chat: chat)
                }
            }
            .listStyle(PlainListStyle())
            .navigationTitle("Chats")
            .navigationDestination(for: Chat.self) { chat in
                ChatDetailView(viewModel: viewModel, chat: chat)
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button(action: { /* Nueva conversación */ }) {
                        Image(systemName: "square.and.pencil")
                    }
                }
            }
        }
    }
}

La Vista de Conversación (ChatDetailView)

Para que nuestro WhatsApp Clon en SwiftUI se sienta real, necesitamos una pantalla de mensajería inmersiva. El uso de ScrollViewReader es crucial aquí para hacer scroll automáticamente hacia el último mensaje enviado, un truco esencial de la programación Swift aplicada a UIs.

import SwiftUI

struct ChatDetailView: View {
    @ObservedObject var viewModel: ChatViewModel
    var chat: Chat
    @State private var messageText = ""
    
    // Obtenemos el chat actualizado del ViewModel
    var currentChat: Chat? {
        viewModel.chats.first(where: { $0.id == chat.id })
    }
    
    var body: some View {
        VStack {
            ScrollView {
                ScrollViewReader { proxy in
                    VStack(spacing: 15) {
                        ForEach(currentChat?.messages ?? []) { message in
                            MessageBubbleView(message: message)
                                .id(message.id)
                        }
                    }
                    .padding()
                    .onChange(of: currentChat?.messages.count) { _ in
                        // Auto-scroll al último mensaje
                        if let lastID = currentChat?.messages.last?.id {
                            withAnimation {
                                proxy.scrollTo(lastID, anchor: .bottom)
                            }
                        }
                    }
                }
            }
            
            // Área de Input de Texto
            HStack {
                Button(action: {}) {
                    Image(systemName: "plus")
                        .font(.title2)
                        .foregroundColor(.blue)
                }
                
                TextField("Escribe un mensaje...", text: $messageText)
                    .padding(10)
                    .background(Color(.systemGray6))
                    .cornerRadius(20)
                
                Button(action: sendMessage) {
                    Image(systemName: messageText.isEmpty ? "mic" : "paperplane.fill")
                        .font(.title2)
                        .foregroundColor(messageText.isEmpty ? .gray : .blue)
                }
            }
            .padding()
            .background(Color(.systemBackground))
        }
        .navigationTitle(chat.participant.name)
        .navigationBarTitleDisplayMode(.inline)
    }
    
    private func sendMessage() {
        guard !messageText.trimmingCharacters(in: .whitespaces).isEmpty else { return }
        viewModel.sendMessage(messageText, to: chat.id)
        messageText = ""
    }
}

// Subvista para las burbujas de chat
struct MessageBubbleView: View {
    let message: Message
    
    var body: some View {
        HStack {
            if message.isCurrentUser { Spacer() }
            
            Text(message.text)
                .padding(12)
                .background(message.isCurrentUser ? Color.blue : Color(.systemGray5))
                .foregroundColor(message.isCurrentUser ? .white : .primary)
                .cornerRadius(16)
                .frame(maxWidth: 250, alignment: message.isCurrentUser ? .trailing : .leading)
            
            if !message.isCurrentUser { Spacer() }
        }
    }
}

5. Paso 4: Llevando nuestro Clon a macOS en Xcode

Como creaste un proyecto Multiplataforma en Xcode, el código de SwiftUI anterior ya se compilará en macOS. Sin embargo, un iOS Developer profesional sabe que la experiencia de usuario (UX) en el escritorio es distinta a la del móvil.

En iOS utilizamos NavigationStack porque la navegación es jerárquica (avanzar y retroceder). En macOS y iPadOS, lo ideal es usar NavigationSplitView para mostrar la lista de chats en una barra lateral y la conversación activa en el panel principal.

Modificaremos nuestro punto de entrada (App.swift o un ContentView principal) para usar compilación condicional o aprovechar la adaptabilidad de SwiftUI:

import SwiftUI

@main
struct WhatsAppCloneApp: App {
    @StateObject private var viewModel = ChatViewModel()
    
    var body: some Scene {
        WindowGroup {
            #if os(macOS) || targetEnvironment(macCatalyst)
            MacNavigationSplitView(viewModel: viewModel)
            #else
            ChatListView() // La vista de iOS que ya creamos
            #endif
        }
    }
}

// Vista específica para macOS/iPadOS
struct MacNavigationSplitView: View {
    @ObservedObject var viewModel: ChatViewModel
    @State private var selectedChat: Chat?
    
    var body: some View {
        NavigationSplitView {
            List(viewModel.chats, selection: $selectedChat) { chat in
                NavigationLink(value: chat) {
                    ChatRowView(chat: chat)
                }
            }
            .navigationTitle("Chats")
            .frame(minWidth: 250)
        } detail: {
            if let chat = selectedChat {
                ChatDetailView(viewModel: viewModel, chat: chat)
            } else {
                Text("Selecciona un chat para comenzar a mensajear")
                    .foregroundColor(.gray)
            }
        }
    }
}

Con solo unas cuantas líneas de programación Swift, hemos transformado nuestra interfaz móvil en una app de escritorio con paneles adaptativos. ¡Esa es la magia de Xcode y el ecosistema de Apple!


6. Paso 5: Expansión a watchOS

El Apple Watch requiere un diseño aún más simplificado. La pantalla es pequeña, por lo que elementos como la barra de búsqueda o inputs de texto complejos deben manejarse con cuidado. Como iOS Developer, debes diseñar para el contexto del usuario.

En tu Target de watchOS en Xcode, crearemos una interfaz especializada. Reutilizaremos los mismos archivos de modelo (Models.swift) y de lógica (ChatViewModel.swift), pero crearemos vistas separadas para el reloj.

import SwiftUI

// WatchContentView.swift (Target de watchOS)
struct WatchContentView: View {
    @StateObject private var viewModel = ChatViewModel()
    
    var body: some View {
        NavigationStack {
            List(viewModel.chats) { chat in
                NavigationLink(value: chat) {
                    VStack(alignment: .leading) {
                        Text(chat.participant.name)
                            .font(.headline)
                        Text(chat.lastMessage?.text ?? "")
                            .font(.caption)
                            .foregroundColor(.gray)
                            .lineLimit(1)
                    }
                }
            }
            .navigationTitle("Mensajes")
            .navigationDestination(for: Chat.self) { chat in
                WatchChatDetailView(viewModel: viewModel, chat: chat)
            }
        }
    }
}

struct WatchChatDetailView: View {
    @ObservedObject var viewModel: ChatViewModel
    var chat: Chat
    
    var currentChat: Chat? {
        viewModel.chats.first(where: { $0.id == chat.id })
    }
    
    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 8) {
                ForEach(currentChat?.messages ?? []) { message in
                    Text(message.text)
                        .padding(8)
                        .background(message.isCurrentUser ? Color.blue : Color.gray.opacity(0.3))
                        .cornerRadius(10)
                        .frame(maxWidth: .infinity, alignment: message.isCurrentUser ? .trailing : .leading)
                }
            }
            .padding(.horizontal)
        }
        .navigationTitle(chat.participant.name)
    }
}

Fíjate cómo eliminamos la imagen de perfil en la lista de chats para ahorrar espacio vertical, una decisión de diseño fundamental al programar para la muñeca.


7. Técnicas Avanzadas de Programación Swift para Optimizar tu Clon

Si quieres que este WhatsApp Clon en SwiftUI destaque en tu portafolio frente a otros desarrolladores, debes implementar buenas prácticas que los reclutadores buscan en un iOS Developer Senior:

1. Inyección de Dependencias

En lugar de inicializar ChatViewModel directamente dentro de las vistas, inyéctalo utilizando el entorno (@EnvironmentObject). Esto hace que tu código de Swift sea más modular y mucho más fácil de probar (Unit Testing).

2. Uso de Swift Concurrency (async/await)

Si conectaras esta aplicación a un backend real (por ejemplo, Firebase o Supabase), deberías sustituir la carga de datos de prueba (loadMockData()) por llamadas de red asíncronas utilizando async/await. La programación Swift moderna abandona los closures tradicionales en favor de un código concurrente más limpio y seguro contra condiciones de carrera.

3. Accesibilidad

Una aplicación creada con SwiftUI es tan buena como su accesibilidad. Añade modificadores como .accessibilityLabel("Mensaje de \(chat.participant.name)") en tus celdas. En Xcode, puedes usar el Accessibility Inspector para verificar que VoiceOver lea correctamente la interfaz.

4. Persistencia Local con SwiftData o CoreData

Un clon de WhatsApp real guarda los mensajes localmente para que puedas leerlos sin conexión. Implementar SwiftData (el framework de persistencia nativo introducido recientemente por Apple) en tu modelo convertirá tu app de un simple prototipo a una aplicación de grado de producción.


Conclusión

Crear un WhatsApp Clon en SwiftUI es un ejercicio magistral de arquitectura, diseño de UI y programación Swift. A lo largo de este tutorial, hemos visto cómo configurar Xcode para el desarrollo multiplataforma, cómo estructurar datos usando MVVM, y cómo adaptar una sola lógica de negocio a tres plataformas completamente distintas: iPhone (iOS), Mac (macOS) y Apple Watch (watchOS).

Ser un gran iOS Developer no se trata solo de conocer la sintaxis de Swift, sino de entender cómo resolver problemas complejos utilizando las herramientas que Apple nos provee. SwiftUI nos ha demostrado que escribir interfaces nativas fluidas es hoy más intuitivo y rápido que nunca.

Leave a Reply

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

Previous Article

Cómo obtener el tamaño de pantalla en SwiftUI

Related Posts