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
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 thanview.layer.bounds
.What about something like this?
In order not to add the layer if it’s already present.