1

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.

edges visible during rotation

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

Buildable demo repo

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
        }
    }
    
}
Ryan
  • 1,252
  • 6
  • 15

1 Answers1

0

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 shouldAutorotate to prevent auto-rotation and use supportedInterfaceOrientations to only return the one allowed orientation.

Rotating UI elements manually can be done using their transform property.

Frank Rupprecht
  • 9,191
  • 31
  • 56
  • Hah, in frustration, I did exactly that on the Nav controller (which is the determinant I think for everyone). I prefer that UI... except context menus come up 90* off. :( An Apple engineer suggested the same thing in a lab last week, but said don't tell anyone about it. :) – Ryan Jun 12 '21 at 17:44