Aprende a crear apps desde tu iPad con Swift Playgrounds 4
Aprende a crear apps desde tu iPad con Swift Playgrounds 4

¿Cómo crear aplicaciones en tu iPad con Swift Playgrounds 4?

Con Swift Playgrounds 4 ya podemos crear aplicaciones desde nuestro iPad. Crea aplicaciones desde cualquier lugar solo con tu iPad. Hoy aprendemos a cómo usar UIImagePickerController para crear una app que saque fotos y aplique filtros y todo esto lo vamos hacer desde nuestro iPad

SwiftBeta

Tabla de contenido

Aprende a capturar fotos creando una app desde el iPad

Hoy en SwiftBeta vamos a aprender a crear otra app en nuestro iPad. Lo único que necesitamos es tener descargado Swift Playgrounds 4.

En nuestra app mostraremos la cámara de nuestro dispositivo, haremos una foto y podremos aplicar un filtro. Lo primero de todo que vamos hacer es crear nuestra vista ContentView. Va a ser muy sencilla:

import SwiftUI

struct ContentView: View {
    @State var selectedImage: Image = Image("placeholder")
    @State var showCamera: Bool = false
    
    var body: some View {
        VStack {
            selectedImage
                .resizable()
                .scaledToFit()
                .frame(width: 300, height: 300)
            Button("Show Camera") {
                // TODO:
            }
        }
    }
}
Creamos nuestro ContentView

La vista de placeholder puedes usar la que quieras, te dejo la que he usado por si quieres tener el mismo resultado:

Imagen usada como placeholder hasta que el user realiza la foto con su cámara
Imagen usada como placeholder hasta que el user realiza la foto con su cámara

Conformamos el protocolo UIViewControllerRepresentable

Lo siguiente que vamos hacer es usar una clase para poder acceder a la cámara de nuestro dispositivo, esta clase llamada UIImagePickerController pertenece a UIKit. Y para poderla usar, en SwiftUI debemos crear como una especie de Wrapper (de envoltorio, como un traductor de UIKit a SwiftUI). Para hacerlo creamos una struct llamada CameraView y conformamos el protocolo UIViewControllerRepresentable, al conformar los métodos de este protocolo podremos aprovechar y usar UIImagePickerController en SwiftUI. Vamos a ello:

struct CameraView: UIViewControllerRepresentable {
    @Binding var selectedImage: Image
    @Environment(\.dismiss) var dismiss
    
    func makeUIViewController(context: Context) -> some UIViewController {
        let imagePickerController = UIImagePickerController()
        imagePickerController.delegate = context.coordinator
        return imagePickerController
    }
    
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
        // Empty
    }
}
Creamos el traductor de nuestra clase UIImagePickerController de UIKit para poder usar en SwiftUI
  • El método makeUIViewController se encarga de crear el controlador (y la vista) UIImagePickerController. Fíjate que asignamos a la propiedad delegate un context.coordinator. Ahora entraremos en detalle con esta parte.
  • El método updateUIViewController se encarga de actualizar la vista, en nuestro caso dejaremos este método vacío ya que no haremos ningún cambio después de instanciarla en makeUIViewController

Si te fijas, tenemos un error en nuestro código.

Cannot assign value of type 'Void' to type '(UIImagePickerControllerDelegate & UINavigationControllerDelegate)?'
Error al asignar nuestro delegado

Creamos nuestro Coordinator

Vamos a arreglarlo creando una nueva clase llamada Coordinator, esta clase va a conformar estos dos protocolos que se muestran en el error anterior, UIImagePickerControllerDelegate y UINavigationControllerDelegate. A parte, la clase Coordinator va a ser otro traductor, va a escuchar cambios que pasen en los delegados de la clase de UIKit UIImagePickerController y cambiará el estado de nuestra vista en SwiftUI. Es decir, cuando un user haga una foto, el método del delegado imagePickerController(_ picker:, didFinishPickingMediaWithInfo info:) se llamará automáticamente y asignará la nueva imagen a CameraView, y este cambio, al ser selectedImage un Binding de ContentView hará que se muestre también en nuestra Image de ContentView.

final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
 
    var cameraView: CameraView
 
    init(cameraView: CameraView) {
        self.cameraView = cameraView
    }
 
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
 
        if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
            cameraView.selectedImage = Image(uiImage: image)
        }
        cameraView.dismiss()
    }
}
Clase Coordinator para poder actualizar el estado de nuestra vista CameraView en SwiftUI
La función del Coordinator es cambiar el estado de nuestra vista en SwiftUI de cambios que llegan de métodos como en este caso imagePickerController(_ picker:, didFinishPickingMediaWithInfo info:)

Una vez hemos creado el Coordinator vamos a añadir otro método en nuestra vista CameraView, vamos a conectar CameraView con nuestro Coordinator. De esta manera la vista se podrá comunicar con los métodos que se triggerean en el delagado de imagePickerController como acabamos de explicar, por ejemplo cuando un user ha realizado una foto:

struct CameraView: UIViewControllerRepresentable {
    @Binding var selectedImage: Image
    @Environment(\.dismiss) var dismiss
    
    func makeUIViewController(context: Context) -> some UIViewController {
        let imagePickerController = UIImagePickerController()
        imagePickerController.delegate = context.coordinator
        imagePickerController.sourceType = .camera
        return imagePickerController
    }
    
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
        // Empty
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(cameraView: self)
    }
}
Añadimos el método makeCoordinator

Ahora nos vamos a nuestra vista ContentView y vamos a añadir lógica para que cuando se pulse el Button aparezca la vista CameraView que hemos creado. Y así el user pueda hacer una foto.

import SwiftUI

struct ContentView: View {
    @State var selectedImage: Image = Image("placeholder")
    @State var showCamera: Bool = false
    
    var body: some View {
        VStack {
            selectedImage
                .resizable()
                .scaledToFit()
                .frame(width: 300, height: 300)
            Button("Show Camera") {
                showCamera.toggle()
            }
            .fullScreenCover(isPresented: $showCamera) {
                CameraView(selectedImage: $selectedImage)
            }
        }
    }
}
Ahora ya podemos usar CameraView en ContentView

Compilamos nuestra app en el iPad

Si compilamos ahora, y pulsamos el Button de "Show Camera" nos aparece el siguiente alert. Si le damos a Ok, vemos como nuestra app muestra la cámara y por lo tanto podemos tomar una foto. Al tomar una foto, la imagen que teníamos en ContentView (la del placeholder) es sustituida por la nueva que acabamos de tomar.

Alert del sistema para que un user acepte el acceso a la cámara
Alert del sistema para que un user acepte el acceso a la cámara
Un dato curioso, si intentaramos usar el mismo código en Xcode nos saldría un error, nos crashearía nuestra aplicación con el siguiente mensaje:

This app has crashed because it attempted to access privacy-sensitive data without a usage description.  The app's Info.plist must contain an NSCameraUsageDescription key with a string value explaining to the user how the app uses this data.

¿Por qué ocurre? debemos especificar en nuestro Info.plist la key Privacy - Camera Usage Description y darle un mensaje al usuario de por qué queremos tener acceso a la cámara.

Ahora vamos con la segunda parte, una vez tenemos una imagen hecha con nuestra cámara vamos a probar dos filtros.


Añadimos un filtro a nuestra imagen

Una vez tenemos se muestra la imagen que acabamos de crear, vamos a crear un button nuevo para poder aplicar un filtro. En este caso usaremos un filtro sepia.

Creamos una struct llamada Filter:

struct Filter {
    @Binding var selectedImage: Image
    @Binding var selectedUIImage: UIImage
    
    func apply() {
        guard let cgImage = selectedUIImage.cgImage else {
            return
        }
        
        let ciImage = CIImage(cgImage: cgImage)
        let sepiaFilter = CIFilter(name:"CISepiaTone")
        sepiaFilter?.setValue(ciImage, forKey: kCIInputImageKey)
                
        guard let imageWithSepiaFilter = sepiaFilter?.outputImage,
        let cgImageWithSepiaFilter = mapCIImageToCGImage(imageWithSepiaFilter) else {
            return
        }
        
        let newImage = UIImage(cgImage: cgImageWithSepiaFilter,
                               scale: 1.0,
                               orientation: .right)
        selectedImage = Image(uiImage: newImage)
    }
    
    private func mapCIImageToCGImage(_ ciImage: CIImage) -> CGImage? {
        let context = CIContext()
        if let cgImage = context.createCGImage(ciImage,
                                               from: ciImage.extent) {
            return cgImage
        }
        return nil
    }
}
Creamos una struct Filter para poder aplicar el filtro Sepia a la imagen capturada con la cámara

Y en CameraView creamos una nueva propiedad llamada:

@Binding var selectedUIImage: UIImage
Añadimos una nueva propiedad de tipo UIImage

Cuando un user haga una foto guardaremos la UIImage en esta propiedad, es decir, nuestro Coordinator quedaría de la siguiente manera:

final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
 
    var cameraView: CameraView
 
    init(cameraView: CameraView) {
        self.cameraView = cameraView
    }
 
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
 
        if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
            cameraView.selectedImage = Image(uiImage: image)
            cameraView.selectedUIImage = image
        }
        cameraView.dismiss()
    }
}
Guardamos la imagen capturada en la propiedad selectedUIImage

Volvemos a nuestra ContentView y aquí vamos a añadir dos propiedades nuevas y un Button nuevo que llame al método de la struct Filter que acabamos de crear. Al llamar al método apply() se aplicará el filtro que hayamos hecho.

import SwiftUI
import CoreImage

struct ContentView: View {
    @State var selectedImage: Image = Image("placeholder")
    @State var selectedUIImage: UIImage = UIImage(named: "placeholder")!
    @State var showCamera: Bool = false
    @State var filter: Filter?
    
    var body: some View {
        VStack {
            selectedImage
                .resizable()
                .scaledToFit()
                .frame(width: 300, height: 300)
            Button("Show Camera") {
                showCamera.toggle()
            }
            Button("Apply Filter") {
                filter = Filter(selectedImage: $selectedImage,
                                selectedUIImage: $selectedUIImage)
                filter?.apply()
            }
            .fullScreenCover(isPresented: $showCamera) {
                CameraView(selectedImage: $selectedImage,
                           selectedUIImage: $selectedUIImage)
            }
        }
    }
}
Añadimos nuevo Button de aplicar filtro y arreglamos errores de compilación

Vamos a probarlo 🚀

0:00
/
Video demostrando el resultado final de nuestra app

Conclusión

Hoy hemos aprendido a cómo crear una aplicación para poder acceder a la cámara de nuestro iPad y una vez tomada una foto aplicar un filtro.

Si quieres seguir aprendiendo sobre SwiftUI, Swift, Xcode, o cualquier tema relacionado con el ecosistema Apple



¡Únete al Discord de SwiftBeta!