flashMode .on overrides locked focus — AF scan runs and refocuses before the strobe

I'm building a fixed-focus camera app (Bayer RAW, manual exposure, focus permanently locked at a known lens position) and need a full-power flash still that keeps that locked focus. On iPhone 17 Pro, iOS 26, any capture with flashMode = .on runs an autofocus-assist scan — visible lens hunt, assist lamp in low light — and the exposure happens at whatever the scan converged on (usually the far background), not my locked position. In low light it reproduces every time; bright-scene behavior varied by configuration (see matrix), and in my current build it hunts in daylight too.

This follows https://developer.apple.com/forums/thread/724897 where DTS confirmed locked lens position + flash "is possible" in a test app, but the differentiating configuration was never identified. I've now exhausted every documented lever, with instrumentation, and would like guidance on whether this stage is bypassable at all.

Session configuration

session.sessionPreset = .photo
// Plain wide camera (also reproduced on .builtInLiDARDepthCamera).
let camera = AVCaptureDevice.default(
    .builtInWideAngleCamera, for: .video, position: .back
)!
session.addInput(try AVCaptureDeviceInput(device: camera))
session.addOutput(photoOutput)

photoOutput.maxPhotoQualityPrioritization = .speed
photoOutput.isZeroShutterLagEnabled = false
photoOutput.isResponsiveCaptureEnabled = true
// Pre-allocate flash capture resources up front.
photoOutput.setPreparedPhotoSettingsArray(
    [makeFlashRawSettings()], completionHandler: nil
)

Device configuration (before any capture)

try camera.lockForConfiguration()

camera.isSubjectAreaChangeMonitoringEnabled = false
camera.isSmoothAutoFocusEnabled = false
camera.autoFocusRangeRestriction = .near
camera.automaticallyEnablesLowLightBoostWhenAvailable = false
camera.automaticallyAdjustsFaceDrivenAutoFocusEnabled = false
camera.isFaceDrivenAutoFocusEnabled = false  // true is worse; see matrix
camera.automaticallyAdjustsFaceDrivenAutoExposureEnabled = false
camera.isFaceDrivenAutoExposureEnabled = false
camera.setFocusModeLocked(lensPosition: 0.60)  // ~1 m on this device

let gains = camera.deviceWhiteBalanceGains(
    for: .init(temperature: 4000, tint: 0)
)
camera.setWhiteBalanceModeLocked(with: gains)

camera.setExposureModeCustom(
    duration: CMTime(value: 1, timescale: 125), iso: 400
)

camera.unlockForConfiguration()

Capture

func makeFlashRawSettings() -> AVCapturePhotoSettings {
    let bayer = photoOutput.availableRawPhotoPixelFormatTypes.first {
        AVCapturePhotoOutput.isBayerRAWPixelFormat($0)
    }!
    let settings = AVCapturePhotoSettings(rawPixelFormatType: bayer)
    settings.flashMode = .on
    settings.isAutoRedEyeReductionEnabled = false
    settings.isAutoStillImageStabilizationEnabled = false
    settings.isAutoVirtualDeviceFusionEnabled = false
    settings.isAutoContentAwareDistortionCorrectionEnabled = false
    settings.photoQualityPrioritization = .speed
    return settings
}

// Right before the request, exposure flips .custom → .locked (same
// duration/ISO), one preview frame presents, then:
photoOutput.capturePhoto(with: makeFlashRawSettings(), delegate: self)

What I measure (KVO during the capture window)

  • isAdjustingFocus goes true after the request: a scan runs despite focusMode == .locked (assist lamp on in low light).
  • lensPosition moves from the locked 0.60 to a far position and stays there through willCapturePhotoFor — the exposure uses the scan's answer.
  • When isAdjustingFocus falls back to false (before the metering preflash) I issue one setFocusModeLocked(lensPosition: 0.60). The call succeeds, yet the exposure still happens at the far position — something re-asserts the scan's focus before the strobe.
  • With flashMode = .off, locked focus and custom exposure are honored perfectly (my app's normal path).

Tried, all reproduced

  • focusMode = .locked + setFocusModeLocked(lensPosition:): scan still runs.
  • Exposure .custom → .locked for the flash window: no change.
  • Plain wide vs LiDAR wide device: no change.
  • Subject-area / smooth AF / face-driven AF+AE / low-light boost all off: scan still runs in low light.
  • isFaceDrivenAutoFocusEnabled = true (steer scan toward faces): worse — scans in daylight too.
  • Red-eye reduction, fusion, distortion correction off: no change.
  • autoFocusRangeRestriction = .near: still converges far.
  • photoQualityPrioritization = .speed everywhere: no change.
  • Prepared photo settings (flash RAW): no change.
  • Re-lock lens mid-scan: fights the scan; worse convergence.
  • Re-lock lens after scan, before strobe: succeeds; exposure still at the scan's position.
  • Torch lit at request time: scans even in bright daylight.

The last two items seem diagnostic: the gate is not scene brightness. A torch adding no visible light in daylight still arms the scan, and face-driven AF arms it in bright scenes — the AF-assist stage runs whenever the sequence sees any reason to focus, and a client's .locked focus mode is never treated as that reason being absent. Questions

  1. Is the flash AF-assist stage skippable when focusMode == .locked? The earlier thread's DTS reply achieved a locked-focus flash photo — what configuration makes that true on current hardware/iOS?
  2. If not skippable: is there a supported way for the final exposure to honor the locked lens position — run the scan but not apply its result?
  3. Why does a successful setFocusModeLocked(lensPosition:) issued between scan end and strobe not stick? Is the sequence re-asserting its own focus at exposure time?
  4. Are the observed triggers (torch active at request; face-driven AF enabled — each arming the scan regardless of brightness) expected for flashMode = .on?

Goal: full-power flash still + fixed focus + manual exposure, which flashMode = .off already delivers minus the flash. Any guidance — including "file a feedback, here's the rdar to duplicate" — appreciated. Happy to attach a focused sample project and sysdiagnose.

flashMode .on overrides locked focus — AF scan runs and refocuses before the strobe
 
 
Q