SwiftUIX / SwiftUIX/SwiftUIX

[BUG] ImagePicker renders a blank camera preview on iOS 27

Open Beginner friendly
#563 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Swift
Stars
8.2k
Forks
495
PR merge metrics
No merged PRs in 30d

Description

ImagePicker renders a blank camera preview on iOS 27 — sourceType is applied too late

Summary

ImagePicker(...).sourceType(.camera) presents the camera UI (shutter button, flash and cancel
controls are all laid out correctly) over a completely black preview area. No frames are ever
delivered. This reproduces on every launch on iOS 27 beta.

The cause is that sourceType is never set on the controller that is actually presented — it is
only assigned afterwards, in updateUIViewController.

Cause

Sources/SwiftUIX/Intramodular/Images/ImagePicker.swift:

public func makeUIViewController(context: Context) -> UIViewControllerType {
    UIImagePickerController().then {
        $0.delegate = context.coordinator
    }
}

public func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
    context.coordinator.base = self

    uiViewController.allowsEditing = allowsEditing
    uiViewController.sourceType = sourceType      // too late
    ...
}

UIImagePickerController() is created with the default sourceType of .photoLibrary, which on
iOS 14+ is backed by an out-of-process picker. sourceType is then mutated to .camera in
updateUIViewController, i.e. after the controller has already configured itself for the photo
library.

UIImagePickerController builds its capture session as part of being configured for .camera, so
a controller that was born as .photoLibrary never starts one. Apple's documentation has always
required the source type to be set before the picker is presented; earlier iOS releases happened to
tolerate the late switch, iOS 27 does not.

Steps to reproduce

Requires a real device — the simulator has no camera.

struct ContentView: View {
    @State private var isShowingCamera = false
    @State private var image: UIImage?

    var body: some View {
        Button("Take photo") { isShowingCamera = true }
            .fullScreenCover(isPresented: $isShowingCamera) {
                ImagePicker(image: $image, onCancel: { isShowingCamera = false })
                    .sourceType(.camera)
                    .edgesIgnoringSafeArea(.all)
            }
    }
}

Grant camera access, then tap "Take photo".

Expected: a live camera preview.
Actual: camera chrome over a black preview; no frames.

Notes

  • The camera hardware and the permission grant are fine: an AVCaptureSession-based view in the
    same app on the same device shows a live preview normally.
  • The photo library path (sourceType left at its .photoLibrary default) is unaffected, since
    no transition happens in that case.
  • ImagePicker.swift is unchanged between 0.2.3 and 0.3.1, so this is not a recent regression in
    SwiftUIX — it is long-standing code that iOS 27 stopped tolerating.

Suggested fix

Set the source type (and the camera-only properties that depend on it) while creating the
controller, and stop mutating sourceType in updateUIViewController:

 public func makeUIViewController(context: Context) -> UIViewControllerType {
-    UIImagePickerController().then {
-        $0.delegate = context.coordinator
-    }
+    UIImagePickerController().then {
+        $0.delegate = context.coordinator
+
+        if UIImagePickerController.isSourceTypeAvailable(sourceType) {
+            $0.sourceType = sourceType
+        }
+
+        if $0.sourceType == .camera {
+            $0.cameraDevice = cameraDevice ?? .rear
+        }
+    }
 }

 public func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
     context.coordinator.base = self

     uiViewController.allowsEditing = allowsEditing
-    uiViewController.sourceType = sourceType

     if let mediaTypes = mediaTypes, uiViewController.mediaTypes != mediaTypes  {
         uiViewController.mediaTypes = mediaTypes
     }
-
-    if uiViewController.sourceType == .camera {
-        uiViewController.cameraDevice = cameraDevice ?? .rear
-    }
 }

Because sourceType and cameraDevice are only meaningful before presentation, moving them into
makeUIViewController is also closer to what UIImagePickerController documents. If keeping the
modifiers reactive matters, the alternative is to key the representable on sourceType so SwiftUI
rebuilds the controller when it changes, rather than mutating a live one.

I am carrying this as an app-side replacement for now. Happy to open a PR if the approach looks
right.

Environment

  • SwiftUIX 0.3.1 (e1754664a6105529b7978489164720185317c29e)
  • Xcode 26.4.1 (17E202), Swift 6.3.1
  • Deployment target iOS 16.0
  • Reproduced on a device running iOS 27 beta

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with Sources/SwiftUIX/Intramodular/Images/ImagePicker.swift, comparing makeUIViewController with updateUIViewController and the reported sourceType and cameraDevice assignments. Apply the lifecycle correction described in the issue, then verify the provided camera reproduction on a real iOS 27 device; done means a live camera preview while the photo-library path remains unaffected.

Written by the indexing model from the issue text.

Assessment

Tech stack
swift
Domain
mobile-dev
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.