Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/image_picker/image_picker_ios/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.8.13+4

* Fixes camera confirmation buttons (e.g., Retake/Use Photo) taps passing through to the underlying Flutter UI while the picker is dismissing on some iOS versions (e.g., iOS 26).

## 0.8.13+3

* Fixes a performance regression on iOS where picking videos could cause a long delay due to transcoding. The picker is now configured to request the original asset to avoid conversion.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,9 @@ class _MyHomePageState extends State<MyHomePage> {
? int.parse(limitController.text)
: null;
onPick(width, height, quality, limit);
Navigator.of(context).pop();
// Leave the dialog open to verify that tapping the transparent area no longer pops it.
// Regression check for https://github.com/flutter/flutter/issues/173453.
// Navigator.of(context).pop();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean up?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To reproduce the issue, have to comment this line. Not sure we are going to remove it, so I comment it.

},
),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ @interface FLTImagePickerPlugin ()
/// the array.
@property(strong, nonatomic)
NSMutableArray<UIImagePickerController *> *imagePickerControllerOverrides;
@property(strong, nonatomic) UIWindow *interactionBlockerWindow;
@property(weak, nonatomic) UIWindow *previousKeyWindow;

@end

Expand Down Expand Up @@ -323,6 +325,7 @@ - (void)showCamera:(UIImagePickerControllerCameraDevice)device
[UIImagePickerController isCameraDeviceAvailable:device]) {
imagePickerController.sourceType = UIImagePickerControllerSourceTypeCamera;
imagePickerController.cameraDevice = device;
[self addInteractionBlocker];
[[self viewControllerWithWindow:nil] presentViewController:imagePickerController
animated:YES
completion:nil];
Expand Down Expand Up @@ -532,7 +535,11 @@ - (void)picker:(PHPickerViewController *)picker
- (void)imagePickerController:(UIImagePickerController *)picker
didFinishPickingMediaWithInfo:(NSDictionary<NSString *, id> *)info {
NSURL *videoURL = info[UIImagePickerControllerMediaURL];
[picker dismissViewControllerAnimated:YES completion:nil];
__weak typeof(self) weakSelf = self;
[picker dismissViewControllerAnimated:YES
completion:^{
[weakSelf removeInteractionBlocker];
}];
// The method dismissViewControllerAnimated does not immediately prevent
// further didFinishPickingMediaWithInfo invocations. A nil check is necessary
// to prevent below code to be unwantly executed multiple times and cause a
Expand Down Expand Up @@ -618,7 +625,11 @@ - (void)imagePickerController:(UIImagePickerController *)picker
}

- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
[picker dismissViewControllerAnimated:YES completion:nil];
__weak typeof(self) weakSelf = self;
[picker dismissViewControllerAnimated:YES
completion:^{
[weakSelf removeInteractionBlocker];
}];
[self sendCallResultWithSavedPathList:nil];
}

Expand Down Expand Up @@ -674,4 +685,44 @@ - (void)sendCallResultWithError:(FlutterError *)error {
self.callContext = nil;
}

- (void)addInteractionBlocker {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this needs some documentation on why we need it.

if (self.interactionBlockerWindow != nil) {
return;
}
UIViewController *topController = [self viewControllerWithWindow:nil];
UIWindow *presentingWindow = topController.view.window;
if (!presentingWindow) {
return;
}
self.previousKeyWindow = presentingWindow;
UIWindow *blockerWindow;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need a window rather than just a view to block the touches

Copy link
Author

@celvinren celvinren Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, @hellohuanlin

We’re using a separate UIWindow instead of dropping a view in the Flutter hierarchy to make the touch blocker reliable:

  • Flutter’s UI lives in a UIWindow managed by FlutterViewController; UIImagePickerController is presented modally in that same window, layered above Flutter. When dismissing (e.g., Retake/Use Photo), the picker’s view is animating away, and stray taps can leak to the Flutter view underneath.

  • A view-based blocker (hitTest) would have to be inserted into the host view tree (e.g., FlutterViewController.view), inheriting its bounds/constraints/rotation and any transitions. During modal animations, size/position changes, or if the parent is re-laid out/replaced, the blocker can shift or get removed, letting touches through.

  • A dedicated UIWindow sits above the presenting window (windowLevel + 1), with its own root VC to swallow touches, and doesn’t depend on the host view hierarchy or transitions. We just restore the previous key window after dismiss, making it less invasive and more reliable for the short dismissal window.

The following 2 screenshots are using view and image picker will replace the flutter view controller. The first one shows we are in flutter view controller, the second one shows when we trigger the camera page from flutter, the flutter view become invisible and image picker takes place all the view.
Screenshot 2025-12-03 at 11 00 57 pm
Screenshot 2025-12-03 at 11 01 57 pm

 

The following is using a window for camera
Screenshot 2025-12-03 at 11 10 08 pm

In the first 2 hierarchy snapshots, the picker is full screen, so the Flutter view is temporarily out of the visible view tree. In the third snapshot, both the picker stack and FlutterViewController appear under the same key UIWindow, showing they share one window for event delivery. That’s why a blocker tied to the host view can shift or disappear during the modal transition, while a separate window stays put and reliably swallows taps.

if (@available(iOS 13.0, *) && presentingWindow.windowScene) {
blockerWindow = [[UIWindow alloc] initWithWindowScene:presentingWindow.windowScene];
} else {
blockerWindow = [[UIWindow alloc] initWithFrame:presentingWindow.bounds];
}
blockerWindow.frame = presentingWindow.bounds;
blockerWindow.autoresizingMask =
UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
blockerWindow.windowLevel = presentingWindow.windowLevel + 1;
UIViewController *vc = [[UIViewController alloc] init];
vc.view.backgroundColor = [UIColor clearColor];
vc.view.userInteractionEnabled = YES;
blockerWindow.rootViewController = vc;
[blockerWindow makeKeyAndVisible];
self.interactionBlockerWindow = blockerWindow;
}

- (void)removeInteractionBlocker {
if (!self.interactionBlockerWindow) {
return;
}
self.interactionBlockerWindow.hidden = YES;
if (self.previousKeyWindow) {
[self.previousKeyWindow makeKeyWindow];
}
self.interactionBlockerWindow = nil;
self.previousKeyWindow = nil;
}

@end
2 changes: 1 addition & 1 deletion packages/image_picker/image_picker_ios/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: image_picker_ios
description: iOS implementation of the image_picker plugin.
repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_ios
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
version: 0.8.13+3
version: 0.8.13+4

environment:
sdk: ^3.9.0
Expand Down