Skip to content

Fix: manual exposure crashed the app on iPhone 18 Pro - #272

Merged
darioalessandro merged 2 commits into
masterfrom
fix-exposure-variable-aperture
Nov 9, 2026
Merged

darioalessandro merged 2 commits into
masterfrom
fix-exposure-variable-aperture

Conversation

@darioalessandro

@darioalessandro darioalessandro commented Nov 9, 2026 •

Copy link
Copy Markdown
Member

If you set manual exposure from the remote on an iPhone 18 Pro, the camera phone's app crashed. This fixes it.

What happened, in plain words

Think of the camera as a room with one window. Three things decide how bright the photo is: how long the window stays open (shutter), how wide it opens (aperture), and how sensitive the eye inside is (ISO).

On every iPhone before the 18 Pro, the window is always the same size. So when you went manual, the app told the phone "freeze all three", and that was fine, because one of them never moved anyway.

The 18 Pro has a window that can change size. Apple doesn't let you freeze all three at once on it. We asked anyway, and instead of politely saying no, the app crashed in the middle of the shoot.

What we do now

Before going manual, the app asks the phone what it's willing to do.

  • Older iPhones say "freeze all three is fine", so nothing changes for them.
  • The 18 Pro says "freeze shutter and ISO, and I'll handle the window size". You still get the shutter and ISO you picked, and the phone keeps the brightness right with the window.
  • If a camera says no to both, the remote simply doesn't offer manual exposure. You still get the brightness slider, like on cameras that never had manual.

Nobody gets a crash.

The build server moves to Xcode 27

The fix uses tools that only exist in Apple's newest developer kit, so this PR also moves our build server to it. Tests, the Mac build and the App Store release now run on Xcode 27.1, a release version that App Store Connect accepts. Before, they ran on Xcode 26.2.

GitHub still calls its Xcode 27 machines a "preview", so CI jobs may wait in line a bit longer than they used to.

Tested

  • iPhone 18 Pro: the exposure test crashed before this change and passes now. Manual lands at 1/250 s and ISO 398.77, and going back to Auto works.
  • 809 simulator tests pass.
  • One photo test on the 18 Pro fails, on this branch and on master alike. It got a 12MP photo where it expects 24MP, and it passed an hour earlier in better light. It's unrelated to exposure and needs its own look.
For reviewers: the technical version
  • setExposureModeCustom(duration:iso:) is, per Apple's header, "equivalent to passing AVCaptureLensApertureCurrent", so it locks aperture too. On the 18 Pro that raised NSInvalidArgumentException ("Unsupported combination of auto/locked parameters (lensAperture locked, duration locked, ISO locked)"), which Swift can't catch.
  • CaptureEngine.manualAperture(for:) asks the active format via iOS 27 supportsExposureModeCustom(lensAperture:duration:iso:): all locked → .locked (old call), auto aperture with shutter + ISO locked → .auto (setExposureModeCustom(lensAperture: .autoLensAperture, …)), neither → nil (no Manual).
  • The same check feeds ExposureFacts.supportsCustom, the reported supportsManual, and the lens-hop choice.
  • The iOS 27 calls sit behind #available(iOS 27.0, macCatalyst 27.0, *). CI and release run on GitHub's xcode-27 image with Xcode 27.1, tests on the iOS 27.0 iPhone 17 simulator, and compilation caches are keyed per Xcode version.
  • testExposureProbeAppliesAndReportsTruth: ISO tolerance ±1 → 1%, because the 18 Pro lands 400 as 398.77.

ISO is really a gain applied to the sensor's signal, not the eye getting more sensitive, and the window size is the f-number (focal length over opening diameter). The picture holds for the point here: the 18 Pro has one more moving part, and it won't let you freeze all of them at once.

🤖 Generated with Claude Code

…18 Pro

setExposureModeCustom(duration:iso:) locks the aperture too, and the
iPhone 18 Pro refuses aperture + shutter + ISO all locked: the call raised
an NSInvalidArgumentException and killed the app. The format now says which
custom mode it takes (iOS 27 supportsExposureModeCustom(lensAperture:...)):
all locked keeps the old call, shutter + ISO locked with auto aperture uses
setExposureModeCustom(lensAperture: .auto, ...), and neither reports no
Manual so the director offers bias only.

The iOS 27 calls sit behind #if compiler(>=6.4) so the Xcode 26.2 CI build
compiles; that build keeps the old behavior.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@darioalessandro darioalessandro changed the title Manual exposure on iPhone 18 Pro: stop the crash on variable-aperture lenses Fix: manual exposure crashed the app on iPhone 18 Pro Nov 9, 2026
The fix needs the iOS 27 SDK. Build, Catalyst and the App Store release
move from macos-26 / Xcode 26.2 to GitHub's xcode-27 image with Xcode 27.1
(a release build, which App Store Connect accepts), tests on the iOS 27.0
iPhone 17 simulator. Compilation caches are keyed per Xcode version.

With the SDK guaranteed, the #if compiler guard goes: a missing API now
fails the build instead of silently dropping the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

📊 Test Coverage Report

✅ 784/809 tests passed · ⏭️ 25 skipped

Metric Coverage
App code (excl. generated + vendored Theater) ██████████░░░░░░░░░░ 50.4% 🟡
Whole target ██████████░░░░░░░░░░ 51.4% 🟡

🎥 Capture & session core

File Coverage Lines
CaptureEngine.swift ██░░░░░░░░░░░░░░░░░░ 11.8% 258/2195 🔴
RecordingPipeline.swift ████████████████░░░░ 80.7% 706/875 🟢
FrameStreamingCoordinator.swift ███░░░░░░░░░░░░░░░░░ 13.0% 32/247 🔴
FrameStreamer.swift ███████████████████░ 92.6% 113/122 🟢
RemoteCmdFlatBuffers.swift ██████████████████░░ 92.0% 1184/1287 🟢
📁 Full report — 103 files
File Coverage Lines
DebugLog.swift █████████████████████ 104.8% 22/21 🟢
JPEGFrameEncoder.swift ████████████████████ 100.0% 12/12 🟢
CameraPreviewMode.swift ████████████████████ 100.0% 13/13 🟢
AppActivityMonitor.swift ████████████████████ 100.0% 31/31 🟢
AppTheme.swift ████████████████████ 100.0% 33/33 🟢
ZoomScale.swift ████████████████████ 100.0% 56/56 🟢
Messages.swift ████████████████████ 100.0% 7/7 🟢
ZoomScaleSeed.swift ████████████████████ 100.0% 14/14 🟢
MonitorChrome.swift ████████████████████ 100.0% 12/12 🟢
PeerSessionCore.swift ████████████████████ 100.0% 26/26 🟢
ClockOffsetEstimator.swift ████████████████████ 100.0% 24/24 🟢
ExposurePolicy.swift ████████████████████ 100.0% 42/42 🟢
StreamingConfig.swift ████████████████████ 100.0% 34/34 🟢
MultipeerMessages.swift ████████████████████ 100.0% 12/12 🟢
FocusedCameraControlState.swift ████████████████████ 100.0% 6/6 🟢
Locked.swift ████████████████████ 100.0% 19/19 🟢
FrameCreditWindow.swift ████████████████████ 100.0% 9/9 🟢
CameraLink.swift ████████████████████ 100.0% 59/59 🟢
AppDelegate.swift ████████████████████ 100.0% 16/16 🟢
HEICFrameEncoder.swift ████████████████████ 100.0% 17/17 🟢
PeerCompatibility.swift ████████████████████ 100.0% 42/42 🟢
RigQualityMenu.swift ████████████████████ 99.3% 140/141 🟢
DeviceScannerViewModel.swift ████████████████████ 98.9% 183/185 🟢
FrameSender.swift ████████████████████ 98.4% 184/187 🟢
FocusPointMapping.swift ████████████████████ 97.6% 40/41 🟢
CaptureSyncMetadata.swift ███████████████████░ 97.0% 65/67 🟢
MultiCamChrome.swift ███████████████████░ 96.9% 31/32 🟢
MulticamViewModel.swift ███████████████████░ 96.0% 145/151 🟢
RemoteCmds.swift ███████████████████░ 95.9% 325/339 🟢
VP9FrameEncoder.swift ███████████████████░ 94.7% 195/206 🟢
DiscoveredPeers.swift ███████████████████░ 94.4% 17/18 🟢
WatchSharedTypes.swift ███████████████████░ 94.0% 236/251 🟢
WatchPreviewStreamer.swift ███████████████████░ 93.6% 44/47 🟢
ZoomPill.swift ███████████████████░ 93.1% 108/116 🟢
MulticamController.swift ███████████████████░ 92.8% 1488/1604 🟢
FrameStreamer.swift ███████████████████░ 92.6% 113/122 🟢
RemoteCmdFlatBuffers.swift ██████████████████░░ 92.0% 1184/1287 🟢
WelcomeView.swift ██████████████████░░ 89.3% 492/551 🟢
VideoTransferProgressView.swift █████████████████░░░ 84.0% 295/351 🟢
UIImage+gif.swift ████████████████░░░░ 81.6% 129/158 🟢
CameraDeviceDescriptor.swift ████████████████░░░░ 81.0% 34/42 🟢
RecordingPipeline.swift ████████████████░░░░ 80.7% 706/875 🟢
CameraProgressOverlayView.swift ████████████████░░░░ 78.5% 186/237 🟡
SessionCoordinator.swift ████████████████░░░░ 77.9% 1870/2399 🟡
UICmds.swift ███████████████░░░░░ 77.4% 72/93 🟡
FlatBufferSchemas_generated.swift (excluded from app-code metric) ███████████████░░░░░ 77.0% 876/1138 🟡
SettingsView.swift ███████████████░░░░░ 76.2% 593/778 🟡
RolePickerView.swift ███████████████░░░░░ 76.2% 314/412 🟡
StreamLog.swift ███████████████░░░░░ 75.0% 15/20 🟡
WelcomeViewModel.swift ███████████████░░░░░ 74.1% 100/135 🟡
CameraRecordingTimerView.swift ███████████████░░░░░ 73.8% 127/172 🟡
SettingsViewModel.swift ██████████████░░░░░░ 70.6% 89/126 🟡
DeviceScannerView.swift ██████████████░░░░░░ 70.1% 1142/1630 🟡
MonitorView.swift ██████████████░░░░░░ 69.9% 553/791 🟡
SwiftConstants.swift █████████████░░░░░░░ 66.7% 12/18 🟡
UIAlertController.swift █████████████░░░░░░░ 66.0% 35/53 🟡
WelcomeViewController.swift █████████████░░░░░░░ 64.1% 41/64 🟡
ScannerLobby.swift ████████████░░░░░░░░ 62.5% 5/8 🟡
CameraViewModel.swift ████████████░░░░░░░░ 59.9% 160/267 🟡
ViewfinderGestureLayer.swift ███████████░░░░░░░░░ 55.8% 92/165 🟡
StoreManager.swift ███████████░░░░░░░░░ 54.6% 83/152 🟡
MulticamView.swift ███████████░░░░░░░░░ 54.2% 735/1356 🟡
RulerPill.swift ███████████░░░░░░░░░ 52.7% 327/621 🟡
SceneDelegate.swift ██████████░░░░░░░░░░ 51.9% 14/27 🟡
UIViewController+SwiftUIHosting.swift ██████████░░░░░░░░░░ 48.4% 15/31 🔴
FrameStreamReceiver.swift ██████████░░░░░░░░░░ 47.7% 244/512 🔴
FrameCodecs.swift █████████░░░░░░░░░░░ 47.1% 24/51 🔴
MonitorViewModel.swift █████████░░░░░░░░░░░ 44.0% 11/25 🔴
PurchaseManaging.swift ████████░░░░░░░░░░░░ 42.0% 21/50 🔴
CameraScreenView.swift ████████░░░░░░░░░░░░ 39.3% 262/666 🔴
MulticamViewController.swift ████████░░░░░░░░░░░░ 38.0% 130/342 🔴
EnumExtensions.swift ███████░░░░░░░░░░░░░ 33.9% 40/118 🔴
HEVCFrameEncoder.swift ██████░░░░░░░░░░░░░░ 31.9% 110/345 🔴
ExposureControls.swift ██████░░░░░░░░░░░░░░ 30.8% 94/305 🔴
CaptureTally.swift █████░░░░░░░░░░░░░░░ 25.0% 13/52 🔴
DeviceScannerViewController.swift █████░░░░░░░░░░░░░░░ 24.3% 117/482 🔴
CameraRig.swift █████░░░░░░░░░░░░░░░ 23.8% 150/629 🔴
WatchSessionManager.swift ████░░░░░░░░░░░░░░░░ 20.8% 37/178 🔴
MediaProcessors.swift ███░░░░░░░░░░░░░░░░░ 17.5% 11/63 🔴
SessionDebugConsole.swift ███░░░░░░░░░░░░░░░░░ 15.0% 74/494 🔴
FrameStreamingCoordinator.swift ███░░░░░░░░░░░░░░░░░ 13.0% 32/247 🔴
CaptureEngine.swift ██░░░░░░░░░░░░░░░░░░ 11.8% 258/2195 🔴
RolePickerController.swift ██░░░░░░░░░░░░░░░░░░ 10.2% 23/226 🔴
PeerLinkStatus.swift █░░░░░░░░░░░░░░░░░░░ 7.1% 10/141 🔴
CameraCountdownTorch.swift █░░░░░░░░░░░░░░░░░░░ 4.9% 3/61 🔴
WatchRemoteCameraController.swift █░░░░░░░░░░░░░░░░░░░ 3.2% 9/279 🔴
SoundManager.swift ░░░░░░░░░░░░░░░░░░░░ 2.1% 1/47 🔴
RecordingTimer.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/52 🔴
CameraPermissionsView.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/676 🔴
CameraHostController.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/205 🔴
RemoteShutterHelpView.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/1372 🔴
CameraPermissionErrorView.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/416 🔴
PreviewSurface.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/100 🔴
PhotosAccessDeniedView.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/613 🔴
MicrophonePermissionPromptView.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/347 🔴
Photos.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/12 🔴
LocalNetworkPermissionView.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/747 🔴
OrientationUtils.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/18 🔴
PermissionManager.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/185 🔴
PhotosAccessHelper.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/347 🔴
UIAlertPresenter.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/43 🔴
MultipeerService.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/123 🔴
Data+MD5.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/4 🔴

Generated from 5e192fd · workflow run

@darioalessandro
darioalessandro merged commit fd0698e into master Nov 9, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant