Programación en Swift y SwiftUI para iOS Developers

Gemini CLI en Xcode

El ecosistema de Apple está en plena transformación. Para un iOS Developer en la actualidad, el dominio de la sintaxis de Swift ya no es suficiente; la integración de la Inteligencia Artificial Generativa se ha convertido en el nuevo estándar de oro.

Google ha lanzado Gemini, su modelo más capaz y flexible hasta la fecha. Aunque muchos interactúan con él a través de la web, la verdadera potencia para un desarrollador reside en su API y en la capacidad de automatizar tareas.

En este tutorial, no solo aprenderemos a integrar Gemini en una app; vamos a ir un paso más allá. Aprenderemos a crear nuestra propia Gemini CLI (Command Line Interface) utilizando programación Swift puro en Xcode, y luego portaremos esa lógica para crear aplicaciones multiplataforma en SwiftUI para iOS, macOS y watchOS.

Prepárate para transformar tu flujo de trabajo en Xcode.

Parte 1: Entendiendo la Arquitectura Gemini en el Ecosistema Apple

Antes de escribir una sola línea de código, es vital entender qué estamos construyendo. No vamos a usar una herramienta cerrada; vamos a usar el SDK oficial de Google Generative AI para Swift.

El flujo de trabajo que implementaremos tiene dos vertientes:

  1. Herramienta de Línea de Comandos (CLI): Un ejecutable de macOS escrito en Swift que nos permitirá consultar a Gemini directamente desde nuestra terminal. Ideal para scripts, automatización o generación rápida de código.
  2. Aplicación SwiftUI Multiplataforma: Una interfaz gráfica que consume la misma lógica para funcionar en iPhone, Mac y Apple Watch.

Requisitos Previos

  • Xcode 15+: Necesario para soportar las últimas características de concurrencia de Swift.
  • Swift 5.9+: Para el uso de macros y async/await avanzado.
  • API Key de Google AI: Debes generarla en Google AI Studio.

Parte 2: Construyendo tu propia “Gemini CLI” con Swift

Como iOS Developer, a menudo olvidamos que Swift es un lenguaje de propósito general excelente para scripts y herramientas de sistema. Vamos a crear una herramienta llamada gemini-swift.

Paso 1: Configuración del Proyecto en Xcode

  1. Abre Xcode.
  2. Selecciona Create New Project.
  3. Ve a la pestaña macOS y selecciona Command Line Tool.
  4. Nombra el proyecto GeminiCLI.
  5. Asegúrate de que el lenguaje sea Swift.

Paso 2: Importando el SDK

Para interactuar con el modelo, usaremos el Swift Package Manager (SPM).

  1. Ve a la configuración del proyecto (el icono azul en la raíz).
  2. Selecciona la pestaña “Package Dependencies”.
  3. Añade el paquete: https://github.com/google/google-generative-ai-sdk-swift.
  4. Añádelo a tu target GeminiCLI.

Paso 3: ArgumentParser (Opcional pero recomendado)

Para que nuestra CLI sea profesional, necesitamos procesar argumentos (como -m "mensaje"). Apple ofrece una librería excelente para esto. Añade también este paquete vía SPM: https://github.com/apple/swift-argument-parser.

Paso 4: El Código Fuente de la CLI

Abre el archivo main.swift. Vamos a reemplazar el código por una estructura robusta que acepte un prompt y devuelva la respuesta de la IA.

import Foundation
import ArgumentParser
import GoogleGenerativeAI

@main
struct GeminiTool: AsyncParsableCommand {
    
    // Configuración de la herramienta
    static var configuration = CommandConfiguration(
        commandName: "gemini",
        abstract: "Una herramienta CLI para interactuar con Google Gemini en Swift."
    )
    
    // Argumento de entrada: El prompt
    @Argument(help: "El texto que quieres enviar a la IA.")
    var prompt: String
    
    // Flag opcional para ser creativo
    @Flag(name: .shortAndLong, help: "Activa el modo creativo.")
    var creative: Bool = false
    
    func run() async throws {
        // 1. Configuración de seguridad (NUNCA hardcodear API Keys en producción real)
        // Lo ideal es leerla de una variable de entorno
        guard let apiKey = ProcessInfo.processInfo.environment["GEMINI_API_KEY"] else {
            print("Error: Por favor configura la variable de entorno GEMINI_API_KEY.")
            return
        }
        
        // 2. Inicialización del modelo
        let model = GenerativeModel(name: "gemini-pro", apiKey: apiKey)
        
        print("🤖 Consultando a Gemini...")
        
        do {
            // 3. Generación de contenido
            let response = try await model.generateContent(prompt)
            
            if let text = response.text {
                print("\n--- RESPUESTA ---\n")
                print(text)
                print("\n-----------------\n")
            } else {
                print("Gemini no devolvió texto.")
            }
        } catch {
            print("Error: \(error.localizedDescription)")
        }
    }
}

Paso 5: Ejecutando tu Gemini CLI

Para probar esto, necesitas editar el esquema en Xcode para pasar argumentos o compilarlo y ejecutarlo en terminal.

  1. Compila con Cmd + B.
  2. Localiza el binario en la carpeta DerivedData.
  3. En tu terminal:
export GEMINI_API_KEY="tu_api_key_aqui"
./GeminiCLI "Escribe un poema sobre la programación Swift"

¡Felicidades! Acabas de crear tu propia interfaz de línea de comandos de IA usando programación Swift. Esto demuestra que Swift no es solo para apps visuales.

Parte 3: De la Terminal a la UI: Integración en SwiftUI

Ahora que dominamos la lógica base, vamos a llevar esto a una aplicación real. Un iOS Developer moderno debe saber estructurar este código para que sea reutilizable en iOS, macOS y watchOS.

Arquitectura: MVVM y Clean Architecture

No pegaremos el código en la Vista. Crearemos una capa de servicio.

1. El Servicio de IA (GeminiService.swift)

Este archivo será el corazón de nuestra lógica, agnóstico de la interfaz (UI).

import Foundation
import GoogleGenerativeAI

enum GeminiError: Error {
    case noAPIKey
    case networkError(String)
}

actor GeminiService {
    private var model: GenerativeModel?
    
    init() {
        // En una app real, usa un archivo .plist seguro o Keychain
        if let path = Bundle.main.path(forResource: "GenerativeAI-Info", ofType: "plist"),
           let plist = NSDictionary(contentsOfFile: path),
           let key = plist["API_KEY"] as? String {
            self.model = GenerativeModel(name: "gemini-pro", apiKey: key)
        }
    }
    
    func sendMessage(_ text: String) async throws -> String {
        guard let model = model else { throw GeminiError.noAPIKey }
        
        do {
            let response = try await model.generateContent(text)
            return response.text ?? "Sin respuesta"
        } catch {
            throw GeminiError.networkError(error.localizedDescription)
        }
    }
    
    // Función para Streaming (Efecto máquina de escribir)
    func sendMessageStream(_ text: String) -> AsyncThrowingStream<String, Error> {
        return AsyncThrowingStream { continuation in
            guard let model = model else {
                continuation.finish(throwing: GeminiError.noAPIKey)
                return
            }
            
            Task {
                do {
                    for try await chunk in model.generateContentStream(text) {
                        if let text = chunk.text {
                            continuation.yield(text)
                        }
                    }
                    continuation.finish()
                } catch {
                    continuation.finish(throwing: error)
                }
            }
        }
    }
}

2. El ViewModel (ChatViewModel.swift)

El ViewModel conecta nuestro servicio con SwiftUI. Usaremos el framework @Observable (disponible desde iOS 17) para una sintaxis más limpia, o ObservableObject para compatibilidad anterior.

import SwiftUI

@MainActor
class ChatViewModel: ObservableObject {
    @Published var responseText: String = ""
    @Published var isLoading: Bool = false
    @Published var userInput: String = ""
    
    private let service = GeminiService()
    
    func sendQuery() {
        guard !userInput.isEmpty else { return }
        
        isLoading = true
        responseText = "" // Limpiar respuesta anterior
        let query = userInput
        userInput = "" // Limpiar input
        
        Task {
            do {
                // Usamos streaming para mejor UX
                let stream = await service.sendMessageStream(query)
                for try await chunk in stream {
                    responseText += chunk
                }
            } catch {
                responseText = "Error: \(error.localizedDescription)"
            }
            isLoading = false
        }
    }
}

Parte 4: Interfaz de Usuario Multiplataforma en SwiftUI

La ventaja de SwiftUI y Xcode es que podemos diseñar una vista que funcione en iPhone, Mac y Apple Watch con cambios mínimos.

Vista Principal (ContentView.swift)

import SwiftUI

struct ContentView: View {
    @StateObject private var viewModel = ChatViewModel()
    @FocusState private var isInputFocused: Bool
    
    var body: some View {
        NavigationStack {
            VStack(spacing: 20) {
                // Área de Resultados
                ScrollView {
                    VStack(alignment: .leading) {
                        if viewModel.responseText.isEmpty && !viewModel.isLoading {
                            ContentUnavailableView(
                                "Pregunta a Gemini",
                                systemImage: "sparkles",
                                description: Text("Escribe algo para comenzar la magia.")
                            )
                            .opacity(0.7)
                        } else {
                            Text(viewModel.responseText)
                                .font(.body)
                                .padding()
                                .textSelection(.enabled) // Importante para macOS
                        }
                    }
                    .frame(maxWidth: .infinity, alignment: .leading)
                }
                .background(Color.gray.opacity(0.1))
                .cornerRadius(12)
                
                // Área de Input
                HStack {
                    TextField("Escribe tu prompt...", text: $viewModel.userInput)
                        .textFieldStyle(.roundedBorder)
                        .focused($isInputFocused)
                        .disabled(viewModel.isLoading)
                        .onSubmit {
                            viewModel.sendQuery()
                        }
                    
                    if viewModel.isLoading {
                        ProgressView()
                            .scaleEffect(0.8)
                    } else {
                        Button(action: {
                            viewModel.sendQuery()
                        }) {
                            Image(systemName: "arrow.up.circle.fill")
                                .font(.title2)
                        }
                        .disabled(viewModel.userInput.isEmpty)
                    }
                }
                .padding()
            }
            .padding()
            .navigationTitle("Gemini Swift")
            #if os(macOS)
            .frame(minWidth: 400, minHeight: 500)
            #endif
        }
    }
}

Adaptación para watchOS

Para el Apple Watch, el espacio es crítico. En Xcode, dentro de tu target de Watch App, puedes reutilizar el ChatViewModel pero simplificar la vista.

// WatchContentView.swift
import SwiftUI

struct WatchContentView: View {
    @StateObject private var viewModel = ChatViewModel()
    
    var body: some View {
        VStack {
            ScrollView {
                Text(viewModel.responseText)
            }
            
            // Usar TextField en watchOS activa dictado o teclado QWERTY automáticamente
            TextField("Preguntar...", text: $viewModel.userInput)
                .onSubmit {
                    viewModel.sendQuery()
                }
        }
    }
}

Parte 5: Avanzado – Multimodalidad e Imágenes

Un verdadero experto en Gemini CLI en Xcode sabe que Gemini no es solo texto. Es multimodal. Podemos enviarle imágenes.

Para esto, necesitamos actualizar nuestro servicio para usar gemini-pro-vision (o las versiones más recientes como gemini-1.5-flash).

// Actualización en GeminiService.swift

func analyzeImage(_ image: UIImage, prompt: String) async throws -> String {
    // Configurar modelo de visión
    let visionModel = GenerativeModel(name: "gemini-1.5-flash", apiKey: "API_KEY")
    
    // Convertir UIImage a formato compatible
    // Nota: En macOS sería NSImage
    let response = try await visionModel.generateContent(prompt, image)
    return response.text ?? ""
}

En la UI, simplemente añadiríamos un PhotosPicker (disponible en SwiftUI) para seleccionar la imagen y pasarla a esta función.

Conclusión: El Futuro del Desarrollo en Swift

Hemos recorrido un camino largo. Empezamos en la terminal, creando una herramienta de línea de comandos (Gemini CLI) usando programación Swift puro, demostrando la versatilidad del lenguaje. Luego, tomamos ese núcleo lógico y lo envolvimos en una arquitectura MVVM moderna con SwiftUI y Xcode, desplegando en iOS, macOS y watchOS.

Para un iOS Developer, integrar IA no es el futuro, es el presente. Herramientas como Gemini nos permiten crear aplicaciones que entienden, ven y generan contenido, elevando la experiencia de usuario a niveles imposibles de alcanzar con la programación tradicional.

¿Qué sigue?

  1. Implementa el historial de chat para que Gemini recuerde el contexto.
  2. Usa SwiftData para guardar las conversaciones localmente.
  3. Experimenta con “Function Calling” de Gemini para que la IA pueda ejecutar código real dentro de tu app (como encender la linterna o poner una alarma).

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

Gemini CLI en SwiftUI

Next Article

Cómo personalizar TabView en SwiftUI

Related Posts