I'm using this camera library in which the author wraps a AVCaptureVideoPreviewLayer
inside a UIViewControllerRepresentable
(link) to use it as a camera preview. Inside updateUIViewController
, the author queues the frame
update with DispatchQueue.main.async
:
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
previewLayer.videoGravity = gravity
if previewLayer.superlayer == nil {
uiViewController.view.layer.addSublayer(previewLayer)
}
// print(uiViewController.view.bounds) -- point A
DispatchQueue.main.async {
// print(uiViewController.view.bounds) -- point B
self.previewLayer.frame = uiViewController.view.bounds
}
}
There can be discrepancies between point A
and point B
, which makes sense since using DispatchQueue.main.async
won't guarantee ordering, but I'm not entirely convinced this approach is correct. I want to be able to set the frame
of this view using something like Preview().frame(width: 350, height: 500)
. However, the lack of ordering here means that depending on other parts of the app/views, the view.frame
update happens at an unknown schedule. For example, the following:
import SwiftUI
import Aespa
@Observable
class ViewModel {
@ObservationIgnored var aespaSession = Aespa.session(with: AespaOption(albumName: nil))
@ObservationIgnored var preview: InteractivePreview {
aespaSession.interactivePreview(
gravity: .resizeAspectFill,
option: InteractivePreviewOption()
)
}
var show = false
}
struct ContentView: View {
@State var viewModel = ViewModel()
var body: some View {
ZStack {
VStack {
Spacer()
Button("Show Camera Preview") {
viewModel.show.toggle()
}
}
.ignoresSafeArea()
.zIndex(0)
if viewModel.show {
VStack {
viewModel.preview
.frame(width: 350, height: 500)
Spacer()
}
.zIndex(1)
}
}
}
}
Prints:
Point A: (0.0, 0.0, 393.0, 852.0)
Point B: (0.0, 0.0, 350.0, 500.0)
But if you change the code to something else, perhaps a different background at .zIndex(0)
(I won't show the exact code here since my app is non-trivial), it might print:
Point A: (0.0, 0.0, 393.0, 852.0)
Point B: (0.0, 0.0, 393.0, 852.0)
So my question is. What is the correct way to set view.frame
? updateUIViewController
is obviously being called in some deterministic order but not before the bound
have "settled". I don't see a magical "updateViewOneLastTime" method?
It sounds like you are looking for viewDidLayoutSubviews
, which is called whenever the view controller's view.bounds
changes. Create a UIViewController
subclass and override it.
class PreviewViewController: UIViewController {
var previewLayer: AVCaptureVideoPreviewLayer?
override func viewDidLayoutSubviews() {
previewLayer?.frame = view.frame
}
}
Then change the methods in the UIViewControllerRepresentable
implementation to use this subclass. Pass the preview layer to the view controller subclass where appropriate.
func makeUIViewController(context: Context) -> PreviewViewController {
let viewController = PreviewViewController()
viewController.view.backgroundColor = .clear
uiViewController.previewLayer = previewLayer
return viewController
}
func updateUIViewController(_ uiViewController: PreviewViewController, context: Context) {
previewLayer.videoGravity = gravity
if previewLayer.superlayer == nil {
uiViewController.view.layer.addSublayer(previewLayer)
}
uiViewController.previewLayer = previewLayer
}
func dismantleUIViewController(_ uiViewController: PreviewViewController, coordinator: ()) {
previewLayer.removeFromSuperlayer()
}