Crea una app con VisionKit para capturar texto de tu cámara en tiempo real
Crea una app con VisionKit para capturar texto de tu cámara en tiempo real

App SwiftUI: Captura y Reproduce Texto con VisionKit

Implementa VisionKit y AVSpeechSynthesizer en SwiftUI. Programa una app que capta y lee texto en tiempo real usando la cámara del dispositivo

SwiftBeta

Tabla de contenido


👇 SÍGUEME PARA APRENDER SWIFTUI, SWIFT, XCODE, etc 👇
Aprende a usar VisionKit para capturar texto en tiempo real en SwiftUI
Aprende a usar VisionKit para capturar texto en tiempo real en SwiftUI

Hoy en SwiftBeta vamos a aprender a usar una parte de VisionKit. Vamos a crear una app de ejemplo capaz  de capturar el texto que captura nuestra cámara en tiempo real, es decir desde nuestro iPhone. Vas a ver que es un video muy chulo y práctico, donde vamos a explorar el framework VisionKit.

Esta app es la que vamos a crear. Con mi dispositivo real, un iPhone, estoy mostrando la web de SwiftBeta.com, y fíjate como la cámara detecta fragmentos de código, y al pulsar en la pantalla puedo marcar el texto que quiero reproducir. Al hacerlo, aparece el texto en el bottom sheet y si le doy al play puedo reproducirlo. La verdad que es una pasada, y vas a ver que con muy pocas líneas de código podemos crear esta app.

Creamos el proyecto en Xcode

Lo primero de todo que vamos hacer es crear una app en Xcode. En mi caso la voy a llamar CaptureNow, pero tu le puedes poner el nombre que quieras. Y antes de crearla, muy importante seleccionamos en Interface SwiftUI, ya que vamos a usar este framework para crear la UI de nuestra app.

Una vez hemos creado nuestra app, pulsamos COMMAND+N y creamos un fichero llamado ScanView. En esta vista es donde va a pasar toda la magia, así que debes prestar atención.

Una vez hemos creado la nueva View en SwiftUI, borramos la preview y añadimos una class llamada ScanProvider:

final class ScanProvider {
...
}

En esta clase vamos a usar un delegado llamado DataScannerViewControllerDelegate, este delagado nos va a permitir detectar texto en tiempo real capturado por nuestra cámara. Es decir, nosotros al iniciar nuestra app mostraremos la cámara, y con este delegado podremos detectar cuando aparece texto, y por lo tanto podremos aplicar la lógica que queramos, que en este caso es capturarlo para poderlo reproducir. Vamos a conformar el protocolo dentro de esta nueva clase, pero antes vamos a importar el framework VisionKit

final class ScanProvider: NSObject, DataScannerViewControllerDelegate {
...
}

Una vez hecho esto, si intentamos compilar vamos a ver qué ocurre. Todo funciona perfectamente, pero no estamos haciendo nada. A continuación vamos a usar 2 métodos que son opcionales al conformar el protocolo DataScannerViewControllerDelegate.

Vamos a empezar por el método que nos va a proporcionar el texto que se está capturando en nuestra cámara:

func dataScanner(_ dataScanner: DataScannerViewController, didTapOn item: RecognizedItem) {
    switch item { // 1
    case .text(let recognizedText): // 2
        print(recognizedText.transcript) // 3
    case .barcode(_): // 4
        break
    @unknown default:
        break
    }
}

Voy a explicar línea por línea:

  1. Nuestra cámara está capturando texto en tiempo real, si toco el fragmento de texto, este método es llamado ejecutando todo su código. El item que he pulsado es un enum, por lo tanto utilizo un switch para extraer su valor.
  2. Este es el case del texto
  3. Obtengo el texto que ha podido capturar la cámara
  4. El item pulsado también puede ser un código de barras, a parte del texto.

El siguiente método que vamos a implementar es un método del delegado que se ejecuta cuando encuentra un error al capturar texto con la cámara en tiempo real. Vamos a implementarlo por si a lo largo de este video aparece algún error, así podemos identificarlo rápido y arreglarlo.

func dataScanner(_ dataScanner: DataScannerViewController,
                 becameUnavailableWithError error: DataScannerViewController.ScanningUnavailable) {
    print(error)
}

Una vez hemos implementado estos 2 métodos, vamos a guardar el texto y el error en propiedades de tipo @Published, de esta manera podremos mostrar la información en nuestra View.

