Apple introduced Locked Capture Extension in iOS 18 and have since made some minor improvements. But one gap, as I realized, is with AVCaptureDevice.RotationCoordinator. This coordinator can report changes to the device orientation regardless of the UI orientation or the device orientation (which is locked to portrait if so desired by the user).

If you’re searching for this, you’re not doing anything wrong — it’s not a misconfiguration. This coordinator just silently fails to report the camera orientation — everything returns 0.

A workaround is the old-fashioned way with Core Motion:

import CoreMotion
import Combine

@MainActor
final class CaptureRotationProvider: NSObject, ObservableObject {
	override init() {
		super.init(){: standalone}

	   start(){: standalone}

   }
	
	private let motionManager = CMMotionManager(){: standalone}

   @Published private(set) var rotationAngle: CGFloat = 90  // portrait default
	
	func start() {
		guard motionManager.isAccelerometerAvailable else { return }
		motionManager.accelerometerUpdateInterval = 0.5
		motionManager.startAccelerometerUpdates(to: .main) { [weak self] data, _ in
			guard let g = data?.acceleration else { return }
			// Ignore when the phone is flat — keep last known angle
			guard abs(g.z) < 0.8 else { return }
			self?.rotationAngle = switch (g.x, g.y) {
			case let (x, y) where abs(y) > abs(x): y < 0 ? 90 : 270   // portrait / upside down
			case let (x, _) where x > 0:           0                  // landscape, home right
			default:                               180                // landscape, home left
			}
		}
	}
	
	func stop() { motionManager.stopAccelerometerUpdates() }
}

You can pass in the rotationAngle property when your camera is running from the Locked Capture Extension:

let photoOutput = ... // your AVCapturePhotoOutput
let coordinator = ... // your RotationCoordinator
if let connection = photoOutput.connection(with: .video) {
	connection.videoRotationAngle = coordinator.videoRotationAngleForHorizonLevelCapture
}
photoOutput.capturePhoto()

I don’t think Apple is making any security decisions here, for two reasons:

  1. If it were indeed for security concerns, there would be documentations.

  2. Core Motion is not handicapped in the extension. Theoretically, there isn’t anything you can’t get from Core Motion that you can from RotationCoordinator.

It’s still not fixed in iOS 27 beta 1. Because this is such a niche case — calling an iOS 17 API inside an iOS 18 extension — I’m not holding my breath here.

A final got-ya

If it wasn’t fun enough to have RotationCoordinator silently fail, you’re in for more!

If your AVCaptureDevice is a back-facing camera, you will need to flip 180 and 0 in the rotation angle. Again, there’s no reason and rhyme, and I don’t count on a bug fix.

let angle = rotationProvider.rotationAngle
if device.devicePosition == .front {
	// Use `angle` as is
	capturePhoto(angle)
} else {
	// Flip 0 and 180, but keep 90 and 270.
	if angle == 0 {
		capturePhoto(180)
	} else
	if angle == 180 {
		capturePhoto(0)
	} else {
		capturePhoto(angle)
	}
}

Fun, isn’t it!