So I ended up with this in my view controller.
When device is in macro mode, physical ultrawide camera's zoom factor is 1.0 (cause macro is just cropped ultra wide image). And virtual device's zoom factor must be anything more than 2.0. Why? because if it's less than 2.0, then that means that virtual device is using physical ultra wide. While it seems contradictory, it is what it is, maybe someone else can give a better explanation 😅
class MacroViewController: UIViewController {
var zoomFactorObserver: NSKeyValueObservation?
var activeConstituentObserver: NSKeyValueObservation?
let videoDeviceDiscoverySession: AVCaptureDevice.DiscoverySession = {
var types: [AVCaptureDevice.DeviceType] = [.builtInWideAngleCamera, .builtInDualCamera, .builtInDualWideCamera, .builtInTripleCamera, .builtInTrueDepthCamera, .builtInUltraWideCamera]
return AVCaptureDevice.DiscoverySession(deviceTypes: types, mediaType: .video, position: .unspecified)
}()
@objc dynamic var videoDeviceInput: AVCaptureDeviceInput? {
didSet {
guard let device = videoDeviceInput?.device else { return }
var isInMacroMode: Bool {
guard let virtualDevice = self.videoDeviceInput?.device else { return false }
if virtualDevice.isVirtualDeviceWithUltraWideCamera,
let activeCamera = virtualDevice.activePrimaryConstituent,
let ultraWideCamera = self.videoDeviceDiscoverySession.backBuiltInUltraWideCamera,
activeCamera.uniqueID == ultraWideCamera.uniqueID,
virtualDevice.videoZoomFactor >= 2.0,
ultraWideCamera.videoZoomFactor == 1.0 {
return true
} else {
return false
}
}
func showMacroIconIfNeeded() {
macroButton.isHidden = !isInMacroMode
}
self.zoomFactorObserver = device.observe(\.videoZoomFactor) { [unowned self] virtualDevice, change in
DispatchQueue.main.async {
showMacroIconIfNeeded()
}
}
if device.activePrimaryConstituentDeviceSwitchingBehavior != .unsupported {
device.setPrimaryConstituentDeviceSwitchingBehavior(.auto, restrictedSwitchingBehaviorConditions: [])
activeConstituentObserver = device.observe(\.activePrimaryConstituent, options: [.new]) { [weak self] device, change in
guard let self = self else { return }
DispatchQueue.main.async {
showMacroIconIfNeeded()
}
}
}
}
}
}
And below are extensions that I used
extension AVCaptureDevice {
var isVirtualDeviceWithUltraWideCamera: Bool {
switch deviceType {
case .builtInTripleCamera, .builtInDualWideCamera, .builtInUltraWideCamera:
return true
default:
return false
}
}
}
extension AVCaptureDevice.DiscoverySession {
var backBuiltInUltraWideCamera: AVCaptureDevice? {
return devices.first(where: { $0.position == .back && $0.deviceType == .builtInUltraWideCamera })
}
}