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 WKWebView
, UIImagePickerController
, 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 unUIViewController
en SwiftUI.UIViewRepresentable
: Permite usar unUIView
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