Skip to content

[iOS] Fix: invoke MediaPicker completion handler after DismissViewController#34250

Merged
jfversluis merged 3 commits intodotnet:inflight/currentfrom
yuriikyry4enko:fix/mediapicker-dismiss-callback
Feb 26, 2026
Merged

[iOS] Fix: invoke MediaPicker completion handler after DismissViewController#34250
jfversluis merged 3 commits intodotnet:inflight/currentfrom
yuriikyry4enko:fix/mediapicker-dismiss-callback

Conversation

@yuriikyry4enko
Copy link
Copy Markdown
Contributor

@yuriikyry4enko yuriikyry4enko commented Feb 25, 2026

Description of Change

On iOS, CompletedHandler was being invoked before DismissViewController
had finished animating. This means any code running in the handler — such as
showing an alert, opening a modal, or triggering navigation — would fire while
the picker view controller was still visible and attached to the view hierarchy.

iOS UIKit requires the presenting view controller to be fully visible and
attached before presenting any new UI (alerts, modals, sheets). If you attempt
to present something while a dismiss animation is still in progress, UIKit
silently drops the presentation or throws a warning, resulting in:

  • DisplayAlert / DisplayActionSheet not appearing after picking an image
  • Custom modal dialogs not showing up
  • Any await-based UI call after PickPhotoAsync / PickVideoAsync silently failing on iOS, while working fine on Android

The root cause is that DismissViewController(animated:completion:) accepts a
completion callback precisely for this purpose — to run logic after the
animation finishes — but the original code passed null and called the handler
immediately after, creating a race condition.

Fix: Moved all CompletedHandler invocations into the completion callback
of DismissViewController across three call sites:

  • UIImagePickerControllerDelegate.FinishedPickingMedia
  • UIImagePickerControllerDelegate.Canceled
  • PHPickerViewControllerDelegate.DidFinishPicking

Issues Fixed

Fixes #21996

Testing

Tested on a physical iOS device.

  • Opened image picker via MediaPicker.PickPhotoAsync()
  • After selecting an image, called DisplayAlert in the continuation
  • Before fix: alert did not appear after dismissing the picker
  • After fix: alert appears correctly after the picker is fully dismissed

@dotnet-policy-service dotnet-policy-service bot added the community ✨ Community Contribution label Feb 25, 2026
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Hey there @@yuriikyry4enko! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed.

@yuriikyry4enko yuriikyry4enko changed the title Fix: invoke MediaPicker completion handler after DismissViewController [iOS] Fix: invoke MediaPicker completion handler after DismissViewController Feb 25, 2026
@jfversluis
Copy link
Copy Markdown
Member

@yuriikyry4enko thanks for this! Are you sure this isn't already working for .NET 10? Because a lot changed for the MediaPicker in .NET 10.

@yuriikyry4enko
Copy link
Copy Markdown
Contributor Author

@yuriikyry4enko thanks for this! Are you sure this isn't already working for .NET 10? Because a lot changed for the MediaPicker in .NET 10.

I was able to reproduce the issue on a physical device — tested on iPhone 16, iOS 26.2.1. Attaching a sample project for reference.

MediaPickerExample.zip

Copy link
Copy Markdown
Member

@jfversluis jfversluis left a comment

Choose a reason for hiding this comment

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

Multi-Model Code Review — PR #34250

Reviewed by 3 independent AI models (Claude Sonnet 4.5, GPT-5.2, Gemini 3 Pro) via Copilot CLI

Is this still needed?

YES. The file MediaPicker.ios.cs has identical SHA on both main and net10.0 branches. The bug is not fixed anywhere.

Is the fix correct?

All 3 models agree: YES, the core approach is correct. Moving CompletedHandler into the DismissViewController completion callback is the textbook iOS UIKit pattern — it ensures the picker is fully dismissed before resolving the TaskCompletionSource, which prevents subsequent UI presentations (like DisplayAlert) from silently failing.

The PHPickerViewController path (iOS 14+, most common) is clean and correct as-is.


Issues Found (consensus across all 3 models)

1. 🚨 Double-dismiss on UIImagePickerController path

All 3 models independently flagged this. After this PR, the sequence is:

  1. FinishedPickingMedia calls DismissViewController(true, () => CompletedHandler?.Invoke(info))
  2. Dismiss completes → CompletedHandler runs
  3. CompletedHandler calls await picker.DismissViewControllerAsync(true)second dismiss on already-dismissed VC

The CompletedHandler lambda (~line 140-144) still contains its own dismiss call:

CompletedHandler = async info =>
{
    GetFileResult(info, tcs, options);
    await picker.DismissViewControllerAsync(true); // ← redundant, VC already dismissed by delegate
}

Risk: Low (UIKit typically no-ops on already-dismissed VCs), but it's a code smell that could produce console warnings. Recommendation: Remove await picker.DismissViewControllerAsync(true) from the CompletedHandler since the delegate now handles dismissal.

2. ⚠️ Whitespace inconsistency (tabs vs spaces)

The FinishedPickingMedia method changed from tab indentation to space indentation, and DidFinishPicking has mixed tabs/spaces. Please run dotnet format or manually align to match the existing file style before merge.


Things That Look Good

  • ✅ The var captured pattern in DidFinishPicking is good defensive coding — captures and sanitizes results before the async callback
  • ✅ No threading concerns — all paths run on the main thread via UIKit completion callbacks
  • tcs.TrySetResult is thread-safe
  • ✅ The slight delay from waiting for dismiss animation (~400ms) is actually desirable — it's what prevents the race condition

Summary

The fix is the right approach and resolves the core issue. Please address:

  1. Remove the redundant dismiss in the UIImagePickerController CompletedHandler (~line 143)
  2. Fix whitespace to match existing file style

After those changes, this is good to merge. 👍

@yuriikyry4enko
Copy link
Copy Markdown
Contributor Author

@yuriikyry4enko please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.

@dotnet-policy-service agree [company="{your company}"]

Options:

  • (default - no company specified) I have sole ownership of intellectual property rights to my Submissions and I am not making Submissions in the course of work for my employer.
@dotnet-policy-service agree
  • (when company given) I am making Submissions in the course of work for my employer (or my employer has intellectual property rights in my Submissions by contract or applicable law). I have permission from my employer to make Submissions and enter into this Agreement on behalf of my employer. By signing below, the defined term “You” includes me and my employer.
@dotnet-policy-service agree company="Microsoft"

Contributor License Agreement

Contribution License Agreement

This Contribution License Agreement ( “Agreement” ) is agreed to by the party signing below ( “You” ), and conveys certain license rights to the .NET Foundation ( “.NET Foundation” ) for Your contributions to .NET Foundation open source projects. This Agreement is effective as of the latest signature date below.

1. Definitions.

“Code” means the computer software code, whether in human-readable or machine-executable form, that is delivered by You to .NET Foundation under this Agreement.

“Project” means any of the projects owned or managed by .NET Foundation and offered under a license approved by the Open Source Initiative (www.opensource.org).

“Submit” is the act of uploading, submitting, transmitting, or distributing code or other content to any Project, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Project for the purpose of discussing and improving that Project, but excluding communication that is conspicuously marked or otherwise designated in writing by You as “Not a Submission.”

“Submission” means the Code and any other copyrightable material Submitted by You, including any associated comments and documentation.

2. Your Submission. You must agree to the terms of this Agreement before making a Submission to any Project. This Agreement covers any and all Submissions that You, now or in the future (except as described in Section 4 below), Submit to any Project.

3. Originality of Work. You represent that each of Your Submissions is entirely Your original work. Should You wish to Submit materials that are not Your original work, You may Submit them separately to the Project if You (a) retain all copyright and license information that was in the materials as you received them, (b) in the description accompanying your Submission, include the phrase "Submission containing materials of a third party:" followed by the names of the third party and any licenses or other restrictions of which You are aware, and (c) follow any other instructions in the Project's written guidelines concerning Submissions.

4. Your Employer. References to “employer” in this Agreement include Your employer or anyone else for whom You are acting in making Your Submission, e.g. as a contractor, vendor, or agent. If Your Submission is made in the course of Your work for an employer or Your employer has intellectual property rights in Your Submission by contract or applicable law, You must secure permission from Your employer to make the Submission before signing this Agreement. In that case, the term “You” in this Agreement will refer to You and the employer collectively. If You change employers in the future and desire to Submit additional Submissions for the new employer, then You agree to sign a new Agreement and secure permission from the new employer before Submitting those Submissions.

5. Licenses.

a. Copyright License. You grant .NET Foundation, and those who receive the Submission directly or indirectly from .NET Foundation, a perpetual, worldwide, non-exclusive, royalty-free, irrevocable license in the Submission to reproduce, prepare derivative works of, publicly display, publicly perform, and distribute the Submission and such derivative works, and to sublicense any or all of the foregoing rights to third parties.

b. Patent License. You grant .NET Foundation, and those who receive the Submission directly or indirectly from .NET Foundation, a perpetual, worldwide, non-exclusive, royalty-free, irrevocable license under Your patent claims that are necessarily infringed by the Submission or the combination of the Submission with the Project to which it was Submitted to make, have made, use, offer to sell, sell and import or otherwise dispose of the Submission alone or with the Project.

c. Other Rights Reserved. Each party reserves all rights not expressly granted in this Agreement. No additional licenses or rights whatsoever (including, without limitation, any implied licenses) are granted by implication, exhaustion, estoppel or otherwise.

6. Representations and Warranties. You represent that You are legally entitled to grant the above licenses. You represent that each of Your Submissions is entirely Your original work (except as You may have disclosed under Section 3 ). You represent that You have secured permission from Your employer to make the Submission in cases where Your Submission is made in the course of Your work for Your employer or Your employer has intellectual property rights in Your Submission by contract or applicable law. If You are signing this Agreement on behalf of Your employer, You represent and warrant that You have the necessary authority to bind the listed employer to the obligations contained in this Agreement. You are not expected to provide support for Your Submission, unless You choose to do so. UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING, AND EXCEPT FOR THE WARRANTIES EXPRESSLY STATED IN SECTIONS 3, 4, AND 6 , THE SUBMISSION PROVIDED UNDER THIS AGREEMENT IS PROVIDED WITHOUT WARRANTY OF ANY KIND, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY OF NONINFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.

7. Notice to .NET Foundation. You agree to notify .NET Foundation in writing of any facts or circumstances of which You later become aware that would make Your representations in this Agreement inaccurate in any respect.

8. Information about Submissions. You agree that contributions to Projects and information about contributions may be maintained indefinitely and disclosed publicly, including Your name and other information that You submit with Your Submission.

9. Governing Law/Jurisdiction. This Agreement is governed by the laws of the State of Washington, and the parties consent to exclusive jurisdiction and venue in the federal courts sitting in King County, Washington, unless no federal subject matter jurisdiction exists, in which case the parties consent to exclusive jurisdiction and venue in the Superior Court of King County, Washington. The parties waive all defenses of lack of personal jurisdiction and forum non-conveniens.

10. Entire Agreement/Assignment. This Agreement is the entire agreement between the parties, and supersedes any and all prior agreements, understandings or communications, written or oral, between the parties relating to the subject matter hereof. This Agreement may be assigned by .NET Foundation.

.NET Foundation dedicates this Contribution License Agreement to the public domain according to the Creative Commons CC0 1.

@dotnet-policy-service agree

@yuriikyry4enko please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.

@dotnet-policy-service agree [company="{your company}"]

Options:

  • (default - no company specified) I have sole ownership of intellectual property rights to my Submissions and I am not making Submissions in the course of work for my employer.
@dotnet-policy-service agree
  • (when company given) I am making Submissions in the course of work for my employer (or my employer has intellectual property rights in my Submissions by contract or applicable law). I have permission from my employer to make Submissions and enter into this Agreement on behalf of my employer. By signing below, the defined term “You” includes me and my employer.
@dotnet-policy-service agree company="Microsoft"

Contributor License Agreement

@dotnet-policy-service agree

Copy link
Copy Markdown
Member

@jfversluis jfversluis left a comment

Choose a reason for hiding this comment

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

Thanks for addressing the double-dismiss issue! 👍

One remaining thing: the lambda at line ~140 still has async but no longer contains any await:

CompletedHandler = async info =>
{
    GetFileResult(info, tcs, options);
}

This will trigger compiler warning CS1998 ("This async method lacks 'await' operators and will run synchronously"). Since the await picker.DismissViewControllerAsync(true) was removed, the async keyword should be removed too:

CompletedHandler = info =>
{
    GetFileResult(info, tcs, options);
}

@jfversluis
Copy link
Copy Markdown
Member

/azp run maui-pr

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(s).

jfversluis added a commit that referenced this pull request Feb 26, 2026
…ss handling

Three fixes for the dogfooding infrastructure:

1. **Workflow trigger**: Reverted dogfood-comment.yml from check_run to
   pull_request_target trigger. Azure DevOps check runs never populate the
   pull_requests[] array, so the check_run-based condition was always false
   and the workflow never ran.

2. **AzDO direct fallback**: Added Azure DevOps API as a fallback when
   GitHub Checks API doesn't return build info. Queries
   refs/pull/{PR}/merge branch directly to find completed builds.

3. **In-progress build detection**: Added detection for builds that are
   currently running. Shows a clear message telling users to wait instead
   of a generic 'no build found' error. Also warns when a newer build is
   in progress but older artifacts are available.

Additional improvements:
- Build ID validation (numeric check) to prevent URL injection
- PR number validation in bash script
- Protection against set -e crashes on invalid jq input
- Better error messages for all failure scenarios

Tested locally with multiple real PRs (#34216, #34250, #99999999) across
both scripts, verifying: successful builds, in-progress builds, error
handling, and input validation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jfversluis added a commit that referenced this pull request Feb 26, 2026
…ss handling (#34259)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

## Description

Three fixes for the dogfooding infrastructure that was merged in #33198
but never worked correctly:

### 1. Workflow trigger fix (`dogfood-comment.yml`)
The workflow was changed from `pull_request_target` to `check_run`
trigger in #33198, but Azure DevOps check runs **never populate the
`pull_requests[]` array**. This means
`github.event.check_run.pull_requests[0] != null` was **always false**,
and the workflow never ran.

**Fix**: Reverted to `pull_request_target` trigger with a comment
explaining why `check_run` does not work.

### 2. AzDO direct fallback (both scripts)
When the GitHub Checks API does not return build info (which can happen
for merge commits or in certain timing windows), the scripts now fall
back to querying the Azure DevOps API directly using
`refs/pull/{PR}/merge` branch name.

This approach is modeled after the working implementation in
[maui-version](https://github.com/jfversluis/maui-version).

### 3. In-progress build detection (both scripts)
When no completed build is found, the scripts now query AzDO for builds
with `status in ("inProgress", "notStarted", "postponed")`. Instead of
the generic "no build found" error, users now see:
- **Build in progress**: "A build is currently in progress. Please wait
for it to complete."
- **Stale artifacts**: When an older build has artifacts but a newer
build is running, warns users

### Additional improvements
- Build ID numeric validation to prevent URL injection
- PR number validation in bash script  
- Protection against `set -e` crashes on invalid `jq` input
- Better error messages for all failure scenarios

## Testing

Tested locally with multiple real PRs across both scripts:
- ✅ PR #34216 (completed, successful) - both scripts find build and
apply artifacts
- ✅ PR #34250 (in-progress build) - correctly finds existing artifacts
with warning
- ✅ PR #99999999 (non-existent) - proper error handling
- ✅ Invalid PR number ("abc") - input validation works
- ✅ Multi-model code review (Claude Sonnet 4.5, GPT-5.1, Gemini 3 Pro) -
all findings addressed

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jfversluis
jfversluis previously approved these changes Feb 26, 2026
@jfversluis jfversluis added this to the .NET 10 SR5 milestone Feb 26, 2026
@jfversluis jfversluis changed the base branch from main to inflight/current February 26, 2026 18:06
@jfversluis jfversluis changed the base branch from inflight/current to main February 26, 2026 18:10
@jfversluis jfversluis dismissed their stale review February 26, 2026 18:10

The base branch was changed.

@jfversluis jfversluis force-pushed the fix/mediapicker-dismiss-callback branch from 2c6b032 to 50ab7c3 Compare February 26, 2026 19:44
@jfversluis jfversluis changed the base branch from main to inflight/current February 26, 2026 19:44
@github-actions
Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34250

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34250"

@jfversluis jfversluis force-pushed the fix/mediapicker-dismiss-callback branch from 50ab7c3 to 2b3bc28 Compare February 26, 2026 19:45
@jfversluis jfversluis merged commit dd0e411 into dotnet:inflight/current Feb 26, 2026
2 of 3 checks passed
Tamilarasan-Paranthaman pushed a commit that referenced this pull request Mar 2, 2026
…ss handling (#34259)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

## Description

Three fixes for the dogfooding infrastructure that was merged in #33198
but never worked correctly:

### 1. Workflow trigger fix (`dogfood-comment.yml`)
The workflow was changed from `pull_request_target` to `check_run`
trigger in #33198, but Azure DevOps check runs **never populate the
`pull_requests[]` array**. This means
`github.event.check_run.pull_requests[0] != null` was **always false**,
and the workflow never ran.

**Fix**: Reverted to `pull_request_target` trigger with a comment
explaining why `check_run` does not work.

### 2. AzDO direct fallback (both scripts)
When the GitHub Checks API does not return build info (which can happen
for merge commits or in certain timing windows), the scripts now fall
back to querying the Azure DevOps API directly using
`refs/pull/{PR}/merge` branch name.

This approach is modeled after the working implementation in
[maui-version](https://github.com/jfversluis/maui-version).

### 3. In-progress build detection (both scripts)
When no completed build is found, the scripts now query AzDO for builds
with `status in ("inProgress", "notStarted", "postponed")`. Instead of
the generic "no build found" error, users now see:
- **Build in progress**: "A build is currently in progress. Please wait
for it to complete."
- **Stale artifacts**: When an older build has artifacts but a newer
build is running, warns users

### Additional improvements
- Build ID numeric validation to prevent URL injection
- PR number validation in bash script  
- Protection against `set -e` crashes on invalid `jq` input
- Better error messages for all failure scenarios

## Testing

Tested locally with multiple real PRs across both scripts:
- ✅ PR #34216 (completed, successful) - both scripts find build and
apply artifacts
- ✅ PR #34250 (in-progress build) - correctly finds existing artifacts
with warning
- ✅ PR #99999999 (non-existent) - proper error handling
- ✅ Invalid PR number ("abc") - input validation works
- ✅ Multi-model code review (Claude Sonnet 4.5, GPT-5.1, Gemini 3 Pro) -
all findings addressed

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jfversluis pushed a commit that referenced this pull request Mar 2, 2026
…troller (#34250)

### Description of Change

On iOS, `CompletedHandler` was being invoked **before**
`DismissViewController`
had finished animating. This means any code running in the handler —
such as
showing an alert, opening a modal, or triggering navigation — would fire
while
the picker view controller was still visible and attached to the view
hierarchy.

iOS UIKit requires the presenting view controller to be fully visible
and
attached before presenting any new UI (alerts, modals, sheets). If you
attempt
to present something while a dismiss animation is still in progress,
UIKit
silently drops the presentation or throws a warning, resulting in:

- `DisplayAlert` / `DisplayActionSheet` not appearing after picking an
image
- Custom modal dialogs not showing up
- Any `await`-based UI call after `PickPhotoAsync` / `PickVideoAsync`
silently failing on iOS, while working fine on Android

The root cause is that `DismissViewController(animated:completion:)`
accepts a
completion callback precisely for this purpose — to run logic **after**
the
animation finishes — but the original code passed `null` and called the
handler
immediately after, creating a race condition.

**Fix:** Moved all `CompletedHandler` invocations into the `completion`
callback
of `DismissViewController` across three call sites:

- `UIImagePickerControllerDelegate.FinishedPickingMedia`
- `UIImagePickerControllerDelegate.Canceled`
- `PHPickerViewControllerDelegate.DidFinishPicking`

### Issues Fixed

Fixes #21996

### Testing

Tested on a physical iOS device.

- Opened image picker via `MediaPicker.PickPhotoAsync()`
- After selecting an image, called `DisplayAlert` in the continuation
- **Before fix:** alert did not appear after dismissing the picker
- **After fix:** alert appears correctly after the picker is fully
dismissed
jfversluis pushed a commit that referenced this pull request Mar 2, 2026
…troller (#34250)

### Description of Change

On iOS, `CompletedHandler` was being invoked **before**
`DismissViewController`
had finished animating. This means any code running in the handler —
such as
showing an alert, opening a modal, or triggering navigation — would fire
while
the picker view controller was still visible and attached to the view
hierarchy.

iOS UIKit requires the presenting view controller to be fully visible
and
attached before presenting any new UI (alerts, modals, sheets). If you
attempt
to present something while a dismiss animation is still in progress,
UIKit
silently drops the presentation or throws a warning, resulting in:

- `DisplayAlert` / `DisplayActionSheet` not appearing after picking an
image
- Custom modal dialogs not showing up
- Any `await`-based UI call after `PickPhotoAsync` / `PickVideoAsync`
silently failing on iOS, while working fine on Android

The root cause is that `DismissViewController(animated:completion:)`
accepts a
completion callback precisely for this purpose — to run logic **after**
the
animation finishes — but the original code passed `null` and called the
handler
immediately after, creating a race condition.

**Fix:** Moved all `CompletedHandler` invocations into the `completion`
callback
of `DismissViewController` across three call sites:

- `UIImagePickerControllerDelegate.FinishedPickingMedia`
- `UIImagePickerControllerDelegate.Canceled`
- `PHPickerViewControllerDelegate.DidFinishPicking`

### Issues Fixed

Fixes #21996

### Testing

Tested on a physical iOS device.

- Opened image picker via `MediaPicker.PickPhotoAsync()`
- After selecting an image, called `DisplayAlert` in the continuation
- **Before fix:** alert did not appear after dismissing the picker
- **After fix:** alert appears correctly after the picker is fully
dismissed
github-actions bot pushed a commit that referenced this pull request Mar 3, 2026
…troller (#34250)

### Description of Change

On iOS, `CompletedHandler` was being invoked **before**
`DismissViewController`
had finished animating. This means any code running in the handler —
such as
showing an alert, opening a modal, or triggering navigation — would fire
while
the picker view controller was still visible and attached to the view
hierarchy.

iOS UIKit requires the presenting view controller to be fully visible
and
attached before presenting any new UI (alerts, modals, sheets). If you
attempt
to present something while a dismiss animation is still in progress,
UIKit
silently drops the presentation or throws a warning, resulting in:

- `DisplayAlert` / `DisplayActionSheet` not appearing after picking an
image
- Custom modal dialogs not showing up
- Any `await`-based UI call after `PickPhotoAsync` / `PickVideoAsync`
silently failing on iOS, while working fine on Android

The root cause is that `DismissViewController(animated:completion:)`
accepts a
completion callback precisely for this purpose — to run logic **after**
the
animation finishes — but the original code passed `null` and called the
handler
immediately after, creating a race condition.

**Fix:** Moved all `CompletedHandler` invocations into the `completion`
callback
of `DismissViewController` across three call sites:

- `UIImagePickerControllerDelegate.FinishedPickingMedia`
- `UIImagePickerControllerDelegate.Canceled`
- `PHPickerViewControllerDelegate.DidFinishPicking`

### Issues Fixed

Fixes #21996

### Testing

Tested on a physical iOS device.

- Opened image picker via `MediaPicker.PickPhotoAsync()`
- After selecting an image, called `DisplayAlert` in the continuation
- **Before fix:** alert did not appear after dismissing the picker
- **After fix:** alert appears correctly after the picker is fully
dismissed
HarishKumarSF4517 pushed a commit to HarishKumarSF4517/maui that referenced this pull request Mar 5, 2026
…troller (dotnet#34250)

### Description of Change

On iOS, `CompletedHandler` was being invoked **before**
`DismissViewController`
had finished animating. This means any code running in the handler —
such as
showing an alert, opening a modal, or triggering navigation — would fire
while
the picker view controller was still visible and attached to the view
hierarchy.

iOS UIKit requires the presenting view controller to be fully visible
and
attached before presenting any new UI (alerts, modals, sheets). If you
attempt
to present something while a dismiss animation is still in progress,
UIKit
silently drops the presentation or throws a warning, resulting in:

- `DisplayAlert` / `DisplayActionSheet` not appearing after picking an
image
- Custom modal dialogs not showing up
- Any `await`-based UI call after `PickPhotoAsync` / `PickVideoAsync`
silently failing on iOS, while working fine on Android

The root cause is that `DismissViewController(animated:completion:)`
accepts a
completion callback precisely for this purpose — to run logic **after**
the
animation finishes — but the original code passed `null` and called the
handler
immediately after, creating a race condition.

**Fix:** Moved all `CompletedHandler` invocations into the `completion`
callback
of `DismissViewController` across three call sites:

- `UIImagePickerControllerDelegate.FinishedPickingMedia`
- `UIImagePickerControllerDelegate.Canceled`
- `PHPickerViewControllerDelegate.DidFinishPicking`

### Issues Fixed

Fixes dotnet#21996

### Testing

Tested on a physical iOS device.

- Opened image picker via `MediaPicker.PickPhotoAsync()`
- After selecting an image, called `DisplayAlert` in the continuation
- **Before fix:** alert did not appear after dismissing the picker
- **After fix:** alert appears correctly after the picker is fully
dismissed
github-actions bot pushed a commit that referenced this pull request Mar 6, 2026
…troller (#34250)

### Description of Change

On iOS, `CompletedHandler` was being invoked **before**
`DismissViewController`
had finished animating. This means any code running in the handler —
such as
showing an alert, opening a modal, or triggering navigation — would fire
while
the picker view controller was still visible and attached to the view
hierarchy.

iOS UIKit requires the presenting view controller to be fully visible
and
attached before presenting any new UI (alerts, modals, sheets). If you
attempt
to present something while a dismiss animation is still in progress,
UIKit
silently drops the presentation or throws a warning, resulting in:

- `DisplayAlert` / `DisplayActionSheet` not appearing after picking an
image
- Custom modal dialogs not showing up
- Any `await`-based UI call after `PickPhotoAsync` / `PickVideoAsync`
silently failing on iOS, while working fine on Android

The root cause is that `DismissViewController(animated:completion:)`
accepts a
completion callback precisely for this purpose — to run logic **after**
the
animation finishes — but the original code passed `null` and called the
handler
immediately after, creating a race condition.

**Fix:** Moved all `CompletedHandler` invocations into the `completion`
callback
of `DismissViewController` across three call sites:

- `UIImagePickerControllerDelegate.FinishedPickingMedia`
- `UIImagePickerControllerDelegate.Canceled`
- `PHPickerViewControllerDelegate.DidFinishPicking`

### Issues Fixed

Fixes #21996

### Testing

Tested on a physical iOS device.

- Opened image picker via `MediaPicker.PickPhotoAsync()`
- After selecting an image, called `DisplayAlert` in the continuation
- **Before fix:** alert did not appear after dismissing the picker
- **After fix:** alert appears correctly after the picker is fully
dismissed
PureWeen added a commit that referenced this pull request Mar 11, 2026
## What's Coming

.NET MAUI inflight/candidate introduces significant improvements across
all platforms with focus on quality, performance, and developer
experience. This release includes 46 commits with various improvements,
bug fixes, and enhancements.


## Button
- [Android] Implemented material3 support for Button by @Dhivya-SF4094
in #33173
  <details>
  <summary>🔧 Fixes</summary>

- [Implement Material3 support for
Button](#33172)
  </details>

## CollectionView
- [Android] Fix RemainingItemsThresholdReachedCommand not firing when
CollectionView has Header and Footer both defined by @SuthiYuvaraj in
#29618
  <details>
  <summary>🔧 Fixes</summary>

- [Android : RemainingItemsThresholdReachedCommand not firing when
CollectionVew has Header and Footer both
defined](#29588)
  </details>

- [iOS/MacCatalyst] Fix CollectionView ScrollTo for horizontal layouts
by @Shalini-Ashokan in #33853
  <details>
  <summary>🔧 Fixes</summary>

- [[iOS/MacCatalyst] CollectionView ScrollTo does not work with
horizontal Layout](#33852)
  </details>

- [iOS & Mac] Fixed IndicatorView Size doesnt update dynamically by
@SubhikshaSf4851 in #31129
  <details>
  <summary>🔧 Fixes</summary>

- [[iOS, Catalyst] IndicatorView.IndicatorSize does not update
dynamically at runtime](#31064)
  </details>

- [Android] Fix for CollectionView Scrolled event is triggered on the
initial app load. by @BagavathiPerumal in
#33558
  <details>
  <summary>🔧 Fixes</summary>

- [[Android] CollectionView Scrolled event is triggered on the initial
app load.](#33333)
  </details>

- [iOS, Android] Fix for CollectionView IsEnabled=false allows touch
interactions by @praveenkumarkarunanithi in
#31403
  <details>
  <summary>🔧 Fixes</summary>

- [More issues with CollectionView IsEnabled, InputTransparent, Opacity
via Styles and code behind](#19771)
  </details>

- [iOS] Fix VerticalOffset Update When Modifying
CollectionView.ItemsSource While Scrolled by @devanathan-vaithiyanathan
in #34153
  <details>
  <summary>🔧 Fixes</summary>

- [[iOS]VerticalOffset Not Reset to Zero After Clearing ItemSource in
CollectionView](#26798)
  </details>

## DateTimePicker
- [Android] Fix DatePicker MinimumDate/MaximumDate not updating
dynamically by @HarishwaranVijayakumar in
#33687
  <details>
  <summary>🔧 Fixes</summary>

- [[regression/8.0.3] [Android] DatePicker control minimum date
issue](#19256)
- [[Android] DatePicker does not update MinimumDate / MaximumDate in the
Popup when set in the viewmodel after first
opening](#33583)
  </details>

## Drawing
- Android drawable perf by @albyrock87 in
#31567

## Editor
- [Android] Implemented material3 support for Editor by
@SyedAbdulAzeemSF4852 in #33478
  <details>
  <summary>🔧 Fixes</summary>

- [Implement Material3 Support for
Editor](#33476)
  </details>

## Entry
- [iOS, Mac] Fix for CursorPosition not updating when typing into Entry
control by @SyedAbdulAzeemSF4852 in
#30505
  <details>
  <summary>🔧 Fixes</summary>

- [Entry control CursorPosition does not update on TextChanged event
[iOS Maui 8.0.7] ](#20911)
- [CursorPosition not calculated correctly on behaviors events for iOS
devices](#32483)
  </details>

## Flyoutpage
- [Android, Windows] Fix for FlyoutPage toolbar button not updating on
orientation change by @praveenkumarkarunanithi in
#31962
  <details>
  <summary>🔧 Fixes</summary>

- [Flyout page in Android does not show flyout button (burger)
consistently](#24468)
  </details>

- Fix for First Item in CollectionView Overlaps in FlyoutPage.Flyout on
iOS by @praveenkumarkarunanithi in
#29265
  <details>
  <summary>🔧 Fixes</summary>

- [[iOS] CollectionView not rendering first item correctly in
FlyoutPage.Flyout](#29170)
  </details>

## Image
- [Android] Fix excessive memory usage for stream and resource-based
image loading by @Shalini-Ashokan in
#33590
  <details>
  <summary>🔧 Fixes</summary>

- [[Android] Unexpected high Bitmap.ByteCount when loading image via
ImageSource.FromResource() or ImageSource.FromStream() in .NET
MAUI](#33239)
  </details>

- [Android] Fix for Resize method returns an image that has already been
disposed by @SyedAbdulAzeemSF4852 in
#29964
  <details>
  <summary>🔧 Fixes</summary>

- [In GraphicsView, the Resize method returns an image that has already
been disposed](#29961)
- [IIMage.Resize bugged
behaviour](#31103)
  </details>

## Label
- Fixed Label Span font property inheritance when applied via Style by
@SubhikshaSf4851 in #34110
  <details>
  <summary>🔧 Fixes</summary>

- [`Span` does not inherit text styling from `Label` if that styling is
applied using `Style` ](#21326)
  </details>

- [Android] Implemented material3 support for Label by
@SyedAbdulAzeemSF4852 in #33599
  <details>
  <summary>🔧 Fixes</summary>

- [Implement Material3 Support for
Label](#33598)
  </details>

## Map
- [Android] Fix Circle Stroke color is incorrectly updated as Fill
color. by @NirmalKumarYuvaraj in
#33643
  <details>
  <summary>🔧 Fixes</summary>

- [[Android] Circle Stroke color is incorrectly updated as Fill
color.](#33642)
  </details>

## Mediapicker
- [iOS] Fix: invoke MediaPicker completion handler after
DismissViewController by @yuriikyry4enko in
#34250
  <details>
  <summary>🔧 Fixes</summary>

- [[iOS] Media Picker UIImagePickerController closing
issue](#21996)
  </details>

## Navigation
- Fix ContentPage memory leak on Android when using NavigationPage
modally (fixes #33918) by @brunck in
#34117
  <details>
  <summary>🔧 Fixes</summary>

- [[Android] Modal TabbedPage whose tabs are NavigationPage(ContentPage)
is retained after
PopModalAsync()](#33918)
  </details>

## Picker
- [Android] Implement material3 support for TimePicker by
@HarishwaranVijayakumar in #33646
  <details>
  <summary>🔧 Fixes</summary>

- [Implement Material3 support for
TimePicker](#33645)
  </details>

- [Android] Implemented Material3 support for Picker by
@SyedAbdulAzeemSF4852 in #33668
  <details>
  <summary>🔧 Fixes</summary>

- [Implement Material3 support for
Picker](#33665)
  </details>

## RadioButton
- [Android] Implemented material3 support for RadioButton by
@SyedAbdulAzeemSF4852 in #33468
  <details>
  <summary>🔧 Fixes</summary>

- [Implement Material3 Support for
RadioButton](#33467)
  </details>

## Setup
- Clarify MA003 error message by @jeremy-visionaid in
#34067
  <details>
  <summary>🔧 Fixes</summary>

- [MA003 false positive with
9.0.21](#26599)
  </details>

## Shell
- [Android] Fix TabBar FlowDirection not updating dynamically by
@SubhikshaSf4851 in #33091
  <details>
  <summary>🔧 Fixes</summary>

- [[Android, iOS] FlowDirection RTL is not updated dynamically on Shell
TabBar](#32993)
  </details>

- [Android] Fix page not disposed on Shell replace navigation by
@Vignesh-SF3580 in #33426
  <details>
  <summary>🔧 Fixes</summary>

- [[Android] [Shell] replace navigation leaks current
page](#25134)
  </details>

- [Android] Fixed Shell flyout does not disable scrolling when
FlyoutVerticalScrollMode is set to Disabled by @NanthiniMahalingam in
#32734
  <details>
  <summary>🔧 Fixes</summary>

- [[Android] Shell.FlyoutVerticalScrollMode="Disabled" does not disable
scrolling](#32477)
  </details>

## Single Project
- Fix: Throw a clear error when an SVG lacks dimensions instead of a
NullReferenceException by @Shalini-Ashokan in
#33194
  <details>
  <summary>🔧 Fixes</summary>

- [MAUI Fails To Convert Valid SVG Files Into PNG Files (Object
reference not set to an instance of an
object)](#32460)
  </details>

## SwipeView
- [iOS] Fix SwipeView stays open on iOS after updating content by
@devanathan-vaithiyanathan in #31248
  <details>
  <summary>🔧 Fixes</summary>

- [[iOS] - Swipeview with collectionview
issue](#19541)
  </details>

## TabbedPage
- [Windows] Fixed IsEnabled Property not works on Tabs by
@NirmalKumarYuvaraj in #26728
  <details>
  <summary>🔧 Fixes</summary>

- [ShellContent IsEnabledProperty does not
work](#5161)
- [[Windows] Shell Tab IsEnabled Not
Working](#32996)
  </details>

- [Android] Fix NavigationBar overlapping StatusBar when NavigationBar
visibility changes by @Vignesh-SF3580 in
#33359
  <details>
  <summary>🔧 Fixes</summary>

- [[Android] NavigationBar overlaps with StatusBar when mixing
HasNavigationBar=true/false in TabbedPage on Android 15 (API
35)](#33340)
  </details>

## Templates
- Fix for unable to open task using keyboard navigation on windows
platform by @SuthiYuvaraj in #33647
  <details>
  <summary>🔧 Fixes</summary>

- [Unable to open task using keyboard: A11y_.NET maui_User can get all
the insights of
Dashboard_Keyboard](#30787)
  </details>

## TitleView
- Fix for NavigationPage.TitleView does not expand with host window in
iPadOS 26+ by @SuthiYuvaraj in #33088

## Toolbar
- [iOS] Fix toolbar items ignoring BarTextColor on iOS/MacCatalyst 26+
by @Shalini-Ashokan in #34036
  <details>
  <summary>🔧 Fixes</summary>

- [[iOS 26] ToolbarItem color with custom BarTextColor not
working](#33970)
  </details>

- [Android] Fix for ToolbarItem retaining the icon from the previous
page on Android when using NavigationPage. by @BagavathiPerumal in
#32311
  <details>
  <summary>🔧 Fixes</summary>

- [Toolbaritem keeps the icon of the previous page on Android, using
NavigationPage (not shell)](#31727)
  </details>

## WebView
- [Android] Fix WebView in a grid expands beyond it's cell by
@devanathan-vaithiyanathan in #32145
  <details>
  <summary>🔧 Fixes</summary>

- [Android - WebView in a grid expands beyond it's
cell](#32030)
  </details>

## Xaml
- ContentPresenter: Propagate binding context to children with explicit
TemplateBinding by @HarishwaranVijayakumar in
#30880
  <details>
  <summary>🔧 Fixes</summary>

- [Binding context in
ContentPresenter](#23797)
  </details>


<details>
<summary>🔧 Infrastructure (1)</summary>

- [Revert] ContentPresenter: Propagate binding context to children with
explicit TemplateBinding by @Ahamed-Ali in
#34332

</details>

<details>
<summary>🧪 Testing (6)</summary>

- [Testing] Feature Matrix UITest Cases for Shell Flyout Page by
@NafeelaNazhir in #32525
- [Testing] Feature Matrix UITest Cases for Brushes by
@LogishaSelvarajSF4525 in #31833
- [Testing] Feature Matrix UITest Cases for BindableLayout by
@LogishaSelvarajSF4525 in #33108
- [Android] Add UI tests for Material 3 CheckBox by
@HarishwaranVijayakumar in #34126
  <details>
  <summary>🔧 Fixes</summary>

- [[Android] Add UI tests for Material 3
CheckBox](#34125)
  </details>
- [Testing] Feature Matrix UITest Cases for Shell Tabbed Page by
@NafeelaNazhir in #33159
- [Testing] Fixed Test case failure in PR 34294 - [03/2/2026] Candidate
- 1 by @TamilarasanSF4853 in #34334

</details>

<details>
<summary>📦 Other (2)</summary>

- Bumps Syncfusion.Maui.Toolkit dependency to version 1.0.9 by
@PaulAndersonS in #34178
- Fix crash when closing Windows based app when using TitleBar by
@MFinkBK in #34032
  <details>
  <summary>🔧 Fixes</summary>

- [Unhandled exception "Value does not fall within the expected range"
when closing Windows app](#32194)
  </details>

</details>
**Full Changelog**:
main...inflight/candidate
@github-actions github-actions bot locked and limited conversation to collaborators Mar 29, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[iOS] Media Picker UIImagePickerController closing issue

2 participants