Goal
With an MTKView, replicate the gravity of the AVCaptureVideoPreviewLayer or Apple's Camera app. Video device orientation does not change. The camera feed's edges do not budge a pixel, never revealing the screen background. Other on-screen VCs rotate normally.
Observed
Applying Tech QA 1890's transform during viewWillTransition, the MTKView does counter-rotate... BUT that rotation is still uncomfortably visible. The edges of the view come unpinned during the animation, masking some camera pixels and showing a white background set for the VC holding the MTKView.
Question
How can I make those edges stick to screen bounds like a scared clam?
I assume my error is in constraints, but I'm open to being wrong in other ways. :)
View Hierarchy
A tiny camera filter app has an overlay of camera controls (VC #1) atop an MTKView (in VC #2) pinned to the screen's edges.
UINavigationController
│
└─ CameraScreenVC
│
├── CameraControlsVC <- Please rotate subviews
│
└── MetalCameraFeedVC
└── MTKView <- Please no rotation edges
Code
Relevant snippets below.
MetalCameraVC.swift
final class MetalCameraVC: UIViewController {
let mtkView = MTKView() // This VC's only view
/// Called in viewDidAppear
func setupMetal(){
metalDevice = MTLCreateSystemDefaultDevice()
mtkView.device = metalDevice
mtkView.isPaused = true
mtkView.enableSetNeedsDisplay = false
metalCommandQueue = metalDevice.makeCommandQueue()
mtkView.delegate = self
mtkView.framebufferOnly = false
ciContext = CIContext(
mtlDevice: metalDevice,
options: [.workingColorSpace: CGColorSpace(name: CGColorSpace.sRGB)!])
}
...
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
// blank
}
func draw(in mtkview: MTKView) {
image = image.transformed(by: scaleToScreenBounds)
image = image.cropped(to: mtkview.drawableSize.zeroOriginRect())
guard let buffer = metalCommandQueue.makeCommandBuffer(),
let currentDrawable = mtkview.currentDrawable
else { return }
ciContext.render(image,
to: currentDrawable.texture,
commandBuffer: buffer,
bounds: mtkview.drawableSize.zeroOriginRect(),
colorSpace: CGColorSpaceCreateDeviceRGB())
buffer.present(currentDrawable)
buffer.commit()
}
}
extension MetalCameraVC {
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(mtkView)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
mtkView.frame = view.frame
if let orientation = AVCaptureVideoOrientation.fromCurrentDeviceOrientation() {
lastOrientation = orientation
}
}
}
+Rotation
/// Apple Technical QA 1890 Prevent View From Rotating
extension MetalCameraVC {
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
mtkView.center = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate { [self] context in
let delta = coordinator.targetTransform
let deltaAngle = atan2(delta.b, delta.a)
var currentAngle = mtkView.layer.value(forKeyPath: "transform.rotation.z") as? CGFloat ?? 0
currentAngle += -1 * deltaAngle + 0.1
mtkView.layer.setValue(currentAngle, forKeyPath: "transform.rotation.z")
} completion: { [self] context in
var rounded = mtkView.transform
rounded.a = round(rounded.a)
rounded.b = round(rounded.b)
rounded.c = round(rounded.c)
rounded.d = round(rounded.d)
mtkView.transform = rounded
}
}
}

I think the "easiest" solution is to actually prevent your UI from rotating and observing device orientation changes to manually rotate only the interface elements you want to rotate.
In your view controller, you can override
shouldAutorotateto prevent auto-rotation and usesupportedInterfaceOrientationsto only return the one allowed orientation.Rotating UI elements manually can be done using their
transformproperty.