Kamerazugriff via SwiftUI – Teil 2

Bild aufnehmen und verarbeiten

Im ersten Teil dieser Reihe begannen wir mit der Integration eines UIImagePickerController in SwiftUI. Diese bestehende Integration erweitern wir nun um einen Coordinator, mit dessen Hilfe wir ein aufgenommenes Bild auslesen und an SwiftUI weiter reichen können.

Betrachten wir zunächst einmal die Ausgangslage aus dem ersten Teil der Artikelreihe. Hier kommt das UIViewControllerRepresentable-Protokoll zum Einsatz, um eine UIImagePickerController-Instanz in SwiftUI zu integrieren.

// Listing 1: Bisheriger Stand
import SwiftUI
import UniformTypeIdentifiers

struct TSCameraView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIImagePickerController {
        let imagePickerController = UIImagePickerController()
        imagePickerController.delegate = context.coordinator
        imagePickerController.mediaTypes = [UTType.image.identifier]
        imagePickerController.sourceType = .camera
        return imagePickerController
    }
    
    func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
}

Um ein aufgenommenes Bild auswerten zu können, braucht es zusätzlich eine Implementierung des UIImagePickerControllerDelegate-Protokolls. Einen solchen Delegate weist man einer UIImagePickerController-Instanz über deren delegate-Property zu. Hat der Nutzer ein Bild aufgenommen oder bricht er die Aufnahme ab, kann der Delegate diese Events mithilfe passender Methoden abfangen und darauf reagieren. Genau das brauchen wir, um das aufgenommene Bild auszuwerten.

Wie man einen Coordinator erzeugt und einsetzt, zeigt das folgende Listing (die Änderungen sind im Code mit zugehörigen Kommentaren versehen). Zunächst fügt man dem Representable eine neue Klasse hinzu, die als Coordinator fungiert. Ich nutze für solche Fälle immer Coordinator als Klassennamen.

Da der Coordinator in diesem Fall Events des UIImagePickerControllerDelegate-Protokolls abfangen soll, muss die Klasse entsprechend konform zu diesem Protokoll sein (Punkt 1 im Listing). Bei der Initialisierung erhält der Coordinator die Instanz von TSCameraView, die auf die Kamera zugreift. Das ermöglicht es, dem Representable Daten (beispielsweise das aufgenommene Bild) zu übergeben.

Um diese neue Coordinator-Klasse mit dem Representable vertraut zu machen, muss innerhalb des Representables die Methode makeCoordinator() implementiert werden (Punkt 2 im Listing). Die liefert in unserem Fall eine neue Instanz des eben definierten Coordinators zurück.

Zu guter Letzt muss der UIImagePickerController-Instanz noch der gewünschte Delegate zugewiesen werden. Zu diesem Zweck erweitern wir die Implementierung der makeUIViewController(context:)-Methode, in der wir jene UIImagePickerController-Instanz erzeugen. Über den context-Parameter können wir den aktiven Coordinator über die gleichnamige Eigenschaft auslesen. Und genau den weisen wir der delegate-Eigenschaft der UIImagePickerController-Instanz zu (Punkt 3 im Listing).

// Listing 2: Integration eines Coordinators
import SwiftUI
import UniformTypeIdentifiers

struct TSCameraView: UIViewControllerRepresentable {
    // 2. Coordinator in Representable erzeugen
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func makeUIViewController(context: Context) -> UIImagePickerController {
        let imagePickerController = UIImagePickerController()
        
        // 3. Coordinator als Delegate zuweisen
        imagePickerController.delegate = context.coordinator
        
        imagePickerController.mediaTypes = [UTType.image.identifier]
        imagePickerController.sourceType = .camera
        return imagePickerController
    }
    
    func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
    
    // 1. Coordinator-Klasse erstellen
    class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
        var parent: TSCameraView
        
        init(_ cameraView: TSCameraView) {
            parent = cameraView
        }
    }
}

Kümmern wir uns nun um die Implementierung des UIImagePickerControllerDelegate. Das Protokoll bringt zwei Methoden mit, die wir beide implementieren:

  • imagePickerController(_:didFinishPickingMediaWithInfo:) informiert uns über die Aufnahme eines Bildes.
  • imagePickerControllerDidCancel(_:) informiert über einen Abbruch der Fotoaufnahme durch den Nutzer.

Im Falle einer erfolgreichen Aufnahme können wir das Bild als UIImage aus dem info-Parameter auslesen. Doch wie stellen wir es dem Aufrufer unserer TSCameraView zur Verfügung?

Eine Möglichkeit besteht darin, TSCameraView um ein Binding zu ergänzen, das ein optionales UIImage erwartet. Diesem Binding weist man dann ein aufgenommenes und vom Nutzer ausgewähltes Bild zu.

Zusätzlich implementieren wir gleich noch eine weitere Property. Über das neue Binding showsCameraView können wir TSCameraView wieder ausblenden, nachdem das Foto aufgenommen oder die Aufnahme abgebrochen wurde. Das vollständige Update von TSCameraView zeigt das folgende Listing:

// Listing 3: Auslesen des aufgenommenen Bildes und Weitergabe an Closure
import SwiftUI
import UniformTypeIdentifiers

struct TSCameraView: UIViewControllerRepresentable {
    @Binding var photo: UIImage?
    
    @Binding var showsCameraView: Bool
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func makeUIViewController(context: Context) -> UIImagePickerController {
        let imagePickerController = UIImagePickerController()
        imagePickerController.delegate = context.coordinator
        imagePickerController.mediaTypes = [UTType.image.identifier]
        imagePickerController.sourceType = .camera
        return imagePickerController
    }
    
    func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
    
    class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
        var parent: TSCameraView
        
        init(_ cameraView: TSCameraView) {
            parent = cameraView
        }
        
        func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
            if let image = info[.originalImage] as? UIImage {
                parent.photo = image
            }
            parent.showsCameraView = false
        }
        
        func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
            parent.showsCameraView = false
        }
    }
}

Einsatz von TSCameraView

Mit diesen Updates testen wir einmal die neuen Funktionsmöglichkeiten von TSCameraView. Dazu kommt die folgende View zum Einsatz:

// Listing 4: Praktischer Einsatz von TSCameraView
struct ContentView: View {
    @State private var photo: UIImage?
    
    @State private var showCameraView = false
    
    var body: some View {
        VStack {
            if let photo = self.photo {
                Image(uiImage: photo)
                    .resizable()
                    .scaledToFit()
            }
            Spacer()
            Button("Show camera") {
                showCameraView = true
            }
        }
        .sheet(isPresented: $showCameraView) {
            TSCameraView(photo: $photo, showsCameraView: $showCameraView)
        }
    }
}

Mithilfe eines Status namens photo steuern wir die Sichtbarkeit einer Image-View, die das aufgenommene Bild anzeigt (sofern eines vorhanden ist). Den Wert dieser Property setzen wir mithilfe des Binding-Parameters, den wir bei Erstellung von TSCameraView übergeben. Zusätzlich erhält TSCameraView ein Binding auf den Sheet-Status, damit nach Aufnahme eines Bildes oder Abbruch erneut die zugrundeliegende ContentView zu sehen ist.

Somit ist TSCameraView bereits sehr funktional und erlaubt die Aufnahme und das Auslesen von Fotos in Form von UIImage-Instanzen. Mittels Binding geben wir diese Instanzen an SwiftUI weiter und können Sie dort auf die gewünschte Art und Weise anzeigen und verarbeiten.

Euer Thomas


Kommentare

Schreibe einen Kommentar

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