Creamos 2 propiedades nuevas dentro de ScanProvider, y nuestra clase debería de quedar de la siguienta manera:

final class ScanProvider: NSObject, DataScannerViewControllerDelegate {
    @Published var error: DataScannerViewController.ScanningUnavailable?
    @Published var text: String = ""
    
    func dataScanner(_ dataScanner: DataScannerViewController, didTapOn item: RecognizedItem) {
        switch item {
        case .text(let recognizedText):
            text = recognizedText.transcript
            print(text)
        case .barcode(_):
            break
        @unknown default:
            break
        }
    }
    
    func dataScanner(_ dataScanner: DataScannerViewController,
                     becameUnavailableWithError error: DataScannerViewController.ScanningUnavailable) {
        self.error = error
    }
}

Perfecto, de momento estamos capturando el texto de nuestra cámara en tiempo real. Más tarde volveremos para reproducir el texto. El siguiente paso que vamos a realizar es implementar la vista ScanView.

Dentro de la vista ScanView vamos a conformar el protocolo UIViewControllerRepresentable, este protocolo nos sirve para poder empaquetar un ViewController y así poderlo usar muy fácilmente en SwiftUI. Este protocolo ya lo vimos en varios videos del canal, y es muy sencillo de implementar. Si conformamos el protocolo en nuestra SwiftUI View qué ocurre? Exacto, tenemos un error, necesitamos implementar los métodos de este protocolo sí o sí (no como antes con el DataScannerViewControllerDelegate que eran opcionales).

struct ScanView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> DataScannerViewController {
        
    }
    
    func updateUIViewController(_ uiViewController: DataScannerViewController, context: Context) {
        
    }
}

Estos son los dos método que debemos crear para que el compilador no se queje. El último lo dejaremos vacío, pero el primero vamos a crear la siguiente implementación.

En el primero, fíjate que estamos retornando un tipo de ViewController llamado DataScannerViewController, este Controller encapsula muchísima lógica y nos libera de implementarla por nosotros mismos. Es decir, dentro de este ViewController está toda la lógica de mostrar la cámara para empezar a capturar texto. Creamos la siguiente implementación:

func makeUIViewController(context: Context) -> DataScannerViewController {
    let dataScannerViewController = DataScannerViewController(recognizedDataTypes: [.text()],
                                                              qualityLevel: .fast,
                                                              isHighlightingEnabled: true) // 1
    try? dataScannerViewController.startScanning() // 2
    return dataScannerViewController // 3
}

Vamos a ver línea por línea:

  1. Usamos el inicializador de DataScannerViewController. Aquí indicamos que queremos que nuestra cámara capture texto, que lo haga con un nivel de calidad rápido, y que se marque con un rectangulo el texto que va capturando la cámara en tiempo real.
  2. Indicamos al ViewController que empieza a scanear todo lo que vaya capturando de la cámara.
  3. Retornamos el ViewController, de esta manera podremos utilizarlo en los próximos minutos en nuestra vista en SwiftUI (en la vista ContentView)

Fíjate que tenemos el ScanView que básicamente es el DataScannerViewController, y también tenemos el ScanProvider, que es el DataScannerViewControllerDelegate, es decir, la clase que va a permitir tratar toda la información que vaya capturando la cámara. Tenemos que conectar estas dos partes, y es muy sencillo. Dentro de ScanView creamos una propiedad @ObservedObject de tipo ScanProvider.

struct ScanView: UIViewControllerRepresentable {
    @ObservedObject var scanProvider: ScanProvider
    ...
}

Al crear la propiedad tenemos un error, vamos a conformar el protocolo ObservableObject en ScanProvider.

final class ScanProvider: NSObject, DataScannerViewControllerDelegate, ObservableObject {
...
}

Y ahora asignamos como delegate del DataScannerViewController, la implementación de nuestro scanProvider:

func makeUIViewController(context: Context) -> DataScannerViewController {
    let dataScannerViewController = DataScannerViewController(recognizedDataTypes: [.text()],
                                                              qualityLevel: .fast,
                                                              isHighlightingEnabled: true)
    dataScannerViewController.delegate = scanProvider
    try? dataScannerViewController.startScanning()
    return dataScannerViewController
}

Y ahora si que podemos decir, que estas dos partes están conectadas. Cuando la cámara del DataScannerViewController encuentre texto, y nosotros pulsemos en la sección del texto, el método del ScanProvider se ejecutará, almacenando en la propiedad text su valor. Muy chulo, pero ahora falta una última parte.

ContentView

Dentro de nuestra vista ContentView creamos una propiedad @StateObject y creamos una instancia de ScanProvider:

struct ContentView: View {
    @StateObject var scanProvider = ScanProvider()
...
}

Y lo siguiente que hacemos es llamar a nuestra vista ScanView. Al final, debe quedarnos un ContentView de la siguiente manera:

struct ContentView: View {
    @StateObject var scanProvider = ScanProvider()
    var body: some View {
        ScanView(scanProvider: scanProvider)
    }
}

Vamos a ver qué ocurre al compilar. Si compilamos aparece la cámara marcando con rectángulos amarillos las secciones donde aparece texto. Y si pulsamos dentro del rectángulo, el texto que ha capturado la cámara se muestra por consola. Ya lo tenemos casi! ahora vamos a añadir un sheet para mostrar el texto que ha capturado la cámara.

Sheet en SwiftUI

Para mostrar el sheet, debemos tener un estado que nos indique cuándo mostrarlo y cuando ocultarlo. En este caso, creamos una propiedad en nuestra class ScanProvider @Publisher de tipo Booleano:

@Published var showSheet = false

¿Y dónde modificamos este valor? cada vez que un user hacer tap en un fragmento de texto. Justo cuando asignamos el valor a la propiedad text, en este caso asignamos la siguiente línea:

showSheet.toggle()

Una vez hecho, volvemos a nuestra vista ContentView. Aquí vamos a usar el ViewModifier sheet que ya hemos visto en varios videos del canal, voy a poner el modo rápido y luego lo explicaré:

import SwiftUI

struct ContentView: View {
    @StateObject var scanProvider = ScanProvider()
    var body: some View {
        ScanView(scanProvider: scanProvider)
            .sheet(isPresented: $scanProvider.showSheet) {
                VStack(alignment: .leading) {
                    Text(scanProvider.text)
                        .font(.system(.body, design: .rounded))
                        .padding(.top, 20)
                        .padding(.horizontal, 20)
                    Spacer()
                }
                .presentationDragIndicator(.visible)
                .presentationDetents([.medium, .large])
            }
    }
}

He añadido un VStack para añadir un Text con el valor del texto capturado. Y en el VStack he usaod el modificador presentationDragIndicator para mostrar esta línea horizontal que aparece en el sheet, y también he especificado los 2 tamaños que quiero que tenga mi sheet, medium y large, large es para que ocupe toda la pantalla.

Si compilamos vemos que funciona perfectamente, aparece la cámara y al pulsar en un fragmento de texto, se muestra el sheet con el texto capturado.

Pero estaría muy guapo poder reproducir el texto, de esta manera no tenemos que leerlo.

AVSpeechSynthesizer en SwiftUI

Vamos a usar una clase que nos va a permitir reproducir el texto guardado en la variable text, esta clase también la vimos en el canal y se llama AVSpeechSynthesizer. Así que lo primero de todo, volvemos a nuestra class ScanProvider y creamos una propiedad:

let synthesizer = AVSpeechSynthesizer()

Y justo a continuación, importo AVFoundation sino tendremos un error al compilar. Ahora nos vamos a final de nuestra class y creamos un método llamado speak(), este método nos va a servir para crear la implementación y así el texto se reproduzca:

func speak() {
    let textCopy = text
    let utterance = AVSpeechUtterance(string: textCopy)
    utterance.voice = AVSpeechSynthesisVoice(language: "es-ES")
    
    synthesizer.pauseSpeaking(at: .word)
    synthesizer.speak(utterance)
}

Con estas líneas de código ya tenemos todo lo necesario. El último paso es volver a ContentView y llamar a este método para que se ejecute.

Dentro del sheet, y justo debajo del VStack, añadimos un Button que lanzará la acción de reproducir el texto capturado por la cámara:

HStack {
    Spacer()
    Button {
        scanProvider.speak()
    } label: {
        Label("Play", systemImage: "play.fill")
    }
    .padding(.top, 20)
    .padding(.trailing, 20)

}

Si ahora compilamos, y capturamos un texto, vemos que aparece un button que pone play. Lo pulso, y efectivamente se reproduce el texto 🙌

Conclusión

Hoy hemos aprendido a crear una app en SwiftUI que usa el framework VisionKit para poder obtener el texto que está capturando la cámara, y más tarde hemos usado la clase AVSpeechSynthesizer para poder reproducir ese texto.