SwiftUI – Cómo usar componentes de UIKit en SwiftUI

Actualmente SwiftUI se encuentra en una primera fase de crecimiento, donde tenemos muchos componentes visuales disponibles, pero también hay otros componentes básicos que son necesarios en la mayoría de los proyectos que no vienen incluidos en la SDK (cómo WKWebViewUIImagePickerController, la posibilidad de pintar un texto HTML, etc). Para estos casos, existen dos protocolos que nos permitirán crear nuestros propios componentes nativos de SwiftUI basados en componentes de UIKit. UIViewControllerRepresentable y UIViewRepresentable:

  • UIViewControllerRepresentable: Permite usar un UIViewController en SwiftUI.
  • UIViewRepresentable: Permite usar un UIView en SwiftUI.

Ambos protocolos tienen prácticamente la misma definición, pero el primero se encarga de manejar UIViewController y el segundo UIView. Esto es necesario porque en UIKit existen esas dos clases para la creación de vistas.

Gracias a estos dos protocolos podemos pintar cualquier componente que exista en UIKit y no tenga equivalencia con SwiftUI, pudiendo realizar por completo todas las pantallas con el framework de SwiftUI. En resumen, estos protocolos son fachadas que se encargan de transformar componentes que no están presentes en SwiftUI para que puedan ser usados en SwiftUI.

Cómo se implementa UIViewRepresentable

Cuando queremos exponer un componente de tipo UIView en SwiftUI debemos usar UIViewRepresentable. Su implementación consiste en crear un struct que implemente este protocolo. En resumen, la implementación del protocolo consiste en usar los métodos que tiene disponible para devolver un UIView, que será el que se pinte en pantalla.

Por ejemplo, vamos a crear un componente para poder pintar texto HTML en la aplicación:

import SwiftUI
import UIKit

struct TextHtml: UIViewRepresentable {
    let html: String?
    
    init(_ html: String?) {
        self.html = html
    }
    
    func makeUIView(context: UIViewRepresentableContext) -> UILabel {
        let label = UILabel()
        DispatchQueue.main.async {
            if let html = html, let data = html.data(using: .utf8), let attributedString = try? NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil) {
                label.attributedText = attributedString
            }
        }
        label.numberOfLines = 0
        
        return label
    }
    
    func updateUIView(_ uiView: UILabel, context: UIViewRepresentableContext) { }
}

De esta forma creamos el componente TextHtml que podremos usar en nuestra vista de SwiftUI. Su implementación consiste implementar el método makeUIView para que devuelva un UILabel seteando el texto html en su propiedad attributedText. De esta forma podemos usarlo de la siguiente manera:

struct ContentView: View {
    var body: some View {
        Group {
            TextHtml("

HTML text
This work fine.

") } } }

Cómo se implementa UIViewControllerRepresentable

Cuando queremos exponer un componente de tipo UIViewController en SwiftUI debemos usar UIViewControllerRepresentable. Su implementación consiste en crear un struct que implemente este protocolo. En resumen, la implementación del protocolo consiste en usar los métodos que tiene disponible para devolver un UIViewController y pintar su propiedad view.

Se implementa de la misma forma que UIViewRepresentable, pero en este caso los nombres de los métodos cambian para indicar que se trabaja con un UIViewController: en el método makeUIViewController debemos devolver el UIViewController que queremos pintar.

Por ejemplo, podemos crearnos un componente para presentar al usuario el componente de selección de imágenes que nos proporciona UIImagePickerController de UIKit:

struct ImagePicker: UIViewControllerRepresentable {
    func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIImagePickerController {
        let picker = UIImagePickerController()
        return picker
    }

    func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext) { }
}

De esta forma el componente ImagePicker presentará la selección de imágenes, pero con esta implementación no tenemos forma de controlar la imagen que el usuario ha seleccionado.

¿Cómo podemos arreglarlo? Pues necesitaríamos setear la propiedad picker.delegate ya que este es el funcionamiento del componente UIImagePickerController para notificar cuando se ha seleccionado una imagen. Para estos casos hay que usar el Coordinator que está definido en UIViewControllerRepresentable.

Según su definición, el Coordinator se deberá encargar de comunicar cambios que ocurran en el UIViewController a otras partes de SwiftUI. En nuestro caso vamos a crearnos una clase Coordinator que cumpla con el protocolo UIImagePickerControllerDelegate y UINavigationControllerDelegate (necesarios para picker.delegate).

class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        //Do something
    }
}

Con esta clase podemos completar la implementación de ImagePicker e indicar cuál es el Coordinator a través del método makeCoordinator.

func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIImagePickerController {
    let picker = UIImagePickerController()
    picker.delegate = context.coordinator
    return picker
}

func makeCoordinator() -> Coordinator {
    Coordinator()
}

Ya tenemos forma de recibir la imagen que el usuario selecciona, pero aún nos queda propagarla para que ImagePicker sea notificado cuando se selecciona. Para ello vamos a declarar una propiedad de tipo Binding en ImagePicker que almacenará la imagen y vamos a pasarle al Coordinator el propio ImagePicker para que pueda setearla cuando los usuarios la seleccionen. De esta forma la implementación completa del componente quedaría de la siguiente manera:

struct ImagePicker: UIViewControllerRepresentable {
    @Environment(.presentationMode) var presentationMode
    @Binding var image: UIImage?
    
    func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIImagePickerController {
        let picker = UIImagePickerController()
        picker.delegate = context.coordinator
        return picker
    }
    
    func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext) { }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
        
        let imagePicker: ImagePicker
        
        init(_ imagePicker: ImagePicker) {
            self.imagePicker = imagePicker
        }
        
        func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
            if let image = info[.originalImage] as? UIImage {
                imagePicker.image = image
            }
            imagePicker.presentationMode.wrappedValue.dismiss()
        }
    }
}

La propiedad @Binding var image: UIImage? será la encargada de almacenar la imagen del usuario y cada vez que se cambie se notificará a la vista donde se incluya el componente ImagePicker, permitiéndonos actualizar la vista para mostrar esta imagen.

struct ContentView: View {
    @State var imageUser: UIImage?
    @State var showPicker = false
    
    var body: some View {
        VStack(spacing: 15) {
            if let imageUser = imageUser {
                Image(uiImage: imageUser)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(height: 300)
            } else {
                Text("No image selected")
            }
            Button("Select image") {
                showPicker.toggle()
            }
        }
        .sheet(isPresented: $showPicker) {
            ImagePicker(image: $imageUser)
        }
    }
}

Rafael Fernández,
iOS Tech Lider