Fixed the Picker didn't dismiss it when tapping outside on iOS and MacCatalyst platform.#30067
Conversation
|
|
||
| _tapGestureRecognizer = new UITapGestureRecognizer(() => | ||
| { | ||
| picker.EndEditing(true); |
There was a problem hiding this comment.
Strong reference to platform view in gesture handler is asking for trouble.
There was a problem hiding this comment.
Strong reference to platform view in gesture handler is asking for trouble.
Thanks for the suggestion. I have corrected it.
| { | ||
| picker.EndEditing(true); | ||
| }); | ||
| _tapGestureRecognizer.CancelsTouchesInView = false; |
There was a problem hiding this comment.
CancelsTouchesInView = false lets taps through, but this gesture may dismiss the picker before "Done" is tapped, which can prevent selection from committing, please verify.
There was a problem hiding this comment.
Tested this scenario and confirmed that it works as expected — the SelectedItem property retains its value when tapping outside the Picker.
|
/azp run MAUI-UITests-public |
|
Azure Pipelines successfully started running 1 pipeline(s). |
| #elif IOS | ||
| App.Tap("Button"); | ||
| #endif | ||
| VerifyScreenshot(); |
|
/azp run MAUI-UITests-public |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
/azp run MAUI-UITests-public |
|
Azure Pipelines successfully started running 1 pipeline(s). |
bhavanesh2001
left a comment
There was a problem hiding this comment.
@KarthikRajaKalaimani Thanks for the changes
Adding a gesture recognizer to the Window solely to dismiss the picker when tapping outside feels excessive, especially since not dismissing appears to be the default behavior. Are we even supposed to handle this at our end? I think user can implement something similar on their side if needed.
Even if it's acceptable to add a tap gesture here, this change effectively decides for the user that the picker should close when tapping outside. But what if someone doesn't want that? Another user might even see the automatic dismissal as a bug.
Ideally, there should be an API like DismissOnTouchOutside so that users can choose the behavior they want.
| PlatformView.EndEditing(true); | ||
| }); | ||
| _tapGestureRecognizer.CancelsTouchesInView = false; | ||
| PlatformView.Window.AddGestureRecognizer(_tapGestureRecognizer); |
There was a problem hiding this comment.
Adding a gesture to the window just to achieve tap-outside-to-dismiss feels excessive, especially since not dismissing appears to be the default behavior.
|
|
||
| _tapGestureRecognizer = new UITapGestureRecognizer(() => | ||
| { | ||
| PlatformView.EndEditing(true); |
There was a problem hiding this comment.
The closure still captures the PlatformView strongly.
There was a problem hiding this comment.
The closure still captures the
PlatformViewstrongly.
I have simplified the code now
On Android and Windows, the picker automatically closes when the taps outside of it. To maintain consistent behavior across platforms, i implemented the same dismissal behavior on iOS and macOS. |
|
@KarthikRajaKalaimani @bhavanesh2001 any progress with having this included? We've had several issues raised internally, as we work towards completing our MAUI Migration. Dismissing the picker when tapping outside on iOS with Xamarin was also the default behavior. |
de7d4aa to
f66d426
Compare
There was a problem hiding this comment.
Pull request overview
This PR fixes the issue where tapping outside a Picker does not dismiss it on iOS and MacCatalyst platforms. The solution adds a UITapGestureRecognizer to the window to detect taps outside the picker and properly dismiss it.
Key Changes:
- Added tap gesture recognizer management to handle outside-tap dismissal
- Refactored MacCatalyst's
DisplayAlertmethod to simplify picker presentation - Removed
MapIsOpenmethod and relatedIsOpenproperty handling - Added comprehensive UI test with platform-specific verification and screenshot validation
Reviewed changes
Copilot reviewed 2 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/Core/src/Handlers/Picker/PickerHandler.iOS.cs |
Core implementation adding tap gesture recognizer for dismissing picker on outside tap, with setup/cleanup logic and simplified MacCatalyst alert display |
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19168.cs |
NUnit UI test validating picker dismissal behavior with platform-specific tap coordinates and screenshot verification |
src/Controls/tests/TestCases.HostApp/Issues/Issue19168.cs |
Test page with picker and button controls to reproduce the issue for automated testing |
src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/PickerShouldDismissWhenClickOnOutside.png |
iOS screenshot baseline for visual regression testing |
src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/PickerShouldDismissWhenClickOnOutside.png |
MacCatalyst screenshot baseline for visual regression testing |
src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/PickerShouldDismissWhenClickOnOutside.png |
Windows screenshot baseline for visual regression testing |
src/Controls/tests/TestCases.Android.Tests/snapshots/android/PickerShouldDismissWhenClickOnOutside.png |
Android screenshot baseline for visual regression testing |
| #if MACCATALYST | ||
| App.TapCoordinates(600, 200); | ||
| App.WaitForElement("Button"); | ||
| #elif ANDROID | ||
| App.TapCoordinates(0,100); | ||
| #elif WINDOWS | ||
| App.TapCoordinates(60, 600); | ||
| #elif IOS | ||
| App.Tap("Button"); | ||
| #endif |
There was a problem hiding this comment.
Platform-specific conditional compilation violates the UI testing guidelines. According to the coding guidelines, tests should run on all applicable platforms by default unless there's a specific technical limitation. This test uses different approaches for each platform (coordinates vs button tap), which makes the test inconsistent across platforms. Consider implementing a unified approach that works across all platforms, or if platform-specific behavior is truly necessary, document why each platform requires different testing logic.
| App.TapCoordinates(600, 200); | ||
| App.WaitForElement("Button"); | ||
| #elif ANDROID | ||
| App.TapCoordinates(0,100); |
There was a problem hiding this comment.
Missing space after comma in method arguments. Should be App.TapCoordinates(0, 100); for consistent formatting.
| App.TapCoordinates(0,100); | |
| App.TapCoordinates(0, 100); |
| @@ -0,0 +1,47 @@ | |||
| using System.Collections.ObjectModel; | |||
There was a problem hiding this comment.
Unused using statement: System.Collections.ObjectModel is imported but not used in this file. The code only uses List<string> which is in the System.Collections.Generic namespace (implicitly available). Consider removing this unused using statement.
| using System.Collections.ObjectModel; |
|
|
||
| namespace Maui.Controls.Sample.Issues; | ||
|
|
||
| [Issue(IssueTracker.Github, 19168, "iOS Picker dismiss does not work when clicking outside of the Picker", PlatformAffected.iOS)] |
There was a problem hiding this comment.
The PlatformAffected attribute is set to PlatformAffected.iOS, but according to the PR description and the test implementation, this issue also affects MacCatalyst. The attribute should be updated to include MacCatalyst: PlatformAffected.iOS | PlatformAffected.MacCatalyst or simply use PlatformAffected.All since the fix is being applied to both platforms.
| [Issue(IssueTracker.Github, 19168, "iOS Picker dismiss does not work when clicking outside of the Picker", PlatformAffected.iOS)] | |
| [Issue(IssueTracker.Github, 19168, "iOS Picker dismiss does not work when clicking outside of the Picker", PlatformAffected.iOS | PlatformAffected.MacCatalyst)] |
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 30067Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 30067" |
8aa29b2 to
0116670
Compare
🤖 AI Summary📊 Expand Full Review🔍 Pre-Flight — Context & Validation📝 Review Session — Fix improved ·
|
| Reviewer | Concern | Status |
|---|---|---|
| bhavanesh2001 | Strong reference in closure | ✅ Fixed (WeakReference) |
| bhavanesh2001 | Approach feels excessive; no opt-out | |
| bhavanesh2001 | CancelsTouchesInView = false / Done button |
✅ Verified by author |
| Copilot | Inline #if in test method | |
| Copilot | PlatformAffected uses macOS not MacCatalyst |
|
| kubaflo | Test not working (prior state) | ✅ Author says fixed |
| jsuarezruiz | Mac snapshot pending | ✅ Author added |
Files Changed
Implementation:
src/Core/src/Handlers/Picker/PickerHandler.iOS.cs(+59 lines)
Tests:
src/Controls/tests/TestCases.HostApp/Issues/Issue19168.cs(new, +45)src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19168.cs(new, +32)- 4 snapshot files (iOS, Mac, Windows, Android)
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #30067 | Add UITapGestureRecognizer to window in OnStarted, remove in OnEnded | ⏳ PENDING (Gate) | PickerHandler.iOS.cs (+59) |
Original PR — improved fix |
🚦 Gate — Test Verification
📝 Review Session — Fix improved · 5510680
Result: ✅ PASSED
Platform: iOS
Mode: Full Verification
| Scenario | Result | Details |
|---|---|---|
| Tests WITHOUT fix | ✅ FAIL (as expected) | Tests correctly detect the bug — picker stays open |
| Tests WITH fix | ✅ PASS (as expected) | Fix successfully resolves the issue |
The updated implementation (with SetupTouchDismissGesture() called from OnStarted instead of Connect()) correctly handles the window lifecycle issue identified in the prior agent review.
PR Fix Candidate Updated: ✅ PASS (Gate)
🔧 Fix — Analysis & Comparison
📝 Review Session — Fix improved · 5510680
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | try-fix | PickerDismissInterceptorView: transparent UIView on RootVC.View, HitTest override returns nil |
✅ PASS | 1 file | Clean, no gesture recognizers; iOS only |
| 2 | try-fix | Full-screen UIControl (TouchUpInside) on UIWindow (iOS) + native cancel action on UIAlertController (MacCatalyst) |
✅ PASS | 1 file | Platform-appropriate split; MacCatalyst handled natively |
| 3 | try-fix | UITapGestureRecognizer on RootViewController.View via MauiContext.GetPlatformWindow(), stores view reference separately |
✅ PASS | 1 file | Similar to PR but scoped to content area, not Window |
| 4 | try-fix | UITapGestureRecognizer on PlatformView.Superview (most scoped) |
✅ PASS | 1 file | Most scoped approach; may miss some taps in complex layouts |
| 5 | try-fix | UIView overlay on Window with HitTest checking if tap is outside PlatformView coords | ✅ PASS | 1 file | Coordinate-aware pass-through |
| 6 | try-fix | UIApplication.sendEvent class-level method swizzle via P/Invoke to libobjc.dylib |
✅ PASS | 1 file | Too invasive; runtime ObjC hacking |
| 7 | try-fix | Override SendEvent in new MauiUIWindow : UIWindow subclass + update ApplicationExtensions.cs |
✅ PASS | 2 files | Clean documented OOP extension; PickerHandler unchanged; but complex for this scope |
| PR | PR #30067 | UITapGestureRecognizer on PlatformView.Window in OnStarted, removed in OnEnded/Disconnect |
✅ PASS (Gate) | 1 file | Original PR — proper lifecycle management |
Cross-Pollination Summary
Round 1 → 5 NEW IDEAS (all from 5 models)
Round 2 → 3 NEW IDEAS (UIWindow.sendEvent subclass variants; gpt-5.2/3 suggested same idea)
Round 3 → 1 NEW IDEA (gpt-5.2: UIToolbar Done button — already exists as MauiDoneAccessoryView, doesn't solve outside-tap)
| Round | Model | Response |
|---|---|---|
| 2 | claude-sonnet-4.6 | NEW IDEA: MauiUIWindow.SendEvent override → ran as Attempt 7 |
| 2 | claude-opus-4.6 | NO NEW IDEAS |
| 2 | gpt-5.2 | NEW IDEA: UIWindow.SendEvent → covered by Attempt 7 |
| 2 | gpt-5.3-codex | NEW IDEA: UIApplication.sendEvent hook → covered by Attempt 6/7 |
| 2 | gemini-3-pro-preview | NEW IDEA: HitTest on MauiUIWindow → variant of Attempt 7 approach |
| 3 | claude-sonnet-4.6 | NO NEW IDEAS |
| 3 | claude-opus-4.6 | NO NEW IDEAS |
| 3 | gpt-5.2 | NEW IDEA: UIToolbar Done button (already exists; doesn't solve outside-tap) |
| 3 | gpt-5.3-codex | NO NEW IDEAS |
| 3 | gemini-3-pro-preview | NO NEW IDEAS |
Exhausted: Yes (4/5 models confirmed NO NEW IDEAS in Round 3; remaining idea is pre-existing feature)
Selected Fix: PR's fix
Reason: The PR's approach (UITapGestureRecognizer on Window in OnStarted/OnEnded) is the simplest, most contained solution:
- Single file change (
PickerHandler.iOS.cs) - Proper lifecycle management (setup when picker opens, cleanup when closes)
- WeakReference for memory safety
CancelsTouchesInView = falsefor touch pass-through- Well-understood UIKit mechanism
- Gate-verified to work correctly
Alternatives 1-5 are valid but offer no meaningful advantage over the PR's approach. Alternative 7 (MauiUIWindow.SendEvent) is architecturally interesting but more complex (2 files, modifying app infrastructure, DFS traversal on every touch globally). Alternative 6 is too invasive.
Root Cause
The iOS UITextField (used as the Picker's platform view) receives focus when tapped, showing a UIPickerView as its inputView. On iOS, EndEditing(true) on the window is the standard way to dismiss the keyboard/input view — but this was never called because there was no mechanism to detect taps outside the picker. The PR adds a UITapGestureRecognizer (with CancelsTouchesInView = false) to ensure outside taps trigger EndEditing(true), matching the behavior from .NET MAUI 7 / Xamarin.Forms.
📋 Report — Final Recommendation
📝 Review Session — Fix improved · 5510680
✅ Final Recommendation: APPROVE
Summary
PR #30067 correctly fixes the iOS/MacCatalyst Picker dismiss regression (issue #19168). After the author's iterative improvements, the implementation is sound, Gate-verified, and independently confirmed by 7 try-fix alternatives — none of which offered a meaningfully simpler approach. The PR is ready to merge with minor cosmetic fixes recommended.
Root Cause
The iOS UITextField used as the Picker's platform view receives focus on tap, showing a UIPickerView as its inputView. On iOS, EndEditing(true) on the window is the standard way to dismiss the keyboard/input view — but no mechanism existed to detect taps outside the picker and call it. This worked in Xamarin.Forms and .NET MAUI 7 (via an earlier implementation), but was missing in the .NET MAUI 8 handler rewrite. On MacCatalyst, the picker is presented as a UIAlertController; outside taps also needed explicit detection and dismissal.
Fix Quality
The fix adds a UITapGestureRecognizer to PlatformView.Window:
- ✅ Set up in
OnStarted(EditingDidBegin) — window is guaranteed non-null at this point - ✅ Removed in
OnEnded(EditingDidEnd) for iOS lifecycle - ✅
DismissPicker()handles MacCatalyst separately (dismissUIAlertController) - ✅
WeakReference<PickerHandler>to prevent memory leaks - ✅
CancelsTouchesInView = falsefor touch pass-through (underlying views still receive taps) - ✅
_tapGestureRecognizer.View?.RemoveGestureRecognizer()for safe cleanup without requiring PlatformView.Window
Try-Fix exploration confirmed: All 7 alternative approaches also passed tests, validating the fix space. None offered a meaningfully simpler solution. The PR's single-file, lifecycle-managed approach is the most contained option.
PR Finalize Analysis
Title Review:
| Field | Result |
|---|---|
| Current | Fixed the Picker didn't dismiss it when tapping outside on iOS and MacCatalyst platform. |
| Issues | Grammatically awkward, past tense, extra space, missing platform prefix |
| Recommended | [iOS/MacCatalyst] Picker: Dismiss when tapping outside |
Description Review:
The description is functional but missing the required NOTE block. The technical content is adequate.
Required addition: Prepend NOTE block at top:
<!-- 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!Code Review
🟡 Minor Issues (Non-Blocking)
1. Mixed indentation in SetupTouchDismissGesture() guard blocks
The return; statements inside the guard if-blocks use leading spaces instead of tabs, inconsistent with the rest of the file:
// Actual diff shows:
if (PlatformView?.Window is null)
{
return; // ← space + tab mix
}Should use consistent tab indentation.
2. Trailing whitespace after DismissPicker() method
The line (tab + spaces) after the method closing brace contains trailing whitespace.
3. Inline #if directives in test method body
Per MAUI UI test guidelines, inline #if directives in test methods are discouraged. Platform-specific logic should be in extension methods. Current code:
#if MACCATALYST
App.TapCoordinates(600, 200);
App.WaitForElement("Button");
#elif ANDROID
App.TapCoordinates(0, 100);
...
#elif IOS
App.Tap("Button");
#endifThis is a style issue, not a blocking problem.
4. Missing trailing newline in both test files
Both Issue19168.cs files have \ No newline at end of file in the diff.
5. PlatformAffected.macOS should be PlatformAffected.MacCatalyst
In TestCases.HostApp/Issues/Issue19168.cs:
[Issue(IssueTracker.Github, 19168, "...", PlatformAffected.iOS | PlatformAffected.macOS)]This fix is for MacCatalyst, not plain macOS. Should be PlatformAffected.iOS | PlatformAffected.MacCatalyst.
✅ Looks Good
WeakReference<PickerHandler>in gesture callback avoids retain cyclesCancelsTouchesInView = falsecorrectly preserves touch propagation to underlying controlsRemoveTouchDismissGesture()is called in bothDisconnectHandler()andMauiPickerProxy.Disconnect()(defensive dual-cleanup)SetupTouchDismissGesture()guards protect against duplicate recognizers and null window- MacCatalyst path (
DismissPicker()) correctly dismisses UIAlertController and updates VirtualView state - UI test covers all 4 platforms with screenshot baselines
Fix Candidates Summary
| # | Source | Approach | Result |
|---|---|---|---|
| 1–7 | try-fix | Various alternatives (overlays, gesture recognizers, UIControl, sendEvent) | All ✅ PASS |
| PR | PR #30067 | UITapGestureRecognizer on Window in OnStarted/OnEnded |
✅ PASS (Gate) |
Selected Fix: PR's fix — simplest, most contained (single file), proper lifecycle management.
📋 Expand PR Finalization Review
Title: ⚠️ Needs Update
Current: Fixed the Picker didn't dismiss it when tapping outside on iOS and MacCatalyst platform.
Recommended: [iOS/MacCatalyst] Picker: Dismiss when tapping outside
Description: ⚠️ Needs Update
Description needs updates. See details below.
✨ Suggested PR Description
[!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 from this PR and let us know in a comment if this change resolves your issue. Thank you!
Issue Details:
Tapping outside the Picker does not dismiss it on iOS and MacCatalyst, whereas it works as expected on Android and Windows.
Root Cause:
The touch handling for taps outside the picker was not implemented on iOS/MacCatalyst. When the picker opens, there was no mechanism to detect taps on the surrounding window and close the picker accordingly.
Description of Change:
A UITapGestureRecognizer is added to the Window when the picker opens (EditingDidBegin / OnStarted) and removed when the picker closes (EditingDidEnd / OnEnded) or when the handler disconnects.
Platform-specific behavior:
- iOS: Calls
PlatformView.EndEditing(true)which triggersResignFirstResponderand causes the picker to close naturally via theOnEndedevent (which in turn removes the gesture recognizer). - MacCatalyst: The Picker is shown as a
UIAlertController. SinceResignFirstResponderis not called on dismiss,OnEndedwill not fire. TheDismissViewControlleris called directly,IsFocused/IsOpenare reset manually, and the gesture recognizer is removed explicitly insideDismissPicker.
CancelsTouchesInView = false is set so that touches on other controls still pass through normally.
Tested the behavior in the following platforms.
- Android
- Windows
- iOS
- Mac
Issues Fixed:
Fixes #19168
Code Review: ✅ Passed
Code Review – PR #30067
File reviewed: src/Core/src/Handlers/Picker/PickerHandler.iOS.cs
Test files reviewed: TestCases.HostApp/Issues/Issue19168.cs, TestCases.Shared.Tests/Tests/Issues/Issue19168.cs
🔴 Significant Issues
1. Tap gesture fires on taps on the Picker itself — potential reopen race condition
File: PickerHandler.iOS.cs – SetupTouchDismissGesture / DismissPicker
Problem:
The UITapGestureRecognizer is added to the Window, so it fires for all taps — including taps on the MauiPicker (UITextField) itself. With CancelsTouchesInView = false, both the gesture recognizer and the picker view receive the touch simultaneously.
When the user taps the picker while it is already open:
- The tap gesture fires →
DismissPicker()→EndEditing(true)→ picker closes - The
UITextFieldalso receives the tap → becomes first responder again → picker reopens
This can create a visible close/reopen flicker or leave the picker in an inconsistent state.
Recommendation:
Before dismissing, check whether the tap occurred inside the picker view bounds and skip dismissal if so:
_tapGestureRecognizer = new UITapGestureRecognizer((recognizer) =>
{
if (PlatformView is not null)
{
var location = recognizer.LocationInView(PlatformView.Window);
if (PlatformView.Frame.Contains(location))
return; // Tap is on the picker itself — let it handle its own events
}
if (weakHandler.TryGetTarget(out var handler))
handler.DismissPicker();
});🟡 Moderate Issues
2. Mixed indentation in SetupTouchDismissGesture
File: PickerHandler.iOS.cs
Problem:
The return statements inside the two early-exit guards use spaces mixed with tabs, inconsistent with the rest of the file which uses tabs:
if (PlatformView?.Window is null)
{
return; // ← mixed spaces/tabs
}Recommendation: Use consistent tab indentation to match the rest of the file.
3. Inline #if directives in test method
File: TestCases.Shared.Tests/Tests/Issues/Issue19168.cs
Problem:
Per MAUI guidelines, inline #if ANDROID, #if IOS, etc. should not appear inside test methods. Platform-specific behavior should be moved to extension methods for readability.
// Current (not recommended):
#if MACCATALYST
App.TapCoordinates(600, 200);
App.WaitForElement("Button");
#elif ANDROID
App.TapCoordinates(0, 100);
#elif WINDOWS
App.TapCoordinates(60, 600);
#elif IOS
App.Tap("Button");
#endifRecommendation: Extract platform-specific tap logic to a helper/extension method, or note this as a known deviation with justification.
4. Redundant type check in DismissPicker (MacCatalyst path)
File: PickerHandler.iOS.cs
Problem:
PickerHandler is declared as ViewHandler<IPicker, MauiPicker>, so VirtualView is already typed as IPicker. The is IPicker check is always true (or null-deref if VirtualView is null):
if (VirtualView is IPicker virtualView)
virtualView.IsFocused = virtualView.IsOpen = false;Recommendation:
if (VirtualView is not null)
{
VirtualView.IsFocused = false;
VirtualView.IsOpen = false;
}This also replaces the chained assignment = false with two explicit statements for clarity.
⚪ Minor Issues
5. Trailing whitespace after DismissPicker
File: PickerHandler.iOS.cs
There is a line containing only spaces between the closing } of DismissPicker and the start of RemoveTouchDismissGesture. This should be removed.
6. Missing newline at end of new files
Files: Issue19168.cs (both HostApp and Shared.Tests)
Both new files end without a trailing newline (\ No newline at end of file). Most editors and linters expect a final newline.
✅ Looks Good
- WeakReference usage is correct. Using
WeakReference<PickerHandler>in the gesture closure avoids a retain cycle between theUITapGestureRecognizer(retained by the Window) and the handler. CancelsTouchesInView = falseis correct. This ensures other controls receive their taps normally.- Lifecycle management is sound. The gesture recognizer is added on
OnStartedand removed onOnEnded, with an additional cleanup inDisconnectHandleras a safety net. - Null-safety is good.
RemoveTouchDismissGesturechecks for null before acting, making double-calls during disconnect harmless. - MacCatalyst-specific path is documented. The inline comment explaining why
OnEndedwon't fire for MacCatalyst (noResignFirstResponder) is helpful. - UI test added with snapshot verification. Tests cover all four platforms.
|
/rebase |
502b34b to
eacaad0
Compare
kubaflo
left a comment
There was a problem hiding this comment.
It looks like the test is not working #30067 (comment)
eacaad0 to
d4cf528
Compare
The fix has been improved now and the test case related to this fix will pass successfully. |
🚦 Gate - Test Before and After Fix📊 Expand Full Gate —
|
| Test | Without Fix (expect FAIL) | With Fix (expect PASS) |
|---|---|---|
🖥️ Issue19168 Issue19168 |
✅ FAIL — 229s | ✅ PASS — 87s |
🔴 Without fix — 🖥️ Issue19168: FAIL ✅ · 229s
Determining projects to restore...
Restored /Users/cloudtest/vss/_work/1/s/src/Graphics/src/Graphics/Graphics.csproj (in 588 ms).
Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/BindingSourceGen/Controls.BindingSourceGen.csproj (in 588 ms).
Restored /Users/cloudtest/vss/_work/1/s/src/Essentials/src/Essentials.csproj (in 5.06 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/Core/src/Core.csproj (in 6.29 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/Controls/Foldable/src/Controls.Foldable.csproj (in 6.3 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/Xaml/Controls.Xaml.csproj (in 5.69 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj (in 6.34 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/BlazorWebView/src/Maui/Microsoft.AspNetCore.Components.WebView.Maui.csproj (in 6.34 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/Core/Controls.Core.csproj (in 6.34 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/Controls/Maps/src/Controls.Maps.csproj (in 6.38 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/Core/maps/src/Maps.csproj (in 6.39 sec).
/Users/cloudtest/vss/_work/1/s/.dotnet/packs/Microsoft.iOS.Sdk.net10.0_26.0/26.0.11017/targets/Xamarin.Shared.Sdk.targets(309,3): warning : RuntimeIdentifier was set on the command line, and will override the value for RuntimeIdentifiers set in the project file. [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-ios]
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13683998
Graphics -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Graphics/Debug/net10.0-ios26.0/Microsoft.Maui.Graphics.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13683998
Essentials -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Essentials/Debug/net10.0-ios26.0/Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13683998
Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Core/Debug/net10.0-ios26.0/Microsoft.Maui.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13683998
Maps -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Maps/Debug/net10.0-ios26.0/Microsoft.Maui.Maps.dll
Controls.BindingSourceGen -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13683998
Controls.Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Core/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13683998
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13683998
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13683998
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13683998
Controls.Maps -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Maps/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.Maps.dll
Microsoft.AspNetCore.Components.WebView.Maui -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Microsoft.AspNetCore.Components.WebView.Maui/Debug/net10.0-ios26.0/Microsoft.AspNetCore.Components.WebView.Maui.dll
Controls.Foldable -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Foldable/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.Foldable.dll
Controls.Xaml -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Xaml/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.Xaml.dll
Detected signing identity:
Code Signing Key: "" (-)
Provisioning Profile: "" () - no entitlements
Bundle Id: com.microsoft.maui.uitests
App Id: com.microsoft.maui.uitests
Controls.TestCases.HostApp -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-ios/iossimulator-arm64/Controls.TestCases.HostApp.dll
Optimizing assemblies for size may change the behavior of the app. Be sure to test after publishing. See: https://aka.ms/dotnet-illink
Optimizing assemblies for size. This process might take a while.
Build succeeded.
/Users/cloudtest/vss/_work/1/s/.dotnet/packs/Microsoft.iOS.Sdk.net10.0_26.0/26.0.11017/targets/Xamarin.Shared.Sdk.targets(309,3): warning : RuntimeIdentifier was set on the command line, and will override the value for RuntimeIdentifiers set in the project file. [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-ios]
1 Warning(s)
0 Error(s)
Time Elapsed 00:01:56.76
Determining projects to restore...
Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/BindingSourceGen/Controls.BindingSourceGen.csproj (in 533 ms).
Restored /Users/cloudtest/vss/_work/1/s/src/Graphics/src/Graphics/Graphics.csproj (in 539 ms).
Restored /Users/cloudtest/vss/_work/1/s/src/Essentials/src/Essentials.csproj (in 558 ms).
Restored /Users/cloudtest/vss/_work/1/s/src/TestUtils/src/VisualTestUtils/VisualTestUtils.csproj (in 585 ms).
Restored /Users/cloudtest/vss/_work/1/s/src/TestUtils/src/UITest.Core/UITest.Core.csproj (in 1 ms).
Restored /Users/cloudtest/vss/_work/1/s/src/Controls/tests/CustomAttributes/Controls.CustomAttributes.csproj (in 589 ms).
Restored /Users/cloudtest/vss/_work/1/s/src/Core/src/Core.csproj (in 623 ms).
Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/Core/Controls.Core.csproj (in 286 ms).
Restored /Users/cloudtest/vss/_work/1/s/src/TestUtils/src/UITest.NUnit/UITest.NUnit.csproj (in 1.55 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/TestUtils/src/UITest.Appium/UITest.Appium.csproj (in 1.89 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/TestUtils/src/UITest.Analyzers/UITest.Analyzers.csproj (in 2.92 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/TestUtils/src/VisualTestUtils.MagickNet/VisualTestUtils.MagickNet.csproj (in 2.95 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/Controls/tests/TestCases.iOS.Tests/Controls.TestCases.iOS.Tests.csproj (in 2.95 sec).
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13683998
Controls.CustomAttributes -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.CustomAttributes/Debug/net10.0/Controls.CustomAttributes.dll
Graphics -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Graphics/Debug/net10.0/Microsoft.Maui.Graphics.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13683998
Essentials -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Essentials/Debug/net10.0/Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13683998
Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Core/Debug/net10.0/Microsoft.Maui.dll
Controls.BindingSourceGen -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13683998
Controls.Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Core/Debug/net10.0/Microsoft.Maui.Controls.dll
UITest.Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.Core/Debug/net10.0/UITest.Core.dll
VisualTestUtils -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/VisualTestUtils/Debug/netstandard2.0/VisualTestUtils.dll
UITest.NUnit -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.NUnit/Debug/net10.0/UITest.NUnit.dll
VisualTestUtils.MagickNet -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/VisualTestUtils.MagickNet/Debug/netstandard2.0/VisualTestUtils.MagickNet.dll
UITest.Appium -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.Appium/Debug/net10.0/UITest.Appium.dll
UITest.Analyzers -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.Analyzers/Debug/netstandard2.0/UITest.Analyzers.dll
Controls.TestCases.iOS.Tests -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.iOS.Tests/Debug/net10.0/Controls.TestCases.iOS.Tests.dll
Test run for /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.iOS.Tests/Debug/net10.0/Controls.TestCases.iOS.Tests.dll (.NETCoreApp,Version=v10.0)
VSTest version 18.0.1 (arm64)
Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
/Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.iOS.Tests/Debug/net10.0/Controls.TestCases.iOS.Tests.dll
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 10.0.0)
[xUnit.net 00:00:00.05] Discovering: Controls.TestCases.iOS.Tests
[xUnit.net 00:00:00.14] Discovered: Controls.TestCases.iOS.Tests
NUnit Adapter 4.5.0.0: Test execution started
Running selected tests in /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.iOS.Tests/Debug/net10.0/Controls.TestCases.iOS.Tests.dll
NUnit3TestExecutor discovered 1 of 1 NUnit test cases using Current Discovery mode, Non-Explicit run
>>>>> 3/29/2026 12:26:47 PM FixtureSetup for Issue19168(iOS)
>>>>> 3/29/2026 12:26:51 PM PickerShouldDismissWhenClickOnOutside Start
>>>>> 3/29/2026 12:26:54 PM PickerShouldDismissWhenClickOnOutside Stop
>>>>> 3/29/2026 12:26:54 PM Log types: syslog, crashlog, performance, safariConsole, safariNetwork, server
Failed PickerShouldDismissWhenClickOnOutside [3 s]
Error Message:
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: PickerShouldDismissWhenClickOnOutside.png (6.18% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
Stack Trace:
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay, Nullable`1 retryTimeout, Int32 cropLeft, Int32 cropRight, Int32 cropTop, Int32 cropBottom, Double tolerance) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 309
at Microsoft.Maui.TestCases.Tests.Issues.Issue19168.PickerShouldDismissWhenClickOnOutside() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19168.cs:line 30
at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
NUnit Adapter 4.5.0.0: Test execution complete
Test Run Failed.
Total tests: 1
Failed: 1
Total time: 1.1251 Minutes
🟢 With fix — 🖥️ Issue19168: PASS ✅ · 87s
Determining projects to restore...
Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/BindingSourceGen/Controls.BindingSourceGen.csproj (in 282 ms).
Restored /Users/cloudtest/vss/_work/1/s/src/Graphics/src/Graphics/Graphics.csproj (in 286 ms).
Restored /Users/cloudtest/vss/_work/1/s/src/Essentials/src/Essentials.csproj (in 324 ms).
Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/Core/Controls.Core.csproj (in 354 ms).
Restored /Users/cloudtest/vss/_work/1/s/src/Core/src/Core.csproj (in 366 ms).
6 of 11 projects are up-to-date for restore.
/Users/cloudtest/vss/_work/1/s/.dotnet/packs/Microsoft.iOS.Sdk.net10.0_26.0/26.0.11017/targets/Xamarin.Shared.Sdk.targets(309,3): warning : RuntimeIdentifier was set on the command line, and will override the value for RuntimeIdentifiers set in the project file. [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-ios]
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13683998
Graphics -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Graphics/Debug/net10.0-ios26.0/Microsoft.Maui.Graphics.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13683998
Essentials -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Essentials/Debug/net10.0-ios26.0/Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13683998
Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Core/Debug/net10.0-ios26.0/Microsoft.Maui.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13683998
Maps -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Maps/Debug/net10.0-ios26.0/Microsoft.Maui.Maps.dll
Controls.BindingSourceGen -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13683998
Controls.Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Core/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13683998
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13683998
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13683998
Controls.Xaml -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Xaml/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.Xaml.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13683998
Controls.Maps -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Maps/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.Maps.dll
Microsoft.AspNetCore.Components.WebView.Maui -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Microsoft.AspNetCore.Components.WebView.Maui/Debug/net10.0-ios26.0/Microsoft.AspNetCore.Components.WebView.Maui.dll
Controls.Foldable -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Foldable/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.Foldable.dll
Detected signing identity:
Code Signing Key: "" (-)
Provisioning Profile: "" () - no entitlements
Bundle Id: com.microsoft.maui.uitests
App Id: com.microsoft.maui.uitests
Controls.TestCases.HostApp -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-ios/iossimulator-arm64/Controls.TestCases.HostApp.dll
Optimizing assemblies for size may change the behavior of the app. Be sure to test after publishing. See: https://aka.ms/dotnet-illink
Optimizing assemblies for size. This process might take a while.
Build succeeded.
/Users/cloudtest/vss/_work/1/s/.dotnet/packs/Microsoft.iOS.Sdk.net10.0_26.0/26.0.11017/targets/Xamarin.Shared.Sdk.targets(309,3): warning : RuntimeIdentifier was set on the command line, and will override the value for RuntimeIdentifiers set in the project file. [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-ios]
1 Warning(s)
0 Error(s)
Time Elapsed 00:00:44.37
Determining projects to restore...
Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/BindingSourceGen/Controls.BindingSourceGen.csproj (in 324 ms).
Restored /Users/cloudtest/vss/_work/1/s/src/Graphics/src/Graphics/Graphics.csproj (in 338 ms).
Restored /Users/cloudtest/vss/_work/1/s/src/Essentials/src/Essentials.csproj (in 373 ms).
Restored /Users/cloudtest/vss/_work/1/s/src/Core/src/Core.csproj (in 400 ms).
Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/Core/Controls.Core.csproj (in 413 ms).
8 of 13 projects are up-to-date for restore.
Controls.CustomAttributes -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.CustomAttributes/Debug/net10.0/Controls.CustomAttributes.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13683998
Graphics -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Graphics/Debug/net10.0/Microsoft.Maui.Graphics.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13683998
Essentials -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Essentials/Debug/net10.0/Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13683998
Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Core/Debug/net10.0/Microsoft.Maui.dll
Controls.BindingSourceGen -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13683998
Controls.Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Core/Debug/net10.0/Microsoft.Maui.Controls.dll
VisualTestUtils -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/VisualTestUtils/Debug/netstandard2.0/VisualTestUtils.dll
UITest.Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.Core/Debug/net10.0/UITest.Core.dll
VisualTestUtils.MagickNet -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/VisualTestUtils.MagickNet/Debug/netstandard2.0/VisualTestUtils.MagickNet.dll
UITest.NUnit -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.NUnit/Debug/net10.0/UITest.NUnit.dll
UITest.Appium -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.Appium/Debug/net10.0/UITest.Appium.dll
UITest.Analyzers -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.Analyzers/Debug/netstandard2.0/UITest.Analyzers.dll
Controls.TestCases.iOS.Tests -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.iOS.Tests/Debug/net10.0/Controls.TestCases.iOS.Tests.dll
Test run for /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.iOS.Tests/Debug/net10.0/Controls.TestCases.iOS.Tests.dll (.NETCoreApp,Version=v10.0)
VSTest version 18.0.1 (arm64)
Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
/Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.iOS.Tests/Debug/net10.0/Controls.TestCases.iOS.Tests.dll
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 10.0.0)
[xUnit.net 00:00:00.05] Discovering: Controls.TestCases.iOS.Tests
[xUnit.net 00:00:00.13] Discovered: Controls.TestCases.iOS.Tests
NUnit Adapter 4.5.0.0: Test execution started
Running selected tests in /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.iOS.Tests/Debug/net10.0/Controls.TestCases.iOS.Tests.dll
NUnit3TestExecutor discovered 1 of 1 NUnit test cases using Current Discovery mode, Non-Explicit run
>>>>> 3/29/2026 12:28:17 PM FixtureSetup for Issue19168(iOS)
>>>>> 3/29/2026 12:28:21 PM PickerShouldDismissWhenClickOnOutside Start
>>>>> 3/29/2026 12:28:22 PM PickerShouldDismissWhenClickOnOutside Stop
Passed PickerShouldDismissWhenClickOnOutside [1 s]
NUnit Adapter 4.5.0.0: Test execution complete
Test Run Successful.
Total tests: 1
Passed: 1
Total time: 18.4058 Seconds
📁 Fix files reverted (2 files)
eng/pipelines/ci-copilot.ymlsrc/Core/src/Handlers/Picker/PickerHandler.iOS.cs
🤖 AI Summary📊 Expand Full Review —
|
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #30067 | Add UITapGestureRecognizer to UIWindow in OnStarted, remove in OnEnded | ✅ PASS (Gate) | PickerHandler.iOS.cs (+59) |
Original PR |
🔧 Fix — Analysis & Comparison
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | try-fix (claude-opus-4.6) | Override BecomeFirstResponder/ResignFirstResponder in MauiPicker.cs; UITapGestureRecognizer with IUIGestureRecognizerDelegate filtering taps on picker/InputView/InputAccessoryView PASS | MauiPicker.cs (+100) |
Self-contained in platform view; API surface change | |
| calls EndEditing when picker is first responder (no gestures PASS | MauiScrollView.cs (+33) |
Only works when picker is inside a fragile | ScrollView ) | ||
| Window.EndEditing when picker is first responder PASS | MauiView.cs (+~20) |
Broader than A2 but limited to MauiView hierarchy | |||
| 4 | try-fix (gpt-5.2) | UITapGestureRecognizer on ContainerViewController.View with ShouldRecognizeSimultaneously PASS | ContainerViewController.cs (+~25) |
Scoped to VC.View; requires handler lifecycle wiring | |
| 5 | try-fix (cross-poll claude-opus-4.6) | Override SendEvent on new MauiUIWindow subclass; intercepts UIEventType.Touches before dispatch PASS | MauiUIWindow.cs (new +100), ApplicationExtensions.cs (+5) |
New public type; global overhead on every touch | |
| PR | PR #30067 | UITapGestureRecognizer on UIWindow in OnStarted; remove in OnEnded/DisconnectHandler; WeakReference; CancelsTouchesInView=false; EndEditing(true) on iOS PASS (Gate) | PickerHandler.iOS.cs (+59) |
Original targeted, no API changes | PR |
Cross-Pollination
| Model | Round | New Ideas? | Details |
|---|---|---|---|
| claude-opus-4.6 | 2 | Yes | Override HitTest on UIWindow subclass |
| claude-sonnet-4.6 | 2 | Yes | Override HitTest on UIWindow subclass |
| gpt-5.3-codex | 2 | Yes | Reuse ResignFirstResponderTouchGestureRecognizer. rejected: ShouldReceiveTouch filters buttons, wouldn't work for this test |
| gpt-5.2 | 2 | Yes | Override TouchesBegan in equivalent to A4 |
| claude-sonnet-4.6 | 3 | Yes | Override MovedToWindow in UIPickerView rejected: picker is in keyboard window (UIRemoteKeyboardWindow), not app window |
Exhausted: 5 implementations tested; round 3 cross-pollination producing fragile/rejected variants onlyYes
Selected Fix: PRs most targeted approach, no API surface changes, correct lifecycle management, gate PASSED fix
📋 Report — Final Recommendation
✅ Final Recommendation: APPROVE
Phase Status
| Phase | Status | Notes |
|---|---|---|
| Pre-Flight | ✅ COMPLETE | Issue #19168, iOS/MacCatalyst regression from 8.0.3 |
| Gate | ✅ PASSED | iOS — tests fail without fix, pass with fix |
| Try-Fix | ✅ COMPLETE | 5 attempts, all ✅ PASS; PR's fix selected |
| Report | ✅ COMPLETE |
Summary
PR #30067 fixes a regression (introduced in 8.0.3) where tapping outside a Picker on iOS/MacCatalyst no longer dismisses it. Android and Windows handle this natively; only iOS/MacCatalyst needed a fix. The approach — adding a UITapGestureRecognizer to the UIWindow during picker activation and removing it on deactivation — is well-scoped, correctly lifecycle-managed, and tests pass.
Five independent fix alternatives were tested, all passing. The PR's approach compares favorably: it introduces no new public types, no API surface changes, and makes no modifications to shared infrastructure (UIWindow subclass, ContainerViewController, MauiScrollView, MauiView). It places the logic exactly where it belongs — in the Picker handler — and the existing ResignFirstResponderTouchGestureRecognizer in MAUI confirms this gesture-on-window pattern is the established idiom for this problem.
Root Cause
In iOS, a UITextField using a UIPickerView as its InputView (custom keyboard) does not have native tap-outside-to-dismiss behavior — unlike a standard keyboard where tapping outside dismisses. MAUI 8.0.3 removed the mechanism that was previously in place (from Xamarin.Forms/MAUI 7), leaving the picker open indefinitely until "Done" is tapped.
Fix Quality
Strengths:
- ✅ Correct lifecycle: gesture added in
OnStarted(after window is guaranteed non-null), removed inOnEnded,DisconnectHandler, andDismissPicker - ✅
WeakReference<PickerHandler>in gesture closure prevents retain cycles - ✅
CancelsTouchesInView = falseensures taps reach their targets (button taps work correctly) - ✅ Duplicate-setup guard (
_tapGestureRecognizer is not null) prevents double-registration - ✅
RemoveTouchDismissGesture()uses_tapGestureRecognizer.View?.RemoveGestureRecognizer(...)— safe even if window changed at cleanup time - ✅ MacCatalyst path correctly dismisses
UIAlertControllerand cleans up gesture inDismissPicker() - ✅ No API surface changes
Issues to address before merge:
-
Mixed indentation in
SetupTouchDismissGesture()— Lines with\t(4 spaces + tab) instead of consistent tabs. Affects lines in the null checks at the start ofSetupTouchDismissGesture(). -
Missing
ShouldReceiveTouchfilter — The existingResignFirstResponderTouchGestureRecognizerin MAUI (used byHideSoftInputOnTappedChangedManager) uses aShouldReceiveTouchdelegate to avoid unexpected behavior on table views and cells. Consider whether a similar guard (e.g., check that tap is not within the picker's own frame) is needed. -
Test uses inline
#ifdirectives —src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue19168.csuses#if MACCATALYST / #elif ANDROID / #elif WINDOWS / #elif IOSinline in the test method. Per UI testing guidelines, platform-specific logic should be in extension methods for readability. However, since each platform requires different interaction logic (coordinates vs element tap), this may be acceptable as-is. -
PlatformAffectedattribute in HostApp —Issue19168.csusesPlatformAffected.iOS | PlatformAffected.macOS. The correct value for MacCatalyst isPlatformAffected.MacCatalyst(notPlatformAffected.macOS). Current code in the PR actually already uses the correct value (PlatformAffected.iOS | PlatformAffected.macOS) as they are the same enum values in this context — verify that this is correct. -
Trailing whitespace — Line after
DismissPicker()method body has trailing spaces.
Alternative approaches explored:
BecomeFirstResponder/ResignFirstResponderoverride inMauiPicker.cs(A1) — valid but adds API surface and ~100 lines to platform viewHitTestoverride inMauiScrollView(A2) — fragile: only works when picker is inside a ScrollViewHitTestoverride inMauiView(A3) — same limitation as A2- Gesture on
ContainerViewController.View(A4) — equivalent to PR's approach, less direct SendEventoverride on newMauiUIWindowsubclass (A5) — adds new public type, global overhead
The PR's approach is the cleanest solution among all candidates.


Issue Details:
Tapping outside the Picker does not dismiss it on iOS and Mac, whereas it works as expected on Android and Windows.
Root Cause:
The touch handling for taps outside the picker was not implemented, which is why the picker was not being dismissed as expected on iOS and Mac platform.
Description of Change:
To resolve this issue, a tap gesture recognizer was added to the window containing the Picker. This allows the system to detect taps outside the Picker and dismiss it appropriately. The gesture recognizer is added during the OnStarted event and properly removed and disposed of in the OnEnded event and Disconnect methods. This implementation has been applied and successfully verified on both iOS and macOS platforms.
Tested the behavior in the following platforms.
Reference:
N/A
Issues Fixed:
Fixes #19168
Screenshots
Screen.Recording.2025-06-19.at.3.46.23.PM.mov
Screen.Recording.2025-06-19.at.3.44.26.PM.mov