Saltar al contenido principal
Be Native Be Native

Artículo

Patrón de dobles en Swift

Aprende a implementar el patrón de dobles en Swift puro para testing y previews instantáneas en SwiftUI, sin librerías de terceros. Protocolos, inyección de dependencias y código testable usando solo las capacidades nativas del lenguaje.

Julio César Fernández Muñoz · · 5 min de lectura

Lee el artículo o escúchalo como podcast en la app. Descarga Be Native ahora.

Descarga Be Native en el App StoreDescarga Be Native en el Mac App Store

El Patrón de Dobles en Swift: Testing y Previews sin Librerías de Terceros

Hablemos de uno de esos patrones que separan al desarrollador que copia código del que entiende lo que hace: el patrón de dobles. Y no, no hablo de acrobacias de cine, hablo de cómo escribir código testable, tener previews en SwiftUI sin esperar 3 minutos a que muestre el resultado y hacerlo sin acudir a librerías de terceros ni frameworks de inyección de dependencias complejos. Todo con código Swift puro y duro.

🎯 El problema que nadie te cuenta

Imagina esto: estás desarrollando una app que carga películas desde una API. Cada vez que quieres ver la vista en el Canvas de Xcode tienes que esperar a que la preview compile, se conecte al servidor, descargue datos... y si no tienes internet o el servidor está caído, directamente la preview falla. Además, cuando quieres hacer tests unitarios de tu lógica de negocio, estás haciendo peticiones reales a un servidor, lo cual es lento, impredecible y totalmente ineficiente.

Lo típico es que mucha gente acuda a librerías de terceros para gestionar esto. Pero como siempre digo: si hay una solución nativa, úsala. Y en este caso la hay: protocolos + inyección de dependencias. Simple, elegante, Swift puro.

🧠 El concepto: acoplamiento leve mediante protocolos

Los protocolos en Swift son contratos. Definen qué debe hacer algo, pero no cómo lo hace. Esto es acoplamiento leve: tu lógica de negocio no depende de una implementación concreta, sino de un contrato. Y aquí es donde entra el patrón de dobles: crear implementaciones alternativas que cumplan el mismo contrato pero con comportamientos diferentes según el contexto.

Vamos con el ejemplo práctico. Primero definimos nuestro contrato:

swift
protocol DataRepository { 
   func load() async throws -> [Movie]
}

Este protocolo dice: "quien implemente esto debe tener un método load() que devuelve un array de películas". Punto. No dice nada de CÓMO las carga. Pueden venir de una API, de una base de datos local, de un archivo JSON o de datos mockeados. Eso es problema de quien implemente el protocolo, no del que lo usa.

👨🏻‍💻 La implementación real: la que usarás en producción

swift
struct Repository: DataRepository { 
   func load() async throws -> [Movie] { 
      // Aquí harías la llamada real a tu API 
      let url = URL(string: "https://api.tuservidor.com/movies")! 
      let data = try await URLSession.shared.data(from: url) 
      let movies = try JSONDecoder().decode([Movie].self, from: data.0) 
      return movies
    }
}

Y ahora el doble para testing: rápido, predecible, sin dependencias externas

swift
struct RepositoryTest: DataRepository { 
    func load() async throws -> [Movie] { 
        [Movie(id: 1, title: "Inception", year: 2010), 
         Movie(id: 2, title: "The Matrix", year: 1999), 
         Movie(id: 3, title: "Interstellar", year: 2014), 
         Movie(id: 4, title: "Blade Runner", year: 1982), 
         Movie(id: 5, title: "Arrival", year: 2016)] 
    } 
}

Aquí viene la magia. Fíjate en esta clase (que en SwiftUI sería @Observable u ObservableObject según uses el nuevo modelo de observabilidad o el antiguo)

swift
@Observable
final class Presentation { 
    var movies: [Movie] 
    let repository: DataRepository // Fíjate: usamos el PROTOCOLO
    init(repository: DataRepository = Repository()) {
       self.repository = repository
       self.movies = repository.load()
    }
}

🤯 ¿Ves la jugada? El constructor recibe un DataRepository (el protocolo), no un Repository concreto. Por defecto usa Repository() (la implementación real), pero podemos inyectarle cualquier cosa que cumpla el protocolo. Esto es inyección de dependencias: no creamos las dependencias dentro de la clase, las recibimos desde fuera.

🎪 Casos de uso prácticos

Caso 1: Producción

swift
let presentation = Presentation() 
// Usa Repository() por defecto, carga datos reales 
print(presentation.movies.count) // 3.000 películas desde tu API

Caso 2: Testing

swift
let presentationTest = Presentation(repository: RepositoryTest()) 
// Usa el doble, sin tocar red ni base de datos 
print(presentationTest.movies.count) // 5 películas de prueba

Caso 3: Preview en SwiftUI

swift
struct MovieListView: View { 
   let presentation: Presentation
   
   var body: some View {
      List(presentation.movies) { movie in
          Text(movie.title)
      }
   }
}
#Preview {
   MovieListView(presentation: Presentation(repository: RepositoryTest()))
   // Preview instantánea, sin esperar a ninguna API
}

🎭 ¿Por qué esto es mejor que las librerías de inyección de dependencias?

Porque no necesitas aprender otra herramienta. No necesitas configurar nada. No dependes de código de terceros que puede quedar obsoleto o tener bugs. Es Swift puro. Son protocolos, que ya conoces (o deberías). Es inyección de dependencias en su forma más simple y efectiva. Además, el compilador te protege: si tu doble no implementa correctamente el protocolo, no compila. Seguridad en tiempo de compilación, no en ejecución.

💡 El verdadero poder: flexibilidad sin complejidad

Con este patrón puedes:

  • Testear tu lógica sin tocar la red
  • Ver previews de SwiftUI al instante
  • Cambiar entre implementaciones según el entorno (debug/release)
  • Crear mocks para diferentes escenarios (datos vacíos, errores, datos masivos)
  • Mantener tu código desacoplado y fácil de mantener

Y todo sin añadir ni una sola dependencia externa. Solo aprovechando el poder de los protocolos y la inyección de dependencias, características nativas de Swift.

🔑 La clave: pensar en contratos, no en implementaciones

Cuando diseñes tu código, pregúntate: ¿qué DEBE hacer esto? No cómo lo hace, sino qué contrato debe cumplir. Define ese contrato en un protocolo. Luego crea las implementaciones que necesites. Tu código de negocio solo debe conocer el contrato. Esto es diseño orientado a interfaces y es la base de un código testable, mantenible y profesional.