0

I'd like to wrap a CaptureSession in a SwiftUI view. The current way I'm doing this is:

struct VideoPreviewView: UIViewRepresentable {
    @EnvironmentObject var cameraManager: CameraManager

    func makeUIView(context: Context) -> UIView {
        let view = UIView()
        return view
    }

    func updateUIView(_ uiView: UIView, context: Context) {
        if cameraManager.isSessionInitialized {
            print("here")
            let previewLayer = AVCaptureVideoPreviewLayer(session: cameraManager.captureSession)
            previewLayer.frame = uiView.layer.bounds
            previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
            uiView.layer.addSublayer(previewLayer)
        }
    }
}

Here my CameraManager is initializing the session and sets the isSessionInitialized when it's ready. The issue is the print("here") is happening multiple times. I'd only like it to run once. I can't figure out how to only run that code once. I initially thought I could make a property private var hasRun, but the struct is immutable so it was useless to update. Then I thought I could do it with @State, but you're not allowed to update state during a view update, so that was also useless.

Ideally, I'd be able to put all the view/layer stuff in makeUIView and conditionally create this view, like so:

struct VideoPreviewView: UIViewRepresentable {
    @EnvironmentObject var cameraManager: CameraManager

    func makeUIView(context: Context) -> UIView {
        let view = UIView()
        let previewLayer = AVCaptureVideoPreviewLayer(session: cameraManager.captureSession)
        previewLayer.frame = view.layer.bounds
        previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
        view.layer.addSublayer(previewLayer)
        return view
    }

    func updateUIView(_ uiView: UIView, context: Context) {}
}

And have a loading screen in my ContentView where I initialize it:

if cameraManager.isSessionInitialized {
    VideoPreviewView().edgesIgnoringSafeArea(.all)
} else {
    Text("loading cam")
}

(rather than just VideoPreviewView().edgesIgnoringSafeArea(.all) in the ContentView).

But this doesn't work. It just shows a blank screen where the video preview should be. I made a gist for the modified code with the whole thing in case it would be easiest to test it out in XCode yourself.

So, can anyone help me figure out how to get the print("here") to only run once?

EDIT (irrelevant since we found the answer): Here are two potential fixes, both of which don't seem to quite work.

First one:

// ContentView
var body: some View {
    ZStack {
        VideoPreviewView(cameraManager: cameraManager).edgesIgnoringSafeArea(.all)
        RecordButton()
    }
    .environmentObject(cameraManager)
}

and

struct VideoPreviewView: UIViewRepresentable {
    let cameraManager: CameraManager
    
    init(cameraManager: CameraManager) {
        self.cameraManager = cameraManager
    }

    func makeUIView(context: Context) -> UIView {
        let view = UIView()
        return view
    }

    func updateUIView(_ uiView: UIView, context: Context) {
        if cameraManager.isSessionInitialized {
            print("now")
            let previewLayer = AVCaptureVideoPreviewLayer(session: cameraManager.captureSession)
            previewLayer.frame = uiView.layer.bounds
            previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
            uiView.layer.addSublayer(previewLayer)
        }
    }
}

This doesn't seem to work (the view is still blank) and the "now" print statement doesn't fire. The second interpretation of the comment is:

// ContentView
var body: some View {
    ZStack {
        if cameraManager.isSessionInitialized {
            VideoPreviewView(cameraManager: cameraManager).edgesIgnoringSafeArea(.all)
        } else {
            Text("nope not in this house")
        }
        RecordButton()
    }
    .environmentObject(cameraManager)
}

and

struct VideoPreviewView: UIViewRepresentable {
    let cameraManager: CameraManager
    
    init(cameraManager: CameraManager) {
        self.cameraManager = cameraManager
    }

    func makeUIView(context: Context) -> UIView {
        let view = UIView()
        if cameraManager.isSessionInitialized {
            print("now")
            let previewLayer = AVCaptureVideoPreviewLayer(session: cameraManager.captureSession)
            previewLayer.frame = view.layer.bounds
            previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
            view.layer.addSublayer(previewLayer)
        }
        return view
    }

    func updateUIView(_ uiView: UIView, context: Context) {
    }
}

The "now" print statement fires, but it's still a blank screen.

10
  • 1
    Check the frame size of the view, maybe it's zero. Commented Sep 12, 2023 at 14:38
  • Remove the environment object wrapper just use a “let” Commented Sep 12, 2023 at 14:48
  • @loremipsum I can't remove the environment object wrapper since I'm initializing it higher up in the view hierarchy. I need to do that since the recording button also needs to access the CameraManager. See my gist. Commented Sep 12, 2023 at 15:17
  • Pass it as a “let” from the parent from the View that has the conditional. You are just removing the wrapper that is what triggers update Commented Sep 12, 2023 at 15:19
  • Ah, that makes sense. I'll try that. (I am new to SwiftUI, so apologies) @loremipsum Commented Sep 12, 2023 at 15:21

2 Answers 2

0

What about something like this?

struct VideoPreviewView: UIViewRepresentable {
    @EnvironmentObject var cameraManager: CameraManager

    func makeUIView(context: Context) -> UIView {
        let view = UIView()
        return view
    }
    
    func updateUIView(_ uiView: UIView, context: Context) {
        if cameraManager.isSessionInitialized && shouldInit(in: uiView) {
            print("here")
            let previewLayer = AVCaptureVideoPreviewLayer(session: cameraManager.captureSession)
            previewLayer.frame = uiView.layer.bounds
            previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
            uiView.layer.addSublayer(previewLayer)
        }
    }
    
    private func shouldInit(in uiView: UIView) -> Bool {
        guard let sublayers = uiView.layer.sublayers, !sublayers.isEmpty else {
            return true
        }
        return !sublayers.contains { layer in
            layer is AVCaptureVideoPreviewLayer
        }
    }
}

In order not to add the layer if it's already present.

Sign up to request clarification or add additional context in comments.

Comments

0

The issue as @vadian pointed out is the bounds were zero since I the view I was taking the bounds from was just initialized. The correct code is a tiny change from my desired modifications. Use UIScreen.main.bounds rather than view.layer.bounds.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.