Skip to content

fix(ios): stop camera setup from discarding the resizeMode prop - #819

Open
ZayanKhan-12 wants to merge 1 commit into
teslamotors:masterfrom
ZayanKhan-12:fix/335-respect-resize-mode
Open

ZayanKhan-12 wants to merge 1 commit into
teslamotors:masterfrom
ZayanKhan-12:fix/335-respect-resize-mode

Conversation

@ZayanKhan-12

@ZayanKhan-12 ZayanKhan-12 commented Sep 19, 2026 •

Copy link
Copy Markdown

Refs #335

The bug

RealCamera.setup() hard-codes the preview layer's videoGravity:

DispatchQueue.main.async {
    self.cameraPreview.session = self.session
    self.cameraPreview.previewLayer.videoGravity = .resizeAspect   // <- always 'contain'

.resizeAspect is resizeMode: 'contain'. So whenever setup runs after the prop has been delivered, it silently reverts resizeMode: 'cover' back to contain, and the preview letterboxes inside its view instead of filling it — which reads as the camera ignoring the size it was given.

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:

  1. didSetProps → update(resizeMode: .cover) → .resizeAspectFill ✅
  2. user accepts the permission prompt
  3. hasPermissionBeenGranted → setupCamera() → setup() → .resizeAspect ❌ overwrites

On second launch, permission is already .authorized, so setup() is enqueued before update(resizeMode:) within the same didSetProps and cover survives. The result is a resizeMode that works on some launches and not others.

There is a second, corroborating symptom in the same code: update(resizeMode:) never stored its argument, so the

private var resizeMode: ResizeMode = .contain

property declared on line 41 was assigned by nothing and read by nothing — dead state that shows the mode was always meant to be remembered and re-applied.

The fix

Store the mode, and apply it from both call sites:

func update(resizeMode: ResizeMode) {
    DispatchQueue.main.async {
        self.resizeMode = resizeMode
        self.applyResizeMode()
    }
}

private func applyResizeMode() {
    switch resizeMode {
    case .cover:   cameraPreview.previewLayer.videoGravity = .resizeAspectFill
    case .contain: cameraPreview.previewLayer.videoGravity = .resizeAspect
    }
}

setup() now calls applyResizeMode() instead of hard-coding .resizeAspect, so it re-asserts whatever the prop asked for rather than clobbering it. Behaviour is unchanged for anyone who never sets resizeMode (the default is .contain, exactly what was hard-coded).

Also documents the actual default in the README — the resizeMode row previously ended with "Default behavior depends on the specific use case", which tells the reader nothing.

Scope

This addresses the iOS half of #335. Two notes for maintainers on the rest of that (2020, pre-rewrite) report:

  • Resizing itself works now. RN calls measure(EXACTLY, EXACTLY) then layout() on the native view for every layout change, on both architectures, and CameraView pins its subviews with constraints (addFullSizeSubview). What was left was the preview gravity inside the correctly-sized view, which is what this PR fixes.
  • resizeMode is iOS-only. Android has no scaleType handling at all; CameraX's PreviewView defaults to FILL_CENTER (equivalent to cover). The README already lists the prop under "iOS only", so this PR does not change Android, but it does mean the two platforms disagree by default. Happy to follow up with an Android scaleType mapping if you'd like that tracked separately.

Verification

  • swiftlint on the changed file: 17 violations before, 17 after — identical to master, none in the changed region, and CI's non-strict invocation exits 0.
  • yarn build (tsc) and yarn lint pass.
  • The ReactNativeCameraKit pod target builds clean for iphoneos/arm64 against the example app's
    CocoaPods setup, with Fabric codegen run — ** BUILD SUCCEEDED **. The full
    CameraKitExample scheme could not be built locally: the vendored fmt pod fails on Xcode 26
    with call to consteval function ... is not a constant expression, which is a React Native
    dependency issue unrelated to this change.

`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 teslamotors#335
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.

2 participants