skip to Main Content

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.

2

Answers


  1. Chosen as BEST ANSWER

    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.


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

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search