From ab570b35c5999f326231599af16f039aa22f203a Mon Sep 17 00:00:00 2001 From: ZayanKhan-12 Date: Sat, 19 Sep 2026 13:12:32 -0400 Subject: [PATCH] fix(ios): stop camera setup from discarding the resizeMode prop `RealCamera.setup()` hard-coded the preview layer's videoGravity: self.cameraPreview.session = self.session self.cameraPreview.previewLayer.videoGravity = .resizeAspect `.resizeAspect` is `resizeMode: 'contain'`, so whenever setup ran after the prop had been delivered it silently reverted `resizeMode: 'cover'` back to `contain`. That ordering is the normal one on a cold start. `didSetProps` sets `hasPropBeenSetup`, but `setupCamera()` also waits on `hasPermissionBeenGranted`. When authorization is `.notDetermined` the system prompt resolves asynchronously, so the sequence is: 1. didSetProps -> update(resizeMode: .cover) -> .resizeAspectFill 2. user accepts the permission prompt 3. hasPermissionBeenGranted -> setup() -> .resizeAspect <- overwrites The preview then letterboxes inside its view instead of filling it, which reads as the camera ignoring the size it was given. `update(resizeMode:)` also never stored its argument, leaving the `resizeMode` property declared on line 41 assigned by nothing and read by nothing. Storing it and applying it from both call sites fixes the revert and gives that property its intended job. Also documents the actual default (`contain`) in the README, which previously said only "Default behavior depends on the specific use case". Refs #335 --- README.md | 2 +- ios/ReactNativeCameraKit/RealCamera.swift | 21 ++++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index ce44e5236e..02b4250686 100644 --- a/README.md +++ b/README.md @@ -234,7 +234,7 @@ To have Play Services [pre-download the model in the background after the app is | `ratioOverlayColor` | Color | Any color with alpha. Default: `'#ffffff77'` | | `resetFocusTimeout` | `number` | Dismiss tap to focus after this many milliseconds. Default `0` (disabled). Example: `5000` is 5 seconds. | | `resetFocusWhenMotionDetected` | Boolean | Dismiss tap to focus when focus area content changes. Native iOS feature, see documentation: https://developer.apple.com/documentation/avfoundation/avcapturedevice/1624644-subjectareachangemonitoringenabl?language=objc). Default `true`. | -| `resizeMode` | `'cover' / 'contain'` | Determines the scaling and cropping behavior of content within the view. `cover` (resizeAspectFill on iOS) scales the content to fill the view completely, potentially cropping content if its aspect ratio differs from the view. `contain` (resizeAspect on iOS) scales the content to fit within the view's bounds without cropping, ensuring all content is visible but may introduce letterboxing. Default behavior depends on the specific use case. | +| `resizeMode` | `'cover' / 'contain'` | Determines the scaling and cropping behavior of content within the view. `cover` (resizeAspectFill on iOS) scales the content to fill the view completely, potentially cropping content if its aspect ratio differs from the view. `contain` (resizeAspect on iOS) scales the content to fit within the view's bounds without cropping, ensuring all content is visible but may introduce letterboxing. Default: `contain`. | | `scanThrottleDelay` | `number` | Duration between scan detection in milliseconds. Default 2000 (2s) | | `maxPhotoQualityPrioritization` | `'balanced'` / `'quality'` / `'speed'` | [iOS 13 and newer](https://developer.apple.com/documentation/avfoundation/avcapturephotooutput/3182995-maxphotoqualityprioritization). `'speed'` provides a 60-80% median capture time reduction vs 'quality' setting. Tested on iPhone 6S Max (66% faster) and iPhone 15 Pro Max (76% faster!). Default `balanced` | | `iOsDeferredStart` | `boolean` | iOS 26+ only. Enables `AVCaptureOutput.deferredStartEnabled` when supported to get the preview visible faster. Default `true`. When enabled, the first capture can be delayed by a few hundred milliseconds. Ignored on Android and on older iOS versions. | diff --git a/ios/ReactNativeCameraKit/RealCamera.swift b/ios/ReactNativeCameraKit/RealCamera.swift index 5774b14efb..15e043f34e 100644 --- a/ios/ReactNativeCameraKit/RealCamera.swift +++ b/ios/ReactNativeCameraKit/RealCamera.swift @@ -133,7 +133,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega // "AVCaptureSession beginConfiguration" from the com.apple.cameracapture subsystem DispatchQueue.main.async { self.cameraPreview.session = self.session - self.cameraPreview.previewLayer.videoGravity = .resizeAspect + self.applyResizeMode() self.startMotionUpdates() @@ -364,12 +364,19 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega func update(resizeMode: ResizeMode) { DispatchQueue.main.async { - switch resizeMode { - case .cover: - self.cameraPreview.previewLayer.videoGravity = .resizeAspectFill - case .contain: - self.cameraPreview.previewLayer.videoGravity = .resizeAspect - } + self.resizeMode = resizeMode + self.applyResizeMode() + } + } + + /// Pushes `resizeMode` onto the preview layer. Called both when the prop changes and when the + /// session is set up, since setup can happen after the prop has already been delivered. + private func applyResizeMode() { + switch resizeMode { + case .cover: + cameraPreview.previewLayer.videoGravity = .resizeAspectFill + case .contain: + cameraPreview.previewLayer.videoGravity = .resizeAspect } }