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