swiftswiftuiuikit

How can I share image data between a UIViewController and a Swift UI View?


In my pained search for a way to use AVCapture for camera functionality that saves an image, I found a great YouTube tutorial that used UIKit. Since I wanted to implement this in an app that has been entirely built on SwiftUI, I simply created a HostedViewController struct in a ViewController that contained the code from the UIKit tutorial. This hosting worked (and still works) great in isolation, but my app requires that the captured image be passed to a SwiftUI view (just the default ContentView file). How should I go about this? My initial thought was to pass the image as a binding from the SwiftUI view to the ViewController, but I was struggling to implement this.

Here is my ContentView file:

import AVFoundation
import UIKit

struct ContentView: View {
    
    var body: some View {
        
        ZStack {
            
            Color.purple
            
            HostedViewController()
                .ignoresSafeArea()
            
        }
        
    }
}

#Preview {
    ContentView()
}

Here are the relevant parts of my ViewController file:

class ViewController: UIViewController {
    
    // capture session
    var session: AVCaptureSession?
    
    // photo output
    let output = AVCapturePhotoOutput()
    
    // video preview
    let previewLayer = AVCaptureVideoPreviewLayer()
    
    //The rest is a bunch of AVCapture stuff that isn't relevant to my question
    
    @objc private func didTapTakePhoto() {
        output.capturePhoto(with: AVCapturePhotoSettings(), delegate: self)
    }

}

extension ViewController: AVCapturePhotoCaptureDelegate {
    func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
        guard let data = photo.fileDataRepresentation() else {
            return
        }

        //saves the image, this is the data I want to pass to the ContentView
        let image = UIImage(data: data)
        
        //displays the image
        session?.stopRunning()
        
        let imageView = UIImageView(image: image)
        imageView.contentMode = .scaleAspectFill
        imageView.frame = view.bounds
        view.addSubview(imageView)
    }
}

struct HostedViewController: UIViewControllerRepresentable {
    
    func makeUIViewController(context: Context) -> UIViewController {
        return ViewController()
    }
    
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
    }
    
    typealias UIViewControllerType = UIViewController
}

What are the exact changes I need to make to this so I can access the image constant from the ViewController file in the ContentView file?

Many thanks in advance.


Solution

  • From my perspective, you can try this approach: creating a Binding property between ViewController and ContentView:

    struct ContentView: View {
        @State private var selectedImage: UIImage?
        
        var body: some View {
            ZStack {
                if let selectedImage {
                    //TODO: - do something with the image that captured from AVCapturePhotoCaptureDelegate
                }
    
                ...
                HostedViewController(image: $selectedImage)
            }
        }
    }
    
    struct HostedViewController: UIViewControllerRepresentable {
        @Binding var image: UIImage?
        
        func makeUIViewController(context: Context) -> ViewController {
            let vc = ViewController()
            vc.selectedImage = $image
            return vc
        }
    }
    

    The last part is setting selectedImage within photoOutput(_ didFinishProcessingPhoto:error:). By doing this way, the image will automatically sync in ContentView to trigger if let selectedImage.

    class ViewController: UIViewController, AVCapturePhotoCaptureDelegate {
        var selectedImage: Binding<UIImage?> = .constant(nil)
    
        func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
            ...
            selectedImage.wrappedValue = image
            ...
        }
    }
    

    Another approach is to create Coordinator in HostedViewController and then reassign AVCapturePhotoCaptureDelegate to its coordinator, something like:

    class Coordinator: NSObject, AVCapturePhotoCaptureDelegate {
        @Binding var selectedImage: UIImage?
        init(selectedImage: Binding<UIImage?>) {
            _selectedImage = selectedImage
        }
    
        func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
            ...
            selectedImage = image
        }
    }