Zugriff auf die Foto-Library in SwiftUI via PHPickerViewController – Teil 2

Auf Fotoauswahl reagieren

Im ersten Teil dieser Artikelreihe befassten wir uns mit der grundlegenden Implementierung eines PHPickerViewController in SwiftUI. Diese bisherige Umsetzung erweitern wir nun um eine funktionierende Auswahlmöglichkeit eines Fotos, welches wir im Anschluss in unserer Beispiel-App präsentieren.

Analog zum Einsatz des UIImagePickerController zum Zugriff auf die iPhone-Kamera nutzt auch PHPickerViewController für die Steuerung der Fotoauswahl einen Delegate. Grundlage dieses Delegates ist das PHPickerViewControllerDelegate-Protokoll. Um die Konfiguration unserer SwiftUI-View also fortsetzen zu können, benötigen wir einen Coordinator, der konform zu jenem Protokoll ist.

PHPickerViewControllerDelegate besitzt lediglich eine einzige Anforderung in Form der Methode picker(_:didFinishPicking:). Das nachfolgende Listing zeigt, wie wir die bestehende ImagePickerView um einen PHPickerViewControllerDelegate-konformen Coordinator erweitern. Die dafür notwendigen Schritte sind:

  • 01: Die Umsetzung des Coordinators erfolgt als Nested Class innerhalb von ImagePickerView. Die Klasse adaptiert das PHPickerViewControllerDelegate-Protokoll und implementiert die notwendige Methode picker(_:didFinishPicking:) (noch ohne Logik). Zusätzlich erhält der Coordinator bei der Initialisierung einen Verweis auf die zugehörige SwiftUI-View-Instanz.
  • 02: Über die makeCoordinator()-Methode bindet man den eben definierten Coordinator in der SwiftUI-View ein.
  • 03: In der makeUIViewController(context:)-Methode weisen wir der von uns erzeugten PHPickerViewController-Instanz unseren Coordinator als Delegate zu. Die Zuweisung erfolgt über die delegate-Property des PHPickerViewController, die Coordinator-Instanz lesen wir über den context-Parameter aus.
import PhotosUI
import SwiftUI

struct ImagePickerView: UIViewControllerRepresentable {
    // #02: Coordinator in SwiftUI-View erstellen.
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func makeUIViewController(context: Context) -> PHPickerViewController {
        var imagePickerConfiguration = PHPickerConfiguration()
        imagePickerConfiguration.filter = .images
        imagePickerConfiguration.preferredAssetRepresentationMode = .current
        imagePickerConfiguration.selectionLimit = 1
        let imagePickerViewController = PHPickerViewController(configuration: imagePickerConfiguration)
        // #03: Coordinator als Delegate zuweisen.
        imagePickerViewController.delegate = context.coordinator
        return imagePickerViewController
    }
    
    func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}
    
    // #01: Coordinator implementieren.
    class Coordinator: NSObject, PHPickerViewControllerDelegate {
        var parent: ImagePickerView
        
        init(_ imagePickerView: ImagePickerView) {
            parent = imagePickerView
        }
        
        func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
            // TODO: Implement.
        }
    }
}

Im nächsten Schritt folgt nun die Implementierung der Delegate-Methode picker(_:didFinishPicking:). Sie wird von PHPickerViewController aufgerufen, sobald der Nutzer ein Foto aus der Library auswählt oder den Abbrechen-Button betätigt. Auf die gewählten Fotos kann man über den results-Parameter zugreifen, bei dem es sich um ein Array von PHPickerResult-Instanzen handelt.

PHPickerResult ermöglicht über die itemProvider-Property Zugriff auf das enthaltene Foto. Da diese Property einen generischen NSItemProvider zurück liefert, ist es notwendig, das enthaltene Bild durch Aufruf der loadDataRepresentation(forTypeIdentifier:)-Methode als Daten-Objekt auszulesen (siehe Punkt 03 im nachfolgenden Listing). Für Bilder kommt der Type Identifier public.image zum Einsatz.

Um das Sheet nach erfolgreicher Bildauswahl zu schließen und das gewählte Bild weiter zu verarbeiten, ergänzen wir ImagePickerView um zwei neue Eigenschaften:

  • 01: Das Binding showsImagePickerView dient dazu, die Sichtbarkeit des Foto-Library-Sheets zu steuern. Durch Setzen auf false können wir so das Sheet programmatisch wieder ausblenden (beispielsweise nach der Auswahl eines Fotos).
  • 02: Das neue action-Closure erhält die Daten des vom Nutzer gewählten Bildes als Data?-Instanz. So kann an der Stelle, an der ImagePickerView aufgerufen wird, das gewählte Bild weiter verarbeitet werden. Das Closure wird innerhalb von picker(_:didFinishPicking:) aufgerufen, sofern erfolgreich Bilddaten aus der Nutzerauswahl bezogen werden konnten.

Um das Sheet sauber ausblenden zu können, habe ich mich zusätzlich für die Implementierung einer Hilfsmethode namens dismissImagePickerViewController(_:) entschieden. Sie setzt showsImagePickerView auf false und stellt hierbei sicher, dass der Aufruf immer auf dem Main-Thread erfolgt. Der Aufruf dieser Hilfsmethode erfolgt immer am Ende von picker(_:didFinishPicking) und greift damit sowohl bei der Fotoauswahl als auch beim Betätigen des Abbrechen-Buttons.

Apropos Fotoauswahl: In der Konfiguration von PHPickerViewController haben wir festgelegt, dass maximal ein Bild ausgewählt werden kann. Entsprechend verarbeite ich innerhalb von picker(_:didFinishPicking:) umgehend das erste zur Verfügung stehende Ergebnis.

import PhotosUI
import SwiftUI

struct ImagePickerView: UIViewControllerRepresentable {
    // #01: Deklaration eines Binding-Parameters zum Ausblenden des Sheets
    @Binding var showsImagePickerView: Bool
    
    // #02: Deklaration eines Closure-Parameters zur Übergabe der Daten des gewählten Bildes.
    var action: (Data?) -> Void
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func makeUIViewController(context: Context) -> PHPickerViewController {
        var imagePickerConfiguration = PHPickerConfiguration()
        imagePickerConfiguration.filter = .images
        imagePickerConfiguration.preferredAssetRepresentationMode = .current
        imagePickerConfiguration.selectionLimit = 1
        let imagePickerViewController = PHPickerViewController(configuration: imagePickerConfiguration)
        imagePickerViewController.delegate = context.coordinator
        return imagePickerViewController
    }
    
    func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}
    
    class Coordinator: NSObject, PHPickerViewControllerDelegate {
        var parent: ImagePickerView
        
        init(_ imagePickerView: ImagePickerView) {
            parent = imagePickerView
        }
        
        // #03: Implementierung der Delegate-Methode
        func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
            if let result = results.first {
                result.itemProvider.loadDataRepresentation(forTypeIdentifier: "public.image") { object, error in
                    self.parent.action(object)
                }
            }
            self.dismissImagePickerViewController()
        }
        
        // #04: Umsetzung einer Hilfsmethode zum Ausblenden des Sheets.
        private func dismissImagePickerViewController() {
            Task {
                await MainActor.run {
                    parent.showsImagePickerView = false
                }
            }
        }
    }
}

Damit ist es geschafft: Bilder lassen sich nun aus unserer ImagePickerView auswählen und mithilfe des action-Closures weiter verarbeiten.

Zum Abschluss folgt noch die Aktualisierung der ContentView. Diese speichert die über das action-Closure erhaltenen Bilddaten in einer neuen State-Property namens photoData. Liegen entsprechende Bilddaten vor und lassen diese sich erfolgreich in eine UIImage-Instanz umwandeln, nutzen wir diese, um das gewählte Bild als SwiftUI-Image darzustellen.

struct ContentView: View {
    @State private var photoData: Data?
    
    @State private var showImagePickerView = false
    
    var body: some View {
        VStack {
            if let photoData = self.photoData, let uiImage = UIImage(data: photoData) {
                Image(uiImage: uiImage)
                    .resizable()
                    .scaledToFit()
            }
            Spacer()
            Button("Select photo") {
                showImagePickerView = true
            }
        }
        .sheet(isPresented: $showImagePickerView) {
            ImagePickerView(showsImagePickerView: $showImagePickerView) { imageData in
                photoData = imageData
            }
        }
    }
}

Den aktualisierten Code für dieses Projekt findet ihr auf GitHub unter https://github.com/Sillivan88/TS-ImagePickerView-Example.

Euer Thomas

Weiterführende Links zum Artikel


Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert