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
:
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.
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:
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)
}
}
}
cameraOverlayView
above the preview layer on step 1. Imagine the red view at center is your opaque view
, another are semi-transparent
parts.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: