Introdução
Quando desenvolvemos uma aplicação em SwiftUI há casos em que vemos a necessidade de utilizar um componente em UIKit por vários motivos: não há um equivalente em SwiftUI, o equivalente possui certas limitações, é um componente personalizado que foi anteriormente implementado em UIKit e queremos reaproveitar, ou algum outro motivo. Felizmente, é possível fazer esse reuso de UIKit para SwiftUI utilizando os protocolos UIViewRepresentable e UIViewControllerRepresentable.
UIViewRepresentable e UIViewControllerRepresentable
Esses protocolos são utilizados para implementar Views que representam uma UIView ou UIViewController e permitem integrar componentes UIKit em uma aplicação em SwiftUI. Esses protocolos possuem os seguinte métodos a serem implementados:
makeUIView/makeUIViewController (Obrigatório)
Esses métodos são utilizado para criar a UIView/UIViewController e realizar configurações iniciais com base nas propriedades da View ou informações disponíveis no context.
updateUIView/updateUIViewController (Obrigatório)
Esses métodos são utilizados para atualizar a UIView/UIViewController e são chamados sempre que há mudanças de estado na aplicação que possuam o componente.
dismantleUIView/dismantleUIViewController (Opcional)
Esses métodos são utilizados para realizar algum ação no momento em que a UIView ou UIViewController vai ser removida.
makeCoordinator (Opcional)
Esse método é utilizado para criar um Coordinator caso seja necessário observar mudanças que acontecem no componente UIKit, implementando métodos de Delegate, por exemplo, e atualizar propriedades da View com base nessas mudanças.
protocol UIViewRepresentable: View where Self.Body == Never {
associatedtype UIViewType: UIView
func makeUIView(context: Self.Context) -> Self.UIViewType
func updateUIView(_ uiView: Self.UIViewType, context: Self.Context)
static func dismantleUIView(_ uiView: Self.UIViewType, coordinator: Self.Coordinator)
func makeCoordinator() -> Self.Coordinator
}
protocol UIViewControllerRepresentable: View where Self.Body == Never {
associatedtype UIViewControllerType: UIViewController
func makeUIViewController(context: Self.Context) -> Self.UIViewControllerType
func updateUIViewController(_ uiViewController: Self.UIViewControllerType, context: Self.Context)
static func dismantleUIViewController(_ uiViewController: Self.UIViewControllerType, coordinator: Self.Coordinator)
func makeCoordinator() -> Self.Coordinator
}
Os tipos associados UIViewType e UIViewControllerType são referentes a UIView ou UIViewController que vai ser utilizada.
ImagePicker
Vamos implementar um componente SwiftUI capaz de selecionar uma imagem da câmera ou da biblioteca utilizando um UIImagePickerController. O primeiro passo é definir um tipo ImagePicker que implementa o protocolo UIViewControllerRepresentable e o tipo associado UIViewControllerType é UIImagePickerController.
struct ImagePicker: UIViewControllerRepresentable {
typealias UIViewControllerType = UIImagePickerController
func makeUIViewController(context: Context) -> UIImagePickerController {
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {
}
}
Esse componente vai possuir uma propriedade image, para armazenar a imagem selecionada, e uma propriedade sourceType, para definir de onde a imagem vai ser selecionada. A propriedade image é anotada com @Binding para permitir a ligação com uma propriedade @State externa ao componente.
struct ImagePicker: UIViewControllerRepresentable {
@Binding var image: UIImage?
let sourceType: UIImagePickerController.SourceType
...
}
Na implementação do método makeUIViewController uma instância UIImagePickerController é criada e configurada com o sourceType inicial. É nesse método também que posteriormente vamos definir o delegate, que no momento será nil.
func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.sourceType = sourceType
picker.delegate = nil
return picker
}
Na implementação do método updateUIViewController fazemos a atualização do sourceType. Se o sourceType do ImagePicker estiver associado com uma propriedade @State esse método vai ser chamado sempre que houve uma alteração na propriedade.
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {
uiViewController.sourceType = sourceType
}
Precisamos definir um tipo Coordinator interno ao ImagePicker que atuará como o delegate do UIImagePickerController, atualizando a propriedade image quando uma imagem for selecionada.
struct ImagePicker: UIViewControllerRepresentable {
...
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
if let originalImage = info[.originalImage] as? UIImage {
parent.image = originalImage
}
}
}
}
Agora implementamos o método makeCoordinator para criar uma instância do Coordinator e configuramos o delegate corretamente no método makeUIViewController.
struct ImagePicker: UIViewControllerRepresentable {
...
func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
picker.sourceType = sourceType
return picker
}
...
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
...
}
Um ImagePicker vai ser usualmente exibido em uma tela sobreposta utilizando os modificadores popover, sheet ou fullScreenCover. Precisamos de uma maneira de fechar a tela e para isso acessamos a propriedade presentationMode do Environment e chamamos o método dismiss quando uma imagem for selecionada. Segue abaixo a implementação completa.
struct ImagePicker: UIViewControllerRepresentable {
@Environment(\.presentationMode) var presentationMode
@Binding var image: UIImage?
let sourceType: UIImagePickerController.SourceType
func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
picker.sourceType = sourceType
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {
uiViewController.sourceType = sourceType
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
if let originalImage = info[.originalImage] as? UIImage {
parent.image = originalImage
}
parent.presentationMode.wrappedValue.dismiss()
}
}
}
O exemplo abaixo utiliza um ImagePicker para selecionar uma imagem e exibi-la na tela.
struct ContentView: View {
@State private var image: UIImage? = nil
@State private var sourceType: UIImagePickerController.SourceType? = nil
var body: some View {
NavigationView {
Image(uiImage: image ?? UIImage())
.resizable()
.aspectRatio(contentMode: .fit)
.navigationBarTitleDisplayMode(.inline)
.fullScreenCover(item: $sourceType) {
ImagePicker(image: $image, sourceType: $0)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Menu(systemImage: "camera") {
Button("Camera") {
sourceType = .camera
}
.disabled(isRunningOnSimulator)
Button("Library") {
sourceType = .photoLibrary
}
}
}
}
}
}
private var isRunningOnSimulator: Bool {
#if targetEnvironment(simulator)
return true
#else
return false
#endif
}
}
extension Menu where Label == Image {
init(systemImage: String, @ViewBuilder content: () -> Content) {
self.init(content: content, label: { Image(systemName: systemImage) })
}
}
extension UIImagePickerController.SourceType: Identifiable {
public var id: Int { rawValue }
}
Para o exemplo funcionar é necessário adicionar as chaves NSCameraUsageDescription e NSPhotoLibraryUsageDescription na Info.plist.
Conclusão
Um dos pontos fortes do SwiftUI é a possibilidade de integrar qualquer componente UIKit na nossa aplicação com os protocolos UIViewRepresentable e UIViewControllerRepresentable e utilizar esses componentes da mesma maneira que utilizamos qualquer outro em SwiftUI. Podemos assim fazer o melhor uso possível dos dois frameworks e realizar uma transição suave de UIKit para SwiftUI.
Oldest comments (0)