DEV Community

loading...
Cover image for Integrando UIKit ao SwiftUI
Apple Developer Academy @IFCE

Integrando UIKit ao SwiftUI

mtsrodrigues profile image Mateus Rodrigues ・4 min read

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
}
Enter fullscreen mode Exit fullscreen mode

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) {

    }

}
Enter fullscreen mode Exit fullscreen mode

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

    ...

}
Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
            }
        }

    }

}
Enter fullscreen mode Exit fullscreen mode

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)
    }

    ...
}
Enter fullscreen mode Exit fullscreen mode

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()
        }

    }

}
Enter fullscreen mode Exit fullscreen mode

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 }
}
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (0)

pic
Editor guide