Search code examples
iosswiftiphoneswiftuicamera

Camera preview with transparent area


I want to create a fullscreen camera preview where the middle of the view is a rectangle. Everything inside the rectangle should be opaque and everything outside should be semi-transparent:

enter image description here

The solid lines indicates opaque parts of the preview (the grey rectangle) and the dotted lines are the semi-transparent parts.

Currently, I'm using the standard AVCaptureVideoPreviewLayer but I don't see a way to apply any alpha to the output let alone applying an alpha to only some areas. Is this possible?


UPDATE:

Working MRE:

import SwiftUI
import AVFoundation

class CameraViewController: UIViewController {
    var captureSession: AVCaptureSession!
    var previewLayer: AVCaptureVideoPreviewLayer!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        captureSession = AVCaptureSession()
        captureSession.sessionPreset = .photo
        
        guard let backCamera = AVCaptureDevice.default(for: .video) else { return }
        
        do {
            let input = try AVCaptureDeviceInput(device: backCamera)
            if captureSession.canAddInput(input) {
                captureSession.addInput(input)
            }
        } catch {
            print("Error: Unable to initialize back camera: \(error.localizedDescription)")
        }
        
        previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
        previewLayer.videoGravity = .resizeAspectFill
        previewLayer.frame = view.layer.bounds
        
        view.layer.addSublayer(previewLayer)
        
        captureSession.startRunning()
    }
}

struct CameraView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> CameraViewController {
        return CameraViewController()
    }
    
    func updateUIViewController(_ uiViewController: CameraViewController, context: Context) {}
}


struct ContentView: View {
    var body: some View {
        ZStack {
            CameraView()
                .edgesIgnoringSafeArea(.all)
        }
    }
}

I found that AVCaptureVideoPreviewLayer has a opacity property that can be set to make the preview transparent, but still don't see a way to make an rectangle that's opaque.


Solution

  • I've tried the Aespa library that you mentioned above. So I will use this project to give you the solution. The entire screen is an AVCaptureVideoPreviewLayer, to achieve your desired UI, you need to:

    1. Wrap the AVCaptureVideoPreviewLayer into a view instead of a controller's view as they did in the library.

    Nothing much on this step, open the Preview file, then declare a custom controller, let's say PreviewController.

    final class PreviewController: UIViewController {
        let previewContainer = UIView()
    
        override func viewDidLoad() {
            super.viewDidLoad()
            view.addSubview(previewContainer)
            previewContainer.translatesAutoresizingMaskIntoConstraints = false
            NSLayoutConstraint.activate([
                transparentView.topAnchor.constraint(equalTo: view.topAnchor),
                transparentView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
                transparentView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
                transparentView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            ])
        }
    }
    
    struct Preview: UIViewControllerRepresentable {
        func updateUIViewController(_ uiViewController: PreviewController, context: Context) {
            ...
            if previewLayer.superlayer == nil {
                uiViewController.previewContainer.layer.addSublayer(previewLayer)
            }
        }
    }
    

    1. Add a cameraOverlayView above the preview layer on step 1. Imagine the red view at center is your opaque view, another are semi-transparent parts.

    enter image description here

    final class CameraOverlayView: UIView {
        private var container: UIStackView = {
            let v = UIStackView()
            v.axis = .vertical
            return v
        }()
    
        private var top = UIView()
        private let bottom = UIView()
    
        private var centerContent: UIStackView = {
            let v = UIStackView()
            v.axis = .horizontal
            v.alignment = .fill
            return v
        }()
    
        private let focusView = UIView()
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            setup()
        }
    
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            setup()
        }
    
        private func setup() {
            addSubview(container)
            container.translatesAutoresizingMaskIntoConstraints = false
            NSLayoutConstraint.activate([
                container.topAnchor.constraint(equalTo: self.topAnchor),
                container.leadingAnchor.constraint(equalTo: self.leadingAnchor),
                container.trailingAnchor.constraint(equalTo: self.trailingAnchor),
                container.bottomAnchor.constraint(equalTo: self.bottomAnchor),
            ])
    
            top.backgroundColor = .black.withAlphaComponent(0.8)
            top.heightAnchor.constraint(equalToConstant: 200).isActive = true
    
            bottom.backgroundColor = .black.withAlphaComponent(0.8)
    
            container.addArrangedSubview(top)
            container.addArrangedSubview(centerContent)
            container.addArrangedSubview(bottom)
    
            let padding = (UIScreen.main.bounds.width - 300) / 2
            let leading = UIView()
            leading.backgroundColor = .black.withAlphaComponent(0.8)
            leading.widthAnchor.constraint(equalToConstant: padding).isActive = true
            let trailing = UIView()
            trailing.backgroundColor = .black.withAlphaComponent(0.8)
            trailing.widthAnchor.constraint(equalToConstant: padding).isActive = true
    
            centerContent.addArrangedSubview(leading)
            centerContent.addArrangedSubview(focusView)
            centerContent.addArrangedSubview(trailing)
            focusView.widthAnchor.constraint(equalToConstant: 300).isActive = true
            focusView.heightAnchor.constraint(equalToConstant: 400).isActive = true
        }
    }
    

    I've fixed the constants. You can change them later or inject them via parameter / function.

    Now add the view to PreviewController and make sure it's above previewContainer.

    final class PreviewController: UIViewController {
        private var cameraOverlayView: CameraOverlayView!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            ...
            cameraOverlayView = CameraOverlayView(frame: .zero)
            view.addSubview(cameraOverlayView)
            cameraOverlayView.translatesAutoresizingMaskIntoConstraints = false
            NSLayoutConstraint.activate([
                cameraOverlayView.topAnchor.constraint(equalTo: view.topAnchor),
                cameraOverlayView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
                cameraOverlayView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
                cameraOverlayView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            ])
        }
    }
    

    Output:

    enter image description here