Aprende a generar imágenes con DALL·E 2 (Inteligencia Artificial) y SwiftUI
Aprende a generar imágenes con DALL·E 2 (Inteligencia Artificial) y SwiftUI

DALL·E 2 en SWIFTUI y SWIFT 🤖 Creamos una app para GENERAR imágenes con Inteligencia Artificial

Creamos una app en SWIFTUI integrando la API de DALL·E 2 para poder generar una imagen a partir de un texto (prompt) con Inteligencia Artificial. Investigamos la API de openai y creamos un API_KEY. Al descargar la imagen, añadimos un Button para poder descargarla y así compartirla en redes sociales

SwiftBeta

Tabla de contenido


👇 SÍGUEME PARA APRENDER SWIFTUI, SWIFT, XCODE, etc 👇
Aprende a crear imágenes generadas por Inteligencia Artificial en SwiftUI
Aprende a crear imágenes generadas por Inteligencia Artificial en SwiftUI

Hoy en SwiftBeta vamos a aprender a integrar la API de DALL·E 2 en una aplicación creada con SWIFTUI.

Para darte un poco de contexto y de forma muy resumida, DALL·E 2 es una IA (una inteligencia artificial) capaz de crear imágenes a través de un texto.

  • En el primer video de esta nueva serie vamos a crear la primera parte de nuestra app, en esta parte añadiremos un texto, y al enviarlo en una petición HTTP a la API de DALL·E 2, como respuesta obtendremos lo que hemos descrito en el texto. La IA generará una imagen a partir del texto que le enviemos, ya verás que va a molar muchísimo.
  • La segunda parte, en lugar de tener solo un input, tendremos dos. Añadiremos una imagen que queremos modificar y aplicaremos una máscara. Esta máscara servirá para indicar a la IA el espacio concreto de nuestra imagen que queremos modificar, pero ¿cómo sabe la IA que queremos añadir dentro de la máscara? Para hacerlo debemos enviar el segundo input, escribiremos un texto descriptivo para indicar a la IA lo que queremos generar, igual que en la primera parte. Y al final como respuesta de la API de DALL·E 2 obtendremos la imagen original, y dentro de la máscara, obtendremos la imagen generada con el texto que hemos especificado.
  • Y puede que cree más videos sobre este tema, si después de ver este video quieres que cree más, no dudes en añadirlo en los comentarios.

Vas a ver que va estar muy entretenido crear este tutorial desde 0, y lo voy a explicar paso a paso, desde cómo crear el token en el portal de developers de DALL·E 2, hasta de cómo subir fotos en una petición HTTP utilizando Alamofire (un framework muy usado en el entorno iOS)

Pero antes, si quieres apoyar el contenido que subo cada semana, suscríbete. De esta manera seguiré publicando contenido completamente grautito en Youtube.

Creamos el proyecto en Xcode

Vamos al lío, lo primero de todo que vamos hacer es crear nuestro proyecto de Xcode. En mi caso lo voy a llamar SwiftBetaDALLE

Creamos App en Xcode
Creamos App en Xcode

Creamos vista en SwiftUI

Lo siguiente que vamos hacer es crear una nueva vista en SwiftUI. Pulsamos CMD+N y llamamos a la vista GenerateView.

Dentro de la nueva vista vamos a añadir un Text y un Form

struct GenerateView: View {
    var body: some View {
        VStack {
            Text("¡Suscríbete a SwifBeta para más contenido gratuito! 🚀")
            Form {
                // TODO
            }
        }
    }
}
Añadimos a nuestra View un VStack y dentro un Text y Form en SwiftUI

Y dentro del Form vamos a añadir un TextField. Dentro de este TextField podremos especificar el texto que describa la imagen que queremos generar.

struct GenerateView: View {
    @State var text = "Two astronauts exploring the dark, cavernous interior of a huge derelict spacecraft, digital art"
    
    var body: some View {
        VStack {
            Text("¡Suscríbete a SwifBeta para más contenido gratuito! 🚀")
            Form {
                TextField("Describe the image that you want to generate",
                          text: $text,
                          axis: .vertical)
                .lineLimit(10)
                .lineSpacing(5)
            }
        }
    }
}
Dentro del Form añadimos más vistas en SwiftUI

Para hacerlo añadimos el TextField y también creamos una propiedad @State de tipo String. En esta propiedad voy a añadir un valor por defecto para crear una imagen con DALL·E 2.

Si quieres crear otra imagen con otro texto, que sepas que puedes buscarlas en Google. Hay varias webs que publican frases para generar imágenes muy chulas, puedes buscar "promt DALL·E 2" y ya tendrás resultados para probar.

Lo siguiente que voy hacer es crear un Button para poder lanzar la acción de crear una imagen. Es decir, al pulsar este Button recogeremos el valor del TextField y lo enviaremos a la API de DALL·E 2:

struct GenerateView: View {
    @State var text = "Two astronauts exploring the dark, cavernous interior of a huge derelict spacecraft, digital art"
    
    var body: some View {
        VStack {
            Text("¡Suscríbete a SwifBeta para más contenido gratuito! 🚀")
            Form {
                TextField("Describe the image that you want to generate",
                          text: $text,
                          axis: .vertical)
                .lineLimit(10)
                .lineSpacing(5)
                
                HStack {
                    Spacer()
                    Button("🪄 Generate Image") {
                        // TODO
                    }
                    .buttonStyle(.borderedProminent)
                    .disabled(false)
                    .padding(.vertical, 12)
                    Spacer()
                }
            }
        }
    }
}
Nuestro Form ya tiene un Textfield y un Button en SwiftUI

Nuestra vista ya va tomando forma. Ya tenemos un TextField para añadir el texto con el que queremos generar la imagen, y también tenemos el Button para lanzar la acción. Ahora solo nos falta poder mostrar el resultado obtenido al hacer la petición HTTP, y para hacerlo vamos a usar otra vista en SWIFTUI llamada AsyncImage

Al inicio de nuestro Form vamos a añadir el siguiente código:

AsyncImage(url: URL(string: "")) { image in
    image
        .resizable()
        .scaledToFit()
} placeholder: {
    VStack {
        Image(systemName: "photo.on.rectangle.angled")
            .resizable()
            .scaledToFill()
            .frame(width: 40, height: 40)
    }
    .frame(width: 300, height: 300)
}
Al principio del Form añadimos un AsyncImage en SwiftUI

Dentro del closure del Placeholder hemos añadido una imagen, un placeholder hasta que recibamos la respuesta de la petición HTTP. Perfecto, ya tenemos la vista creada, luego volveremos para aplicar unos pequeños cambios.

A continuación vamos a crear un nuevo fichero llamado ViewModel y vamos a centrarnos en la lógica para realizar la petición HTTP a la API de DALL·E 2.

Creamos ViewModel con petición HTTP

Lo primero que vamos a añadir es una propiedad de tipo URLSession. Esta clase nos permitirá poder realizar la petición HTTP al endpoint de DALL·E 2.

final class ViewModel: ObservableObject {
    private let urlSession: URLSession
    
    init(urlSession: URLSession = URLSession.shared) {
        self.urlSession = urlSession
    }
}
Creamos el ViewModel con una propiedad de tipo URLSession

A continuación, vamos a crear un método que acepte como parámetro de entrada el texto que hay dentro de nuestro TextField, es decir, cuando llamemos a este método desde la vista le pasaremos el texto que hay dentro del TextField.

final class ViewModel: ObservableObject {
    private let urlSession: URLSession
    
    init(urlSession: URLSession = URLSession.shared) {
        self.urlSession = urlSession
    }
    
    func generateImage(withText text: String) {
        
    }
}
Creamos el método que realizará la petición HTTP

Antes de continuar, debemos saber el endpoint que vamos a usar para generar una imagen con un texto. Para conocer esta API vamos a la documentation de DALL·E 2, esta URL:

OpenAI API
An API for accessing new AI models developed by OpenAI
URL para ver la documentación de la API de DALL·E 2

La URL que debemos usar es esta de aquí:

Endpoint de DALL·E 2 que usaremos para generar nuestra imagen con Inteligencia Artificial
Endpoint de DALL·E 2 que usaremos para generar nuestra imagen con Inteligencia Artificial

Fíjate que aparte del endpoint, necesitamos enviar en esta petición una serie de valores. Para ser más concreto, son estos de aquí:

  • 3 parámetros, llamados prompt, que es el texto que enviamos desde nuestro TextField, otro parámetro llamado n, que es el número de imágenes que queremos recibir como resultado (en nuestro caso solo queremos recibir una imagen) y el size, que es el tamaño de nuestra imagen (la imagen que generará la IA),
  • Otro parámetro, o mejor dicho valor que hay que tener en cuenta, es el API_KEY que tenemos que enviar. En los próximos minutos crearemos uno.

Lo primero de todo, vamos a especificar la URL que vamos a usar:

    func generateImage(withText text: String) {
        guard let url = URL(string: "https://api.openai.com/v1/images/generations") else {
            return
        }
    }
Creamos la URL con el endpoint de DALL·E 2

Ahora, vamos a crear una URLRequest con la información necesaria para realizar la petición correctamente, voy a escribir todo el código y lo explico paso a paso.

    func generateImage(withText text: String) async {
        guard let url = URL(string: "https://api.openai.com/v1/images/generations") else {
            return
        }
        
        var urlRequest = URLRequest(url: url) // 1
        
        urlRequest.httpMethod = "POST" 		// 2
        urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type") // 3
        urlRequest.addValue("Bearer TODO", forHTTPHeaderField: "Authorization") // 4
        
        let dictionary: [String : Any] = [ // 5
            "n": 1,
            "size": "1024x1024",
            "prompt": text
        ]
        
        urlRequest.httpBody = try! JSONSerialization.data(withJSONObject: dictionary, options: []) // 6
    }
Añadimos toda la información necesaria: Header y Parámetros
  1. Creamos la URLRequest a partir de la url
  2. Nuestro httpMethod será POST
  3. Añadimos una key en el header de nuestra petición indicando que será de tipo "application/json"
  4. Añadimos otra key a nuestro header, esta vez será la autenticación necesaria para poder realizar la petición correctamente. Una vez acabemos este método crearemos nuestro API_Key en nuestra sección de developers de DALL-E 2 y lo añadiremos aquí.
  5. Creamos un Dictionary con todos los parámetros necesarios (justo los que te comentaba antes: el número de imágenes que queremos recibir como resultado, el tamaño de la imagen y el prompt necesario para generar la imagen, el texto de nuestro TextField)
  6. Pasamos todos los datos de dictionary y los añadimos al body de nuestra petición HTTP.

Una vez tenemos todo esto, vamos a continuar añadiendo código a nuestro método generateImage. Ahora necesitamos saber los datos que vamos a recibir, es decir, cuando nosotros enviemos toda esta información en nuestra petición y la IA nos genera una imagen, ¿Cómo la vamos a recibir? Es decir, como transformamos la respuesta de la API de DALL-E 2 a algo que nuestra app pueda entender.
Pues el resultado que obtenemos si todo va bien, es una URL, y va a ser con el formato del siguiente JSON, lo puedes ver en esta app que uso para debuggar petiones HTTP llamada PAW;

{
  "created": 1667849478,
  "data": [
    {
      "url": "https://oaidalleapiprodscus.blob.core.windows.net/private/org-V9RsqRAp77mY3mn0W7Uj4A8q/user-7knqcxyyqdVNkWzAEJ5sOBEQ/img-7hxAodUi3kNO0ImLRL8vFaIp.png?st=2022-11-07T18%3A31%3A18Z&se=2022-11-07T20%3A31%3A18Z&sp=r&sv=2021-08-06&sr=b&rscd=inline&rsct=image/png&skoid=6aaadede-4fb3-4698-a8f6-684d7786b067&sktid=a48cca56-e6da-484e-a814-9c849652bcb3&skt=2022-11-07T01%3A48%3A37Z&ske=2022-11-08T01%3A48%3A37Z&sks=b&skv=2021-08-06&sig=m908gzZCwJBHliSCnvgUNbrPjrI3jcYbzte6TMQhHEI%3D"
    }
  ]
}
JSON que recibimos cuando todo ha ido correctamente

Al final, nosotros vamos a coger la URL que aparece en el JSON y la vamos a asignar al AsyncImage de nuestra vista en SwiftUI. Pero para hacer esto, primero debemos parsear correctamente el JSON, así que vamos a crear el modelo. Pulsamos CMD+N y creamos un fichero llamado Model

struct DataResponse: Decodable {
    let url: String
}

struct ModelResponse: Decodable {
    let data: [DataResponse]
}
Modelo con el que paresearemos los datos recibidos al realizar la petición HTTP

Sencillo, ¿verdad? A continuación vamos a añadir la últimas lineas nuestro ViewModel

do {
    let (data, _) = try await urlSession.data(for: urlRequest) // 1
    let model = try JSONDecoder().decode(ModelResponse.self, from: data) // 2
    DispatchQueue.main.async {
        guard let firstModel = model.data.first else { // 3
            return
        }
        let imageURL = URL(string: firstModel.url)
        print(imageURL)
    }
} catch {
    print(error.localizedDescription) // 4
}
Realizamos la petición HTTP
  1. Realizamos la petición HTTP con Async/Await en Swift
  2. Parseamos el JSON a un modelo de nuestro dominio (justo el modelo que hemos creado hace un momento)
  3. Extraemos el valor de la URL y la asignamos a una constante
  4. En caso de tener un error, lo capturamos y lo mostramos en un print. Lo suyo sería que al acabar el video gestionas los errores, en este caso solo los vamos a omitir, pero como buen developer siempre tienes que gestionarlos en tu app.

Ahora, para poder mostrar la imageURL en nuestra vista en SwiftUI, vamos a crear una property @Published llamada imageURL.

final class ViewModel: ObservableObject {
    private let urlSession: URLSession
    @Published var imageURL: URL?
    ...
}
Añadimos una propiedad llamada imageURL para poder refrescar la vista en SwiftUI

Y, justo donde asignábamos el valor obtenido en nuestra petición HTTP. Ahora lo que vamos hacer es asignarlo a la propiedad que acabamos de crear.

            DispatchQueue.main.async {
                guard let firstModel = model.data.first else {
                    return
                }
                self.imageURL = URL(string: firstModel.url)
            }
Asignamos el valor recibido de la petición HTTP a la propiedad que acabamos de crear

Volvemos al vista en SwiftUI. Ahora vamos a crear una propiedad de tipo ViewModel y vamos a crear una instancia. Y vamos a sustituir el parámetro de la URL por la propiedad del ViewModel. Es decir:

struct GenerateView: View {
    @State var text = "Two astronauts exploring the dark, cavernous interior of a huge derelict spacecraft, digital art"
    @StateObject var viewModel = ViewModel()
    
    var body: some View {
        VStack {
            Text("¡Suscríbete a SwifBeta para más contenido gratuito! 🚀")
            Form {
                AsyncImage(url: viewModel.imageURL) { image in
                    image
                        .resizable()
                        .scaledToFit()
En la vista instanciamos el ViewModel y usamos las propiedas y métodos que hemos creado

También vamos a conectar el Button que hemos creado con nuestro ViewModel. De esta manera cada vez que se pulse crearemos la petición HTTP y una nueva imagen se generará.

Generar API_KEY en DALLE2

Y ahora, finalmente, ya podemos probar nuestro código. PERO antes de probarlo debemos crear el API_Key que te comentaba. Para crear el API_Key debes tener una cuenta como developer en https://beta.openai.com , solo debes rellenar un formulario y tendrás créditos gratuitos para hacer miles de pruebas.

Una vez creada la cuenta, vamos a la sección de API Keys, y  puedes ir con el siguiente enlace

OpenAI API
An API for accessing new AI models developed by OpenAI

o desde el menu lateral de la página https://beta.openai.com/docs/guides/images

Una vez creada la cuenta en openai.com, podemos ir a la sección de API keys
Una vez creada la cuenta en openai.com, podemos ir a la sección de API keys

Una vez estás aquí dentro, tan solo debes dar a este botón

Generamos una API_Key nueva
Generamos una API_Key nueva

Al hacerlo te aparece un alert con una API_KEY. Cópiala y añadela al proyecto d Xcode.

Copiamos la API_Key y la añadimos a Xcode
Copiamos la API_Key y la añadimos a Xcode

¿Dónde la añadimos? En el header de nuestra petición HTTP, es el campo que te mencionada antes que ibamos a modificar, solo tienes que ir al método del ViewModel y pegar el API_Key en el header.

Si ahora vamos a nuestro Canvas y probamos de pulsar el Button (y esperamos) vemos que ha funcionado!

Si probamos nuestra app, este es el resultado
Si probamos nuestra app, este es el resultado

Al realizar la petición HTTP la API de DALLE2 tarde unos segundos en generar nuestra imagen.

Añadir ProgressView para mejorar la UI

Vamos a mejorar muy rápidamente la UI de nuestra app y vamos a añadir un ProgressView con un texto, de esta manera tenemos feedback de lo que está ocurriendo.

Vamos al ViewModel y vamos a crear una propiedad @Published de tipo Bool.

final class ViewModel: ObservableObject {
    private let urlSession: URLSession
    @Published var imageURL: URL?
    @Published var isLoading = false
    ...
}
Añadimos una propiedad llamada isLoading

Y vamos a modificar el valor de esta propiedad justo antes de realizar la petición HTTP y justo cuando haya finalizado.

do {
    DispatchQueue.main.async {
        self.isLoading = true
    }
    let (data, _) = try await urlSession.data(for: urlRequest)
    let model = try JSONDecoder().decode(ModelResponse.self, from: data)
    DispatchQueue.main.async {
        self.isLoading = false
        guard let firstModel = model.data.first else {
            return
        }
        self.imageURL = URL(string: firstModel.url)
    }
} catch {
    print(error.localizedDescription)
}
Cambiamos su valor antes y después de realizar la petición HTTP

Ahora volvemos a nuestra vista en SwiftUI y añadimos el siguiente cambio en la vista AsyncImage:

AsyncImage(url: viewModel.imageURL) { image in
    image
        .resizable()
        .scaledToFit()
} placeholder: {
    VStack {
        if !viewModel.isLoading {
            Image(systemName: "photo.on.rectangle.angled")
                .resizable()
                .scaledToFill()
                .frame(width: 40, height: 40)
        } else {
            ProgressView()
                .padding(.bottom, 12)
            Text("¡Tu imagen se está generando, espera 2 segundos! 🚀")
                .multilineTextAlignment(.center)
        }
    }
    .frame(width: 300, height: 300)
}
Usamos un ProgressView para darle feedback al user

Y también asignamos la propiedad isLoading al modificador el Button. De esta manera si se está realizando una petición HTTP el Button quedará deshabilitado:

Button("🪄 Generate Image") {
    Task {
        await viewModel.generateImage(withText: text)
    }
}
.buttonStyle(.borderedProminent)
.disabled(viewModel.isLoading)
.padding(.vertical, 12)

Si ahora probamos la vista en nuestro Canvas, vamos a ver qué ocurre

Aparece la ProgressView cuando realizamos la petición HTTP
Aparece la ProgressView cuando realizamos la petición HTTP

y a continuación aparece el resultado:

Cuando recibimos la respuesta de la petición HTTP, el ProgressView desaparece
Cuando recibimos la respuesta de la petición HTTP, el ProgressView desaparece

Ya lo tenemos. Pero ya que tenemos la imagen, ¿podríamos guardarla en nuestra galería de imágenes no?  De esta manera podríamos compartirla con nuestro amigos o redes sociales. Vamos a añadir un Button para que cuando DALL-E 2 nos genere una imagen, aparezca la posibilidad de descargar a nuestra galería.

Guardar imagen generada en la galería el iPhone o simulador

El método que vamos a añadir a nuestro ViewModel es el siguiente:

import UIKit 

func saveImageInGallery() {
    guard let imageURL = imageURL else {
        return
    }
    DispatchQueue.main.async {
        let data = try! Data(contentsOf: imageURL)
        let image = UIImage(data: data)!
        UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
    }
}
Método para guardar una imagen en la galería del iPhone (o simulador)

Y ahora vamos a añadir el Button que realizará esta lógica en nuestra vista de SwiftUI:

AsyncImage(url: viewModel.imageURL) { image in
    image
        .resizable()
        .scaledToFit()
        .overlay(alignment: .bottomTrailing, content: {
            Button {
                viewModel.saveImageInGallery()
            } label: {
                HStack {
                    Image(systemName: "arrow.down.circle.fill")
                        .resizable()
                        .frame(width: 30, height: 30)
                        .shadow(color: .black, radius: 0.2)
                }
                .padding(8)
                .foregroundColor(.green)
            }
        })
} placeholder: {
    VStack {
        if !viewModel.isLoading {
            Image(systemName: "photo.on.rectangle.angled")
                .resizable()
                .scaledToFill()
                .frame(width: 40, height: 40)
        } else {
            ProgressView()
                .padding(.bottom, 12)
            Text("¡Tu imagen se está generando, espera 2 segundos! 🚀")
                .multilineTextAlignment(.center)
        }
    }
    .frame(width: 300, height: 300)
}
Añadimos un Button en SwiftUI para poder descargar la imagen

Y ahora podemos compilar y probar nuestra app. Vamos a guardar la imagen que nos ha generado la IA, no va a funcionar, pero quiero que veas que ocurre antes de arreglarlo.

Al probarlo, la app crashea al intentare guardar la app en la galería de nuestro iPhone o simulador, esto es porque debemos especificar en nuestro Info.plist una key (por temas de seguridad). Si te fijas en la consola, allí aparece el error y como solucionarlo.

Añadimos permisos para guardar en nuestra galería de fotos

Vamos al Info.plist y añadimos la siguiente key NSPhotoLibraryAddUsageDescription y podemos añadir el siguiente texto Guardar imágenes en tu galería generadas por DALLE2.

Si ahora compilamos y probamos nuestra app vamos a ver qué ocurre. Todo funciona perfectamente 🙌

Añadimos una TabView

Ahora si queremos integrar la vista en nuestra vista ContentView, podemos dejarlo preparado para nuestro próximo video. Vamos a añadir un TabView.

struct ContentView: View {
    var body: some View {
        TabView {
            GenerateView()
                .tabItem {
                    Image(systemName: "wand.and.stars.inverse")
                    Text("Generate")
                }
        }
    }
}
Añadimos un TabView para poder navegar entre las diferentes vistas de nuestra app

Conclusión

Hoy hemos aprendido a integrar la API de DALL-E 2 para poder generar imágenes creadas por una inteligencia artificial en SwiftUI. En el próximo video vamos a continuar explorando esta API para poder modificar imágenes asignando una máscara a una imagen y un texto describiendo lo que queremos generar dentro de la máscara.