[iOS] MauiView: Round SafeArea insets to pixels to fix infinite layout cycle#34024
[iOS] MauiView: Round SafeArea insets to pixels to fix infinite layout cycle#34024
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes an iOS infinite layout cycle that occurs when nested views both have SafeAreaEdges = Container by implementing a generation counter mechanism to track SafeArea change events.
Changes:
- Implemented a generation counter system using a static counter incremented on genuine SafeArea changes and per-view tracking to prevent re-invalidation within the same generation
- Added test cases for nested SafeArea views with animations (Issue32586) and runtime SafeAreaEdges toggling (Issue33595)
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/Core/src/Platform/iOS/MauiView.cs | Added generation counter fields and logic to break infinite layout cycles in LayoutSubviews |
| src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33595.cs | UI test verifying navigation with padding and ScrollView doesn't crash |
| src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32586.cs | UI tests for TranslateToAsync animation and runtime SafeAreaEdges toggling |
| src/Controls/tests/TestCases.HostApp/Issues/Issue33595.cs | Test page with Grid containing ScrollView that previously caused freezing |
| src/Controls/tests/TestCases.HostApp/Issues/Issue32586.cs | Test page with nested SafeArea views and animation triggers |
| // Global counter incremented each time any MauiView receives SafeAreaInsetsDidChange. | ||
| // Used to detect when a ValidateSafeArea "changed" result is due to the same safe area | ||
| // event being re-processed (infinite cycle) vs. a genuinely new safe area change. | ||
| static int s_safeAreaGeneration; |
There was a problem hiding this comment.
The static counter s_safeAreaGeneration is accessed from multiple threads (read in LayoutSubviews() at line 565 and incremented atomically in SafeAreaInsetsDidChange() at line 730), but the read operation is not atomic. Consider using Interlocked.Read() or Volatile.Read() when accessing this counter to ensure thread-safe reads.
| static int s_safeAreaGeneration; | |
| static volatile int s_safeAreaGeneration; |
Test Results ✅Successfully fixed the infinite layout cycle! The fix now passes all tests: Issue32586 Test (the freeze repro)
All SafeAreaEdges Tests
How the Fix WorksThe root cause was that The fix uses a global generation counter that increments each time any view invalidates ancestors from This preserves correct SafeArea behavior while breaking the infinite loop. Bonus: Test Infrastructure ImprovementsAlso added resilience fixes to prevent tests from hanging indefinitely when apps freeze:
These ensure tests fail gracefully instead of hanging, making it much easier to debug layout issues like this one. |
|
/rebase |
…tion counter When both a parent and child view respond to SafeArea (both have SafeAreaEdges = Container), an infinite layout cycle occurs: each view's layout change triggers the other to re-validate, creating an endless loop that freezes the app. Root cause: Both views apply SafeArea padding independently. Each layout pass invalidates ancestors, which triggers another layout pass, ad infinitum. Previous approach (PR #32797): Check if parent handles SafeArea via hierarchy walk. Problem: This silences ALL child SafeArea edges when parent handles ANY edge, breaking nested SafeArea scenarios where parent handles top and child independently handles bottom. This fix: Use a generation counter incremented on genuine SafeAreaInsetsDidChange events. Each view tracks the last generation where it invalidated ancestors. If already invalidated for current generation, skip re-invalidation and just arrange. Benefits: - Breaks the cycle: within a single SafeArea change event, each view only invalidates once - Preserves correct behavior: both parent and child can still apply their own SafeArea edges - Simpler logic: no hierarchy walking, just atomic counter check Test cases (Issue32586, Issue33595): - Nested Grid + VerticalStackLayout with SafeArea=Container - TranslateToAsync animation that previously triggered infinite cycle - Runtime SafeAreaEdges toggling Fixes #32586 Fixes #33595
When an app enters an infinite loop (e.g., layout cycle), WDA (WebDriverAgent) blocks forever waiting for the main thread to become idle. This causes Appium commands to hang indefinitely, which in turn hangs the entire test run. Changes: **HelperExtensions.cs:** - Add RunWithTimeout wrapper (45s hard limit) around all Appium commands - Wrap Tap(), Click(), and Wait() query calls with timeout - Uses CancellationTokenSource for proper cleanup hygiene - Logs warning about orphaned threads when timeout occurs - Throws clear TimeoutException when app is unresponsive - Uses Task.Run + GetAwaiter().GetResult() to properly unwrap exceptions **AppiumLifecycleActions.cs:** - Add ForceCloseApp command using OS-level termination - iOS: xcrun simctl terminate - Android: adb shell am force-stop - Mac: osascript quit - Wrap CloseApp with 15s timeout, auto-falls back to ForceCloseApp - Bypasses WDA when normal Appium termination hangs **UITestBase.cs:** - Catch TimeoutException for unresponsive apps in TearDown - Force-terminate and reset session when app freezes - Allows subsequent tests to continue instead of hanging forever Result: Tests now fail gracefully after 45s with clear error message instead of hanging indefinitely when app freezes.
- Added 5-minute process-level timeout to BuildAndRunHostApp.ps1 PowerShell script using Start-Job + Wait-Job pattern - Wrapped GetText(), GetAttribute(), GetRect(), IsSelected() property getters with RunWithTimeout in HelperExtensions.cs - Wrapped App.AppState query and SaveUIDiagnosticInfo with Task.Run + timeout in UITestBase.UITestBaseTearDown - Fixed TimeoutException handling to properly propagate to unresponsive app handler These changes ensure UI tests fail gracefully when the app becomes unresponsive (e.g., infinite layout loop) instead of hanging indefinitely. The script-level timeout is the ultimate safety net that kills the test process if it runs too long. Fixes test hangs for issues #32586 and #33595 SafeArea tests.
When both parent and child views respond to SafeArea changes (both have SafeAreaEdges=Container), an infinite layout cycle can occur where each view's layout invalidation triggers the other to re-layout indefinitely. Root cause: InvalidateMeasure() sets _safeAreaInvalidated=true on ancestor views, causing ValidateSafeArea() to run again even when safe area values haven't changed. Each LayoutSubviews calls InvalidateAncestorsMeasures, which propagates back up and triggers another cycle. Fix: Use a global generation counter incremented each time any view invalidates ancestors from LayoutSubviews. Each view tracks the generation at which it last invalidated. If asked to layout again at the same generation (meaning we're in a cascade/cycle), skip the invalidation. This allows both parent and child to respond to safe area correctly on genuine safe area changes, while breaking the cycle when they trigger each other's layout repeatedly. Fixes #32586 Fixes #33595
42adf00 to
ba9c901
Compare
|
/azp run maui-pr-uitests, maui-pr-devicetests |
|
Azure Pipelines successfully started running 2 pipeline(s). |
…n oscillation fix Cherry-picked from PR #34024 (ios-safearea-infinite-layout-fix): - MauiView.cs: generation counter to break parent↔child safe area cycles - MauiView.cs: global rate limiter for safe area invalidation cascades - Tests: Issue32586, Issue33595, Issue33934 (safe area layout cycle tests) Additional fix for Issue33934 animation-driven layout oscillation: - InvalidateMeasure(isPropagating: true) no longer sets _safeAreaInvalidated - A descendant changing size/transform doesn't affect system safe area insets - This prevented spurious safe area revalidation during TranslateToAsync animations, which caused measurement oscillation (±2px) and infinite SizeChanged → cancel animation → restart cycles Validated locally: - Issue33934: was infinite loop, now completes in ≤2 iterations ✅ - Issue33595: simple layout cycle fix ✅ - Issue32586: safe area layout ✅ - Issue18896: existing safe area test ✅ - Issue33458: another safe area test ✅ - SafeAreaEdges category: all 28 tests pass ✅ Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…s guard iOS fires SafeAreaInsetsDidChange when a view's position changes during animations, not just for genuine system safe area changes. Per-view SafeAreaInsets reflect position relative to safe area boundaries, so they change whenever the view moves — causing measurement oscillation and infinite layout cycles when combined with SizeChanged handlers that restart animations (see #33934). Fix uses two complementary mechanisms: 1. SafeAreaInsetsDidChange: Compare Window's safe area insets (which only change for genuine system events like keyboard/rotation) instead of per-view insets. Blocks animation-driven noise. 2. InvalidateMeasure: When propagating from descendants, only set _safeAreaInvalidated if the global safe area generation advanced (meaning a parent's safe area path ran). This ensures descendants pick up runtime SafeAreaEdges changes while staying inactive during animation cycles (where no safe area path runs). Fixes #33934 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove MauiView.cs safe area changes, Issue33934/32586/33595 test files, and ViewModelBase changes from this PR. These belong in PR #34024 (ios-safearea-infinite-layout-fix) which is the dedicated safe area fix PR. This PR now focuses solely on UITest resilience infrastructure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When an app enters an infinite loop (e.g., layout cycle), WDA (WebDriverAgent) blocks forever waiting for the main thread to become idle. This causes Appium commands to hang indefinitely, which in turn hangs the entire test run. Changes: **HelperExtensions.cs:** - Add RunWithTimeout wrapper (45s hard limit) around all Appium commands - Wrap Tap(), Click(), and Wait() query calls with timeout - Uses CancellationTokenSource for proper cleanup hygiene - Logs warning about orphaned threads when timeout occurs - Throws clear TimeoutException when app is unresponsive - Uses Task.Run + GetAwaiter().GetResult() to properly unwrap exceptions **AppiumLifecycleActions.cs:** - Add ForceCloseApp command using OS-level termination - iOS: xcrun simctl terminate - Android: adb shell am force-stop - Mac: osascript quit - Wrap CloseApp with 15s timeout, auto-falls back to ForceCloseApp - Bypasses WDA when normal Appium termination hangs **UITestBase.cs:** - Catch TimeoutException for unresponsive apps in TearDown - Force-terminate and reset session when app freezes - Allows subsequent tests to continue instead of hanging forever Result: Tests now fail gracefully after 45s with clear error message instead of hanging indefinitely when app freezes.
Add robust checks for external process execution and improve timeout/error handling. - AppiumLifecycleActions: verify process is non-null, WaitForExit succeeded, and ExitCode == 0 for xcrun/adb/osascript calls; log failures and return FailedEmptyResponse instead of assuming success. - HelperExtensions: attach a continuation to orphaned Appium tasks (OnlyOnFaulted) to observe and log future exceptions, preventing unobserved task exceptions. - UITestBase: explicitly rethrow TimeoutException from AppState probe so unresponsive-app timeouts bubble to the outer handler, and simplify the outer TimeoutException catch to handle timeout cases consistently. These changes prevent false-success when command-line helpers fail, surface background task faults, and ensure unresponsive app timeouts are handled reliably.
- Fix Wait() timeout composition: inner RunWithTimeout now respects caller timeout - macOS ForceCloseApp: use force-terminate instead of graceful osascript quit - Add Windows ForceCloseApp support via taskkill - Terminate orphaned processes when WaitForExit times out - Add diagnostic logging for iOS null UDID - Wrap GetText/GetAttribute/GetRect/IsSelected in RunWithTimeout - Wrap App.AppState and SaveUIDiagnosticInfo in timeouts - Add regression tests for infinite layout cycle (Issue32586, Issue33595) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- iOS 18.x: prefer iPhone Xs (matches CI UITest.cs default) - iOS 26.x: prefer iPhone 11 Pro (matches CI visual test requirement) - Fallback to newer Pro models if preferred device unavailable Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Some tests (e.g., RefreshView) leave the app briefly busy after the test body completes. The 15s AppState timeout was triggering false positives. Now retries once after a 5s delay before declaring the app frozen. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tests using small timeouts (e.g., 2s) were failing because CapTimeout was limiting inner RunWithTimeout calls to the caller's timeout. Each individual Appium command needs the full AppiumCommandTimeout to complete normally. CapTimeout now only caps when the caller's total timeout exceeds AppiumCommandTimeout, preventing multiple wasteful 45s iterations for frozen apps while letting short-timeout tests work as before. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…n oscillation fix Cherry-picked from PR #34024 (ios-safearea-infinite-layout-fix): - MauiView.cs: generation counter to break parent↔child safe area cycles - MauiView.cs: global rate limiter for safe area invalidation cascades - Tests: Issue32586, Issue33595, Issue33934 (safe area layout cycle tests) Additional fix for Issue33934 animation-driven layout oscillation: - InvalidateMeasure(isPropagating: true) no longer sets _safeAreaInvalidated - A descendant changing size/transform doesn't affect system safe area insets - This prevented spurious safe area revalidation during TranslateToAsync animations, which caused measurement oscillation (±2px) and infinite SizeChanged → cancel animation → restart cycles Validated locally: - Issue33934: was infinite loop, now completes in ≤2 iterations ✅ - Issue33595: simple layout cycle fix ✅ - Issue32586: safe area layout ✅ - Issue18896: existing safe area test ✅ - Issue33458: another safe area test ✅ - SafeAreaEdges category: all 28 tests pass ✅ Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove MaxSafeAreaInvalidationsPerEvent static rate limiter and replace with a proper root-cause fix for animation-driven safe area oscillation. The rate limiter was a blunt instrument that capped total safe area invalidations and only reset on keyboard/window events, which could permanently block legitimate safe area changes. The new approach uses two complementary mechanisms: 1. SafeAreaInsetsDidChange: Compare Window's safe area insets instead of per-view insets. Per-view SafeAreaInsets change whenever a view moves (including during TranslateToAsync animations), but Window insets only change for genuine system events (keyboard, rotation, status bar). This prevents animation-driven measurement oscillation. 2. InvalidateMeasure: When a propagating invalidation arrives and the global safe area generation has advanced, re-evaluate safe area. This ensures descendants pick up changes when a parent modifies SafeAreaEdges at runtime, since the parent's safe area path increments the generation counter. Together these eliminate the infinite layout cycle in Issue33934 while preserving runtime SafeAreaEdges changes (Issue32586) and all existing safe area behavior. Fixes #33934 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove MauiView.cs safe area changes, Issue33934/32586/33595 test files, and ViewModelBase changes from this PR. These belong in PR #34024 (ios-safearea-infinite-layout-fix) which is the dedicated safe area fix PR. This PR now focuses solely on UITest resilience infrastructure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add robust checks for external process execution and improve timeout/error handling. - AppiumLifecycleActions: verify process is non-null, WaitForExit succeeded, and ExitCode == 0 for xcrun/adb/osascript calls; log failures and return FailedEmptyResponse instead of assuming success. - HelperExtensions: attach a continuation to orphaned Appium tasks (OnlyOnFaulted) to observe and log future exceptions, preventing unobserved task exceptions. - UITestBase: explicitly rethrow TimeoutException from AppState probe so unresponsive-app timeouts bubble to the outer handler, and simplify the outer TimeoutException catch to handle timeout cases consistently. These changes prevent false-success when command-line helpers fail, surface background task faults, and ensure unresponsive app timeouts are handled reliably.
- Fix Wait() timeout composition: inner RunWithTimeout now respects caller timeout - macOS ForceCloseApp: use force-terminate instead of graceful osascript quit - Add Windows ForceCloseApp support via taskkill - Terminate orphaned processes when WaitForExit times out - Add diagnostic logging for iOS null UDID - Wrap GetText/GetAttribute/GetRect/IsSelected in RunWithTimeout - Wrap App.AppState and SaveUIDiagnosticInfo in timeouts - Add regression tests for infinite layout cycle (Issue32586, Issue33595) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tests using small timeouts (e.g., 2s) were failing because CapTimeout was limiting inner RunWithTimeout calls to the caller's timeout. Each individual Appium command needs the full AppiumCommandTimeout to complete normally. CapTimeout now only caps when the caller's total timeout exceeds AppiumCommandTimeout, preventing multiple wasteful 45s iterations for frozen apps while letting short-timeout tests work as before. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The 2s timeout in VerifyCarouselViewBindingAndRendering was never honestly enforced before — it silently got 45s. With CapTimeout now enforcing it, the test fails. Fix the test to use an honest 45s timeout instead of weakening CapTimeout. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…n oscillation fix Cherry-picked from PR #34024 (ios-safearea-infinite-layout-fix): - MauiView.cs: generation counter to break parent↔child safe area cycles - MauiView.cs: global rate limiter for safe area invalidation cascades - Tests: Issue32586, Issue33595, Issue33934 (safe area layout cycle tests) Additional fix for Issue33934 animation-driven layout oscillation: - InvalidateMeasure(isPropagating: true) no longer sets _safeAreaInvalidated - A descendant changing size/transform doesn't affect system safe area insets - This prevented spurious safe area revalidation during TranslateToAsync animations, which caused measurement oscillation (±2px) and infinite SizeChanged → cancel animation → restart cycles Validated locally: - Issue33934: was infinite loop, now completes in ≤2 iterations ✅ - Issue33595: simple layout cycle fix ✅ - Issue32586: safe area layout ✅ - Issue18896: existing safe area test ✅ - Issue33458: another safe area test ✅ - SafeAreaEdges category: all 28 tests pass ✅ Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove MaxSafeAreaInvalidationsPerEvent static rate limiter and replace with a proper root-cause fix for animation-driven safe area oscillation. The rate limiter was a blunt instrument that capped total safe area invalidations and only reset on keyboard/window events, which could permanently block legitimate safe area changes. The new approach uses two complementary mechanisms: 1. SafeAreaInsetsDidChange: Compare Window's safe area insets instead of per-view insets. Per-view SafeAreaInsets change whenever a view moves (including during TranslateToAsync animations), but Window insets only change for genuine system events (keyboard, rotation, status bar). This prevents animation-driven measurement oscillation. 2. InvalidateMeasure: When a propagating invalidation arrives and the global safe area generation has advanced, re-evaluate safe area. This ensures descendants pick up changes when a parent modifies SafeAreaEdges at runtime, since the parent's safe area path increments the generation counter. Together these eliminate the infinite layout cycle in Issue33934 while preserving runtime SafeAreaEdges changes (Issue32586) and all existing safe area behavior. Fixes #33934 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove MauiView.cs safe area changes, Issue33934/32586/33595 test files, and ViewModelBase changes from this PR. These belong in PR #34024 (ios-safearea-infinite-layout-fix) which is the dedicated safe area fix PR. This PR now focuses solely on UITest resilience infrastructure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sort matching runtimes descending so iOS-18-5 is preferred over iOS-18-3 when both have an iPhone Xs available. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…te-layout-fix # Conflicts: # src/TestUtils/src/UITest.Appium/Actions/AppiumLifecycleActions.cs # src/TestUtils/src/UITest.Appium/HelperExtensions.cs # src/TestUtils/src/UITest.NUnit/UITestBase.cs
Replace the static global s_safeAreaGeneration counter with a per-view _hasInvalidatedAncestorsForSafeArea boolean flag. The global counter was shared across all MauiView instances which caused cross-window and cross-hierarchy interference in multi-window scenarios (iPad split view, multiple independent view hierarchies). The per-view flag: - Set in LayoutSubviews when a view invalidates ancestors from the safe area path, preventing re-entry (breaks parent/child cycles) - Cleared on every genuine safe area event: window insets change, keyboard show/hide, MovedToWindow, direct InvalidateMeasure iterates direct child MauiViews to set their _safeAreaInvalidated and clear their flags, ensuring descendants re-evaluate This gives complete multi-window isolation with no global state while preserving all existing safe area behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
dedicated InvalidateDescendantSafeAreas() method called only from MapSafeAreaEdges. This avoids O(N) child iterations on every property change (BackgroundColor, IsVisible, etc.) — the iteration now only runs when SafeAreaEdges actually changes. The recursive descent also walks through non-MauiView intermediaries (e.g., WrapperView) to reach grandchildren that the previous direct-children-only loop would miss. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the previous 3-layer approach (Window-level guard + isPropagating guard + cycle-break flag + MapSafeAreaEdges descendant propagation) with a single surgical fix: round _safeArea to the nearest device pixel when storing in ValidateSafeArea. This fixes the root cause — SafeAreaPadding uses exact double equality, but per-view SafeAreaInsets oscillate by sub-pixel amounts during TranslateToAsync animations. Rounding absorbs the noise so the comparison returns true, preventing InvalidateAncestorsMeasures from firing and breaking the infinite layout cycle. Also stabilizes constraints fed to CrossPlatformMeasure, preventing per-frame jitter from floating-point noise in _safeArea values. Fixes #33934 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tScaleFactor - Use ContentScaleFactor instead of Window.Screen.Scale for safer scale access - Apply rounding fix to MauiScrollView to prevent latent cycle - Remove Task.Delay from Issue32586 test for reliability Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/azp run |
|
Azure Pipelines successfully started running 3 pipeline(s). |
|
Alright @Tamilarasan-Paranthaman @sheiksyedm Let me know what you think of this fix |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/azp run |
|
Azure Pipelines successfully started running 3 pipeline(s). |
Issue32586: - Add VerifyFooterPositionRespectsSafeArea: verifies footer extends into bottom safe area when SafeAreaEdges=None (tester reported footer always rendered above safe area regardless of setting) - Rename VerifyLayoutWithTranslateToAsync -> VerifyFooterAnimationCompletes with clearer assertion messages - Add BottomMarker and MainGrid AutomationId to HostApp for position checks Issue33934: - Move from Navigation to SafeAreaEdges category (correct categorization) - Remove async Task.Delay pattern, use synchronous WaitForElement with clear timeout (15s) that directly proves whether animation loop exits - Improve assertion messages to describe the infinite loop behavior Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The previous test compared footer positions relative to each other (>=), which passed even when both positions were wrong (both above safe area). Now asserts footer reaches within 20pt of screen bottom when SafeAreaEdges=None. Confirmed regression: footer is 34pt short (exact safe area inset) on iPhone Xs 18.5. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔬 Fix Approach Comparison — 4 Independent AttemptsWe ran 4 independent try-fix attempts, each proposing a fundamentally different approach to fix the iOS safe area infinite layout cycle. After retesting with the updated UITests (with Retest Results (Updated UITests)
Common failure in approaches 1, 2, 4: ✅ Selected: Approach 3 — Window-Level Safe Area Comparison (applied in
|
- Add [Order(1/2/3)] to ensure deterministic test execution - Add state reset at start of VerifyRuntimeSafeAreaEdgesChange to handle SafeAreaEdges state left by previous tests - Fix Step 5 expected text to 'Footer is now hidden' (toggle hides if already shown) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…oise Replace pixel-rounding approach with Window.SafeAreaInsets comparison. During animations, a view's own SafeAreaInsets fluctuate with sub-pixel noise, but Window.SafeAreaInsets remain stable (device-level insets like status bar and home indicator). By comparing at the Window level, SafeAreaInsetsDidChange skips animation-induced noise while still detecting genuine safe area changes (rotation, keyboard, status bar). Changes: - MauiView.cs: Add _lastWindowSafeAreaInsets field, filter in SafeAreaInsetsDidChange(), reset in MovedToWindow() - MauiScrollView.cs: Same Window-level filter, fix pre-existing bug where _safeAreaInvalidated was set to true instead of false in ValidateSafeArea() (flag was never being cleared after validation) Fixes #32586, Fixes #33934 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…Edges toggling - VerifyRotationDuringAnimationPreservesSafeArea: validates genuine Window.SafeAreaInsets changes from rotation still propagate through the SafeAreaInsetsDidChange guard during active TranslateToAsync - VerifyRapidSafeAreaToggleCycling: 3x rapid None/Container cycling validates the _safeAreaInvalidated bug fix doesn't break re-validation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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!
Root Cause
TranslateToAsyncanimations on iOS causeSafeAreaInsetsDidChangeto fire repeatedly as the view moves relative to the window. These updates often contain sub-pixel differences (e.g.,0.0000001pt). BecauseSafeAreaPaddingusesdoubleprecision, exact equality checks fail (oldSafeArea == newSafeAreareturns false), triggering:InvalidateAncestorsMeasuresSafeAreaInsetsDidChangeeventDescription of Change
In
MauiView.ValidateSafeAreaandMauiScrollView.ValidateSafeArea, we now round the safe area insets to the nearest device pixel usingContentScaleFactor.This stabilizes the values by absorbing sub-pixel oscillation into the same pixel bucket. The comparison
oldSafeArea == newSafeAreanow returnstruefor sub-pixel changes, preventing unnecessary layout invalidation and breaking the cycle.Refinements:
ContentScaleFactorinstead ofWindow.Screen.Scalefor safer access.MauiScrollViewto prevent latent cycles there.Issues Fixed
Fixes #32586
Fixes #33934
Fixes #33595
Fixes #34042
Platforms Tested