[iOS] Fixed issue where group Header/Footer template was set to all items when IsGrouped was true for an ObservableCollection - #29144
Conversation
There was a problem hiding this comment.
Pull Request Overview
This PR fixes the issue where group header and footer templates were incorrectly applied and not updating as expected when the ObservableCollection’s IsGrouped property was true.
- Adds group header/footer template mappings for iOS in CollectionViewHandler2.iOS.cs.
- Updates ObservableGroupedSource.cs to only add groups that implement IEnumerable.
- Removes platform-specific mapping conditions in GroupableItemsViewHandler.cs.
Reviewed Changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/Controls/src/Core/Handlers/Items2/CollectionViewHandler2.iOS.cs | Adds new mapping for group header/footer templates to support dynamic updates on iOS. |
| src/Controls/src/Core/Handlers/Items/iOS/ObservableGroupedSource.cs | Adjusts group source creation with a type-check to include only IEnumerable groups. |
| src/Controls/src/Core/Handlers/Items/GroupableItemsViewHandler.cs | Removes conditional mapping for group header/footer templates on certain platforms. |
|
/azp run MAUI-UITests-public |
|
Azure Pipelines successfully started running 1 pipeline(s). |
@jsuarezruiz , yes, the failures were caused by these changes. I have updated the fix and committed the changes. Could you please review them and let me know if you have any concerns? |
|
/azp run MAUI-UITests-public |
|
Azure Pipelines successfully started running 1 pipeline(s). |
095b58a to
2650d18
Compare
@kubaflo Based on the AI summary, I implemented the recommended improvements using a try-fix approach. Also, I added test cases to cover the suggested edge cases and applied these improvements to Android as well. |
…tems when IsGrouped was true for an ObservableCollection (#29144) <!-- 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! ## Issue 1: Group header and footer templates not updating correctly at runtime on iOS. ### Root Cause The mapper for `GroupFooterTemplateProperty` and `GroupHeaderTemplateProperty` in `GroupableItemsViewHandler` was conditionally compiled with `#if WINDOWS || __ANDROID__ || TIZEN`, meaning it was excluded from iOS builds. As a result, changing the template at runtime on iOS had no effect and templates were never displayed. ### Description of Change Removed the `#if WINDOWS || __ANDROID__ || TIZEN` preprocessor guard from `GroupableItemsViewHandler.cs`, making the `GroupFooterTemplateProperty` and `GroupHeaderTemplateProperty` mappers active on all platforms including iOS. Both mappers call `MapIsGrouped`, which triggers `UpdateItemsSource()` and refreshes the grouping state. --- ## Issue 2: Group header/footer templates incorrectly applied to all items in a flat ObservableCollection when `IsGrouped = true`. ### Root Cause In `ObservableGroupedSource.cs` (iOS), the `GroupsCount()` method iterated over all items in `_groupSource` and counted every item, regardless of whether it was an `IEnumerable` (i.e., an actual group). When `IsGrouped = true` but the source was a flat `ObservableCollection<T>` (non-grouped), each non-grouped item was counted as a section, causing `NumberOfSections` to be inflated. This led to header and footer templates being incorrectly applied to every item. ### Description of Change Modified `GroupsCount()` to only increment the count for items that implement `IEnumerable`. Non-`IEnumerable` items are no longer counted as sections. As a result, `NumberOfSections` now correctly reflects the number of actual groups, preventing header/footer templates from appearing for non-grouped items. --- ### Issues Fixed Fixes #29141 ### Test Case Tests for this fix are included in this PR: - `src/Controls/tests/TestCases.HostApp/Issues/Issue29141.cs` — HostApp page with a `CollectionView` bound to a flat `ObservableCollection`, with radio buttons to toggle `IsGrouped`, `GroupHeaderTemplate`, and `GroupFooterTemplate` at runtime. - `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29141.cs` — NUnit UI test verifying that group header/footer template views are NOT shown when the source collection is not grouped. > **Note:** The test is currently excluded from Windows (unrelated NullReferenceException — see #28824) and Android (separate fix in PR #28886). It runs on iOS and MacCatalyst. ### Platforms Tested - [x] iOS - [x] Android - [x] Mac - [ ] Windows </details> --------- Co-authored-by: Shane Neuville <5375137+PureWeen@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Vignesh-SF3580 <102575140+Vignesh-SF3580@users.noreply.github.com>
…tems when IsGrouped was true for an ObservableCollection (#29144) <!-- 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! ## Issue 1: Group header and footer templates not updating correctly at runtime on iOS. ### Root Cause The mapper for `GroupFooterTemplateProperty` and `GroupHeaderTemplateProperty` in `GroupableItemsViewHandler` was conditionally compiled with `#if WINDOWS || __ANDROID__ || TIZEN`, meaning it was excluded from iOS builds. As a result, changing the template at runtime on iOS had no effect and templates were never displayed. ### Description of Change Removed the `#if WINDOWS || __ANDROID__ || TIZEN` preprocessor guard from `GroupableItemsViewHandler.cs`, making the `GroupFooterTemplateProperty` and `GroupHeaderTemplateProperty` mappers active on all platforms including iOS. Both mappers call `MapIsGrouped`, which triggers `UpdateItemsSource()` and refreshes the grouping state. --- ## Issue 2: Group header/footer templates incorrectly applied to all items in a flat ObservableCollection when `IsGrouped = true`. ### Root Cause In `ObservableGroupedSource.cs` (iOS), the `GroupsCount()` method iterated over all items in `_groupSource` and counted every item, regardless of whether it was an `IEnumerable` (i.e., an actual group). When `IsGrouped = true` but the source was a flat `ObservableCollection<T>` (non-grouped), each non-grouped item was counted as a section, causing `NumberOfSections` to be inflated. This led to header and footer templates being incorrectly applied to every item. ### Description of Change Modified `GroupsCount()` to only increment the count for items that implement `IEnumerable`. Non-`IEnumerable` items are no longer counted as sections. As a result, `NumberOfSections` now correctly reflects the number of actual groups, preventing header/footer templates from appearing for non-grouped items. --- ### Issues Fixed Fixes #29141 ### Test Case Tests for this fix are included in this PR: - `src/Controls/tests/TestCases.HostApp/Issues/Issue29141.cs` — HostApp page with a `CollectionView` bound to a flat `ObservableCollection`, with radio buttons to toggle `IsGrouped`, `GroupHeaderTemplate`, and `GroupFooterTemplate` at runtime. - `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29141.cs` — NUnit UI test verifying that group header/footer template views are NOT shown when the source collection is not grouped. > **Note:** The test is currently excluded from Windows (unrelated NullReferenceException — see #28824) and Android (separate fix in PR #28886). It runs on iOS and MacCatalyst. ### Platforms Tested - [x] iOS - [x] Android - [x] Mac - [ ] Windows </details> --------- Co-authored-by: Shane Neuville <5375137+PureWeen@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Vignesh-SF3580 <102575140+Vignesh-SF3580@users.noreply.github.com>
…tems when IsGrouped was true for an ObservableCollection (#29144) <!-- 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! ## Issue 1: Group header and footer templates not updating correctly at runtime on iOS. ### Root Cause The mapper for `GroupFooterTemplateProperty` and `GroupHeaderTemplateProperty` in `GroupableItemsViewHandler` was conditionally compiled with `#if WINDOWS || __ANDROID__ || TIZEN`, meaning it was excluded from iOS builds. As a result, changing the template at runtime on iOS had no effect and templates were never displayed. ### Description of Change Removed the `#if WINDOWS || __ANDROID__ || TIZEN` preprocessor guard from `GroupableItemsViewHandler.cs`, making the `GroupFooterTemplateProperty` and `GroupHeaderTemplateProperty` mappers active on all platforms including iOS. Both mappers call `MapIsGrouped`, which triggers `UpdateItemsSource()` and refreshes the grouping state. --- ## Issue 2: Group header/footer templates incorrectly applied to all items in a flat ObservableCollection when `IsGrouped = true`. ### Root Cause In `ObservableGroupedSource.cs` (iOS), the `GroupsCount()` method iterated over all items in `_groupSource` and counted every item, regardless of whether it was an `IEnumerable` (i.e., an actual group). When `IsGrouped = true` but the source was a flat `ObservableCollection<T>` (non-grouped), each non-grouped item was counted as a section, causing `NumberOfSections` to be inflated. This led to header and footer templates being incorrectly applied to every item. ### Description of Change Modified `GroupsCount()` to only increment the count for items that implement `IEnumerable`. Non-`IEnumerable` items are no longer counted as sections. As a result, `NumberOfSections` now correctly reflects the number of actual groups, preventing header/footer templates from appearing for non-grouped items. --- ### Issues Fixed Fixes #29141 ### Test Case Tests for this fix are included in this PR: - `src/Controls/tests/TestCases.HostApp/Issues/Issue29141.cs` — HostApp page with a `CollectionView` bound to a flat `ObservableCollection`, with radio buttons to toggle `IsGrouped`, `GroupHeaderTemplate`, and `GroupFooterTemplate` at runtime. - `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29141.cs` — NUnit UI test verifying that group header/footer template views are NOT shown when the source collection is not grouped. > **Note:** The test is currently excluded from Windows (unrelated NullReferenceException — see #28824) and Android (separate fix in PR #28886). It runs on iOS and MacCatalyst. ### Platforms Tested - [x] iOS - [x] Android - [x] Mac - [ ] Windows </details> --------- Co-authored-by: Shane Neuville <5375137+PureWeen@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Vignesh-SF3580 <102575140+Vignesh-SF3580@users.noreply.github.com>
### Root Cause of the issue - The failing test: `SetVisibility(Visibility.Collapsed)` device test on iOS. Why it fails: PR #28983's fix maps Visibility → MapIsRunning in ActivityIndicatorHandler, which bypasses the standard ViewExtensions.UpdateVisibility path. The standard path calls Collapse() to add a CollapseConstraint (an NSLayoutConstraint that zeros the view's size). The PR's UpdateIsRunning only sets Hidden = true — it never calls Collapse() - Crash in the FeatureMatrix Navigation Page Scenario in the iOS 26 PR #34326 added an else block in UpdateBarTextColor() that explicitly sets BackButtonAppearance = null, BackIndicatorImage = null, and BackIndicatorTransitionMaskImage = null on the navigation bar when no custom color is applied. On iOS versions before 26, assigning null to these properties was treated as "use system defaults." On iOS 26, the Liquid Glass rendering pipeline reads these properties during push/pop navigation transitions and throws an exception when it encounters an explicit null — it expects either an unset property or a valid object. Since useCustomColor is false by default (no IconColor set), this crashes on every standard page navigation. - Setting ThumbTintList = null in the else block removes the tint entirely, causing the thumb to appear white instead of the default blue, because SwitchCompat does not re-resolve its theme colors once the tint list is cleared. - SetAdjustViewBounds(false) was being applied to all images regardless of their Aspect value. When this property is false, Android’s ImageView does not resize itself based on the image’s intrinsic aspect ratio and instead expands to fill the available space. As a result, an Image control with the default AspectFit setting ignored its height constraint and overflowed its container, causing the image to appear taller than expected in the screenshot test.(LoadAndVerifyGif, ThemeRelated feature tests) - PR #29144 changed the group-detection guard in ObservableGroupedSource (iOS and Android) from is IEnumerable to is ICollection to prevent strings (which implement IEnumerable<char>) from being treated as groups. While the intent was correct, the change was too broad. Custom group types that implement only IEnumerable<T> were also excluded. As a result, _groupCount became zero on iOS and _groups remained empty on Android, causing grouped CollectionView rendering failures and IndexOutOfRangeException during Add/Remove operations. - StepperHandler.iOS.cs compiles for both iOS and Mac Catalyst. On Mac Catalyst / macOS 26, OperatingSystem.IsIOS() and IsIOSVersionAtLeast(26) both return true, and the screen is always landscape. As a result, the 20pt glass pill compensation was incorrectly applied, inflating GetDesiredSize(1,1) to width = 21. (Native View Bounding Box is not empty - device Test failures) - In PR #33428, the WindowInsetsCompat.Builder block in MauiWindowInsetListener.ApplyDefaultWindowInsets was simplified to return insets; to standardize inset handling. This change was unrelated to the actual fix for #33344, which only required passing the bottom inset through as unconsumed. As part of that refactor, top inset consumption was also removed, changing the prior contract where the top inset was consumed when appBarHasContent = true. While normal safe-area scenarios worked correctly, transient layout states (e.g., temporary Height = 0 during keyboard dismissal, rotation, animation, or dynamic item generation) triggered a second inset dispatch. In those moments, the top inset satisfied the overlap condition and was applied to content before SafeAreaExtensions could normalize it, causing test failures. Bottom insets did not regress because their overlap condition cannot be met in the same transient state. (EntryScrollTest, HorizontalStackLayout_Spacing_WithLandscape, VerticalStackLayout_Spacing_WithLandscape and so on failures) ### Description of Change - Handles the three Visibility cases the same way as ViewExtensions.UpdateVisibility: `Visible`: Calls Inflate() (restores layout size) and sets Hidden = false — identical to UpdateVisibility. Additionally starts/stops the animation based on IsRunning. `Hidden`: Calls Inflate() (preserves layout space) and sets Hidden = true — identical to UpdateVisibility. The indicator keeps its layout footprint but is invisible. `Collapsed`: Sets Hidden = true and calls Collapse() (zeros out layout size via constraints) — identical to UpdateVisibility. The indicator is both invisible and takes up no space. - Removed the else block entirely. When no custom color is applied, these properties should remain untouched. The system defaults work correctly on their own — there is no need to explicitly reset them to null - Removed the else block from UpdateThumbColor in SwitchExtensions.cs. The default thumb color is managed by SwitchCompat internally from the Material theme, so no explicit reset is needed. - Restored the correct logic in ImageViewExtensions.UpdateAspect to call SetAdjustViewBounds based on the image’s Aspect value: - The guard has been updated from is ICollection to is IEnumerable && not string in the ObservableGroupedSource implementations for both iOS and Android (GroupsCount(), UpdateGroupTracking(), Add(), Remove()). This change specifically excludes string while allowing legitimate custom group types that implement only IEnumerable<T>. The fix restores the behavior for Issue22320 while keeping all Issue29141 scenarios working correctly. -Added !OperatingSystem.IsMacCatalyst() to the guard condition in GetDesiredSize, restricting the 20pt compensation to real iOS 26+ only — as intended in the original PR comment. - Restored the original behavior by consuming the top inset when appBarHasContent = true, while continuing to pass the bottom inset through unconsumed (the only change required for #33344). This retains the Android edge-to-edge fix and restores deterministic safe-area handling. ### Testing related description of change EditorNoOverlapAfterRotateToLandscape, EditorNoOverlapAfterRotateToPortrait, EntryFocusedShouldNotCauseGapAfterRotation Added cropLeft to remove the navbar on Android and re-saved the image due to entry text changes in this commit – 8d17a6d, 91047fb. DrawStringShouldDrawText – The automation ID set to GraphicsView was not found by Appium on the Windows platform, so a test condition was added for Windows to take the image directly instead of waiting for the GraphicsView. Added the base images for iOS 26 and Mac that were not added previously. Re-saved the images that failed due to the wrong iOS version image being added in the PR. Re-saved the test images that failed due to this fix PR: #31254 — e.g., GraphicsViewFeatureTests and others. Resaved the slider-related test images due to this fix PR — #34064. ### Issues Fixed Fixes #34437 ### Tested the behaviour in the following platforms - [x] iOS - [x] Mac --------- Co-authored-by: SyedAbdulAzeem <syedabdulazeem.a@syncfusion.com> Co-authored-by: Vignesh-SF3580 <102575140+Vignesh-SF3580@users.noreply.github.com> Co-authored-by: TamilarasanSF4853 <tamilarasan.velu@syncfusion.com> Co-authored-by: LogishaSelvarajSF4525 <logisha.selvaraj@syncfusion.com>
…tems when IsGrouped was true for an ObservableCollection (#29144) <!-- 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! ## Issue 1: Group header and footer templates not updating correctly at runtime on iOS. ### Root Cause The mapper for `GroupFooterTemplateProperty` and `GroupHeaderTemplateProperty` in `GroupableItemsViewHandler` was conditionally compiled with `#if WINDOWS || __ANDROID__ || TIZEN`, meaning it was excluded from iOS builds. As a result, changing the template at runtime on iOS had no effect and templates were never displayed. ### Description of Change Removed the `#if WINDOWS || __ANDROID__ || TIZEN` preprocessor guard from `GroupableItemsViewHandler.cs`, making the `GroupFooterTemplateProperty` and `GroupHeaderTemplateProperty` mappers active on all platforms including iOS. Both mappers call `MapIsGrouped`, which triggers `UpdateItemsSource()` and refreshes the grouping state. --- ## Issue 2: Group header/footer templates incorrectly applied to all items in a flat ObservableCollection when `IsGrouped = true`. ### Root Cause In `ObservableGroupedSource.cs` (iOS), the `GroupsCount()` method iterated over all items in `_groupSource` and counted every item, regardless of whether it was an `IEnumerable` (i.e., an actual group). When `IsGrouped = true` but the source was a flat `ObservableCollection<T>` (non-grouped), each non-grouped item was counted as a section, causing `NumberOfSections` to be inflated. This led to header and footer templates being incorrectly applied to every item. ### Description of Change Modified `GroupsCount()` to only increment the count for items that implement `IEnumerable`. Non-`IEnumerable` items are no longer counted as sections. As a result, `NumberOfSections` now correctly reflects the number of actual groups, preventing header/footer templates from appearing for non-grouped items. --- ### Issues Fixed Fixes #29141 ### Test Case Tests for this fix are included in this PR: - `src/Controls/tests/TestCases.HostApp/Issues/Issue29141.cs` — HostApp page with a `CollectionView` bound to a flat `ObservableCollection`, with radio buttons to toggle `IsGrouped`, `GroupHeaderTemplate`, and `GroupFooterTemplate` at runtime. - `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29141.cs` — NUnit UI test verifying that group header/footer template views are NOT shown when the source collection is not grouped. > **Note:** The test is currently excluded from Windows (unrelated NullReferenceException — see #28824) and Android (separate fix in PR #28886). It runs on iOS and MacCatalyst. ### Platforms Tested - [x] iOS - [x] Android - [x] Mac - [ ] Windows </details> --------- Co-authored-by: Shane Neuville <5375137+PureWeen@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Vignesh-SF3580 <102575140+Vignesh-SF3580@users.noreply.github.com>
### Root Cause of the issue - The failing test: `SetVisibility(Visibility.Collapsed)` device test on iOS. Why it fails: PR #28983's fix maps Visibility → MapIsRunning in ActivityIndicatorHandler, which bypasses the standard ViewExtensions.UpdateVisibility path. The standard path calls Collapse() to add a CollapseConstraint (an NSLayoutConstraint that zeros the view's size). The PR's UpdateIsRunning only sets Hidden = true — it never calls Collapse() - Crash in the FeatureMatrix Navigation Page Scenario in the iOS 26 PR #34326 added an else block in UpdateBarTextColor() that explicitly sets BackButtonAppearance = null, BackIndicatorImage = null, and BackIndicatorTransitionMaskImage = null on the navigation bar when no custom color is applied. On iOS versions before 26, assigning null to these properties was treated as "use system defaults." On iOS 26, the Liquid Glass rendering pipeline reads these properties during push/pop navigation transitions and throws an exception when it encounters an explicit null — it expects either an unset property or a valid object. Since useCustomColor is false by default (no IconColor set), this crashes on every standard page navigation. - Setting ThumbTintList = null in the else block removes the tint entirely, causing the thumb to appear white instead of the default blue, because SwitchCompat does not re-resolve its theme colors once the tint list is cleared. - SetAdjustViewBounds(false) was being applied to all images regardless of their Aspect value. When this property is false, Android’s ImageView does not resize itself based on the image’s intrinsic aspect ratio and instead expands to fill the available space. As a result, an Image control with the default AspectFit setting ignored its height constraint and overflowed its container, causing the image to appear taller than expected in the screenshot test.(LoadAndVerifyGif, ThemeRelated feature tests) - PR #29144 changed the group-detection guard in ObservableGroupedSource (iOS and Android) from is IEnumerable to is ICollection to prevent strings (which implement IEnumerable<char>) from being treated as groups. While the intent was correct, the change was too broad. Custom group types that implement only IEnumerable<T> were also excluded. As a result, _groupCount became zero on iOS and _groups remained empty on Android, causing grouped CollectionView rendering failures and IndexOutOfRangeException during Add/Remove operations. - StepperHandler.iOS.cs compiles for both iOS and Mac Catalyst. On Mac Catalyst / macOS 26, OperatingSystem.IsIOS() and IsIOSVersionAtLeast(26) both return true, and the screen is always landscape. As a result, the 20pt glass pill compensation was incorrectly applied, inflating GetDesiredSize(1,1) to width = 21. (Native View Bounding Box is not empty - device Test failures) - In PR #33428, the WindowInsetsCompat.Builder block in MauiWindowInsetListener.ApplyDefaultWindowInsets was simplified to return insets; to standardize inset handling. This change was unrelated to the actual fix for #33344, which only required passing the bottom inset through as unconsumed. As part of that refactor, top inset consumption was also removed, changing the prior contract where the top inset was consumed when appBarHasContent = true. While normal safe-area scenarios worked correctly, transient layout states (e.g., temporary Height = 0 during keyboard dismissal, rotation, animation, or dynamic item generation) triggered a second inset dispatch. In those moments, the top inset satisfied the overlap condition and was applied to content before SafeAreaExtensions could normalize it, causing test failures. Bottom insets did not regress because their overlap condition cannot be met in the same transient state. (EntryScrollTest, HorizontalStackLayout_Spacing_WithLandscape, VerticalStackLayout_Spacing_WithLandscape and so on failures) ### Description of Change - Handles the three Visibility cases the same way as ViewExtensions.UpdateVisibility: `Visible`: Calls Inflate() (restores layout size) and sets Hidden = false — identical to UpdateVisibility. Additionally starts/stops the animation based on IsRunning. `Hidden`: Calls Inflate() (preserves layout space) and sets Hidden = true — identical to UpdateVisibility. The indicator keeps its layout footprint but is invisible. `Collapsed`: Sets Hidden = true and calls Collapse() (zeros out layout size via constraints) — identical to UpdateVisibility. The indicator is both invisible and takes up no space. - Removed the else block entirely. When no custom color is applied, these properties should remain untouched. The system defaults work correctly on their own — there is no need to explicitly reset them to null - Removed the else block from UpdateThumbColor in SwitchExtensions.cs. The default thumb color is managed by SwitchCompat internally from the Material theme, so no explicit reset is needed. - Restored the correct logic in ImageViewExtensions.UpdateAspect to call SetAdjustViewBounds based on the image’s Aspect value: - The guard has been updated from is ICollection to is IEnumerable && not string in the ObservableGroupedSource implementations for both iOS and Android (GroupsCount(), UpdateGroupTracking(), Add(), Remove()). This change specifically excludes string while allowing legitimate custom group types that implement only IEnumerable<T>. The fix restores the behavior for Issue22320 while keeping all Issue29141 scenarios working correctly. -Added !OperatingSystem.IsMacCatalyst() to the guard condition in GetDesiredSize, restricting the 20pt compensation to real iOS 26+ only — as intended in the original PR comment. - Restored the original behavior by consuming the top inset when appBarHasContent = true, while continuing to pass the bottom inset through unconsumed (the only change required for #33344). This retains the Android edge-to-edge fix and restores deterministic safe-area handling. ### Testing related description of change EditorNoOverlapAfterRotateToLandscape, EditorNoOverlapAfterRotateToPortrait, EntryFocusedShouldNotCauseGapAfterRotation Added cropLeft to remove the navbar on Android and re-saved the image due to entry text changes in this commit – 8d17a6d, 91047fb. DrawStringShouldDrawText – The automation ID set to GraphicsView was not found by Appium on the Windows platform, so a test condition was added for Windows to take the image directly instead of waiting for the GraphicsView. Added the base images for iOS 26 and Mac that were not added previously. Re-saved the images that failed due to the wrong iOS version image being added in the PR. Re-saved the test images that failed due to this fix PR: #31254 — e.g., GraphicsViewFeatureTests and others. Resaved the slider-related test images due to this fix PR — #34064. ### Issues Fixed Fixes #34437 ### Tested the behaviour in the following platforms - [x] iOS - [x] Mac --------- Co-authored-by: SyedAbdulAzeem <syedabdulazeem.a@syncfusion.com> Co-authored-by: Vignesh-SF3580 <102575140+Vignesh-SF3580@users.noreply.github.com> Co-authored-by: TamilarasanSF4853 <tamilarasan.velu@syncfusion.com> Co-authored-by: LogishaSelvarajSF4525 <logisha.selvaraj@syncfusion.com>
…tems when IsGrouped was true for an ObservableCollection (#29144) <!-- 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! ## Issue 1: Group header and footer templates not updating correctly at runtime on iOS. ### Root Cause The mapper for `GroupFooterTemplateProperty` and `GroupHeaderTemplateProperty` in `GroupableItemsViewHandler` was conditionally compiled with `#if WINDOWS || __ANDROID__ || TIZEN`, meaning it was excluded from iOS builds. As a result, changing the template at runtime on iOS had no effect and templates were never displayed. ### Description of Change Removed the `#if WINDOWS || __ANDROID__ || TIZEN` preprocessor guard from `GroupableItemsViewHandler.cs`, making the `GroupFooterTemplateProperty` and `GroupHeaderTemplateProperty` mappers active on all platforms including iOS. Both mappers call `MapIsGrouped`, which triggers `UpdateItemsSource()` and refreshes the grouping state. --- ## Issue 2: Group header/footer templates incorrectly applied to all items in a flat ObservableCollection when `IsGrouped = true`. ### Root Cause In `ObservableGroupedSource.cs` (iOS), the `GroupsCount()` method iterated over all items in `_groupSource` and counted every item, regardless of whether it was an `IEnumerable` (i.e., an actual group). When `IsGrouped = true` but the source was a flat `ObservableCollection<T>` (non-grouped), each non-grouped item was counted as a section, causing `NumberOfSections` to be inflated. This led to header and footer templates being incorrectly applied to every item. ### Description of Change Modified `GroupsCount()` to only increment the count for items that implement `IEnumerable`. Non-`IEnumerable` items are no longer counted as sections. As a result, `NumberOfSections` now correctly reflects the number of actual groups, preventing header/footer templates from appearing for non-grouped items. --- ### Issues Fixed Fixes #29141 ### Test Case Tests for this fix are included in this PR: - `src/Controls/tests/TestCases.HostApp/Issues/Issue29141.cs` — HostApp page with a `CollectionView` bound to a flat `ObservableCollection`, with radio buttons to toggle `IsGrouped`, `GroupHeaderTemplate`, and `GroupFooterTemplate` at runtime. - `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29141.cs` — NUnit UI test verifying that group header/footer template views are NOT shown when the source collection is not grouped. > **Note:** The test is currently excluded from Windows (unrelated NullReferenceException — see #28824) and Android (separate fix in PR #28886). It runs on iOS and MacCatalyst. ### Platforms Tested - [x] iOS - [x] Android - [x] Mac - [ ] Windows </details> --------- Co-authored-by: Shane Neuville <5375137+PureWeen@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Vignesh-SF3580 <102575140+Vignesh-SF3580@users.noreply.github.com>
### Root Cause of the issue - The failing test: `SetVisibility(Visibility.Collapsed)` device test on iOS. Why it fails: PR #28983's fix maps Visibility → MapIsRunning in ActivityIndicatorHandler, which bypasses the standard ViewExtensions.UpdateVisibility path. The standard path calls Collapse() to add a CollapseConstraint (an NSLayoutConstraint that zeros the view's size). The PR's UpdateIsRunning only sets Hidden = true — it never calls Collapse() - Crash in the FeatureMatrix Navigation Page Scenario in the iOS 26 PR #34326 added an else block in UpdateBarTextColor() that explicitly sets BackButtonAppearance = null, BackIndicatorImage = null, and BackIndicatorTransitionMaskImage = null on the navigation bar when no custom color is applied. On iOS versions before 26, assigning null to these properties was treated as "use system defaults." On iOS 26, the Liquid Glass rendering pipeline reads these properties during push/pop navigation transitions and throws an exception when it encounters an explicit null — it expects either an unset property or a valid object. Since useCustomColor is false by default (no IconColor set), this crashes on every standard page navigation. - Setting ThumbTintList = null in the else block removes the tint entirely, causing the thumb to appear white instead of the default blue, because SwitchCompat does not re-resolve its theme colors once the tint list is cleared. - SetAdjustViewBounds(false) was being applied to all images regardless of their Aspect value. When this property is false, Android’s ImageView does not resize itself based on the image’s intrinsic aspect ratio and instead expands to fill the available space. As a result, an Image control with the default AspectFit setting ignored its height constraint and overflowed its container, causing the image to appear taller than expected in the screenshot test.(LoadAndVerifyGif, ThemeRelated feature tests) - PR #29144 changed the group-detection guard in ObservableGroupedSource (iOS and Android) from is IEnumerable to is ICollection to prevent strings (which implement IEnumerable<char>) from being treated as groups. While the intent was correct, the change was too broad. Custom group types that implement only IEnumerable<T> were also excluded. As a result, _groupCount became zero on iOS and _groups remained empty on Android, causing grouped CollectionView rendering failures and IndexOutOfRangeException during Add/Remove operations. - StepperHandler.iOS.cs compiles for both iOS and Mac Catalyst. On Mac Catalyst / macOS 26, OperatingSystem.IsIOS() and IsIOSVersionAtLeast(26) both return true, and the screen is always landscape. As a result, the 20pt glass pill compensation was incorrectly applied, inflating GetDesiredSize(1,1) to width = 21. (Native View Bounding Box is not empty - device Test failures) - In PR #33428, the WindowInsetsCompat.Builder block in MauiWindowInsetListener.ApplyDefaultWindowInsets was simplified to return insets; to standardize inset handling. This change was unrelated to the actual fix for #33344, which only required passing the bottom inset through as unconsumed. As part of that refactor, top inset consumption was also removed, changing the prior contract where the top inset was consumed when appBarHasContent = true. While normal safe-area scenarios worked correctly, transient layout states (e.g., temporary Height = 0 during keyboard dismissal, rotation, animation, or dynamic item generation) triggered a second inset dispatch. In those moments, the top inset satisfied the overlap condition and was applied to content before SafeAreaExtensions could normalize it, causing test failures. Bottom insets did not regress because their overlap condition cannot be met in the same transient state. (EntryScrollTest, HorizontalStackLayout_Spacing_WithLandscape, VerticalStackLayout_Spacing_WithLandscape and so on failures) ### Description of Change - Handles the three Visibility cases the same way as ViewExtensions.UpdateVisibility: `Visible`: Calls Inflate() (restores layout size) and sets Hidden = false — identical to UpdateVisibility. Additionally starts/stops the animation based on IsRunning. `Hidden`: Calls Inflate() (preserves layout space) and sets Hidden = true — identical to UpdateVisibility. The indicator keeps its layout footprint but is invisible. `Collapsed`: Sets Hidden = true and calls Collapse() (zeros out layout size via constraints) — identical to UpdateVisibility. The indicator is both invisible and takes up no space. - Removed the else block entirely. When no custom color is applied, these properties should remain untouched. The system defaults work correctly on their own — there is no need to explicitly reset them to null - Removed the else block from UpdateThumbColor in SwitchExtensions.cs. The default thumb color is managed by SwitchCompat internally from the Material theme, so no explicit reset is needed. - Restored the correct logic in ImageViewExtensions.UpdateAspect to call SetAdjustViewBounds based on the image’s Aspect value: - The guard has been updated from is ICollection to is IEnumerable && not string in the ObservableGroupedSource implementations for both iOS and Android (GroupsCount(), UpdateGroupTracking(), Add(), Remove()). This change specifically excludes string while allowing legitimate custom group types that implement only IEnumerable<T>. The fix restores the behavior for Issue22320 while keeping all Issue29141 scenarios working correctly. -Added !OperatingSystem.IsMacCatalyst() to the guard condition in GetDesiredSize, restricting the 20pt compensation to real iOS 26+ only — as intended in the original PR comment. - Restored the original behavior by consuming the top inset when appBarHasContent = true, while continuing to pass the bottom inset through unconsumed (the only change required for #33344). This retains the Android edge-to-edge fix and restores deterministic safe-area handling. ### Testing related description of change EditorNoOverlapAfterRotateToLandscape, EditorNoOverlapAfterRotateToPortrait, EntryFocusedShouldNotCauseGapAfterRotation Added cropLeft to remove the navbar on Android and re-saved the image due to entry text changes in this commit – 8d17a6d, 91047fb. DrawStringShouldDrawText – The automation ID set to GraphicsView was not found by Appium on the Windows platform, so a test condition was added for Windows to take the image directly instead of waiting for the GraphicsView. Added the base images for iOS 26 and Mac that were not added previously. Re-saved the images that failed due to the wrong iOS version image being added in the PR. Re-saved the test images that failed due to this fix PR: #31254 — e.g., GraphicsViewFeatureTests and others. Resaved the slider-related test images due to this fix PR — #34064. ### Issues Fixed Fixes #34437 ### Tested the behaviour in the following platforms - [x] iOS - [x] Mac --------- Co-authored-by: SyedAbdulAzeem <syedabdulazeem.a@syncfusion.com> Co-authored-by: Vignesh-SF3580 <102575140+Vignesh-SF3580@users.noreply.github.com> Co-authored-by: TamilarasanSF4853 <tamilarasan.velu@syncfusion.com> Co-authored-by: LogishaSelvarajSF4525 <logisha.selvaraj@syncfusion.com>
- The failing test: `SetVisibility(Visibility.Collapsed)` device test on iOS. Why it fails: PR dotnet#28983's fix maps Visibility → MapIsRunning in ActivityIndicatorHandler, which bypasses the standard ViewExtensions.UpdateVisibility path. The standard path calls Collapse() to add a CollapseConstraint (an NSLayoutConstraint that zeros the view's size). The PR's UpdateIsRunning only sets Hidden = true — it never calls Collapse() - Crash in the FeatureMatrix Navigation Page Scenario in the iOS 26 PR dotnet#34326 added an else block in UpdateBarTextColor() that explicitly sets BackButtonAppearance = null, BackIndicatorImage = null, and BackIndicatorTransitionMaskImage = null on the navigation bar when no custom color is applied. On iOS versions before 26, assigning null to these properties was treated as "use system defaults." On iOS 26, the Liquid Glass rendering pipeline reads these properties during push/pop navigation transitions and throws an exception when it encounters an explicit null — it expects either an unset property or a valid object. Since useCustomColor is false by default (no IconColor set), this crashes on every standard page navigation. - Setting ThumbTintList = null in the else block removes the tint entirely, causing the thumb to appear white instead of the default blue, because SwitchCompat does not re-resolve its theme colors once the tint list is cleared. - SetAdjustViewBounds(false) was being applied to all images regardless of their Aspect value. When this property is false, Android’s ImageView does not resize itself based on the image’s intrinsic aspect ratio and instead expands to fill the available space. As a result, an Image control with the default AspectFit setting ignored its height constraint and overflowed its container, causing the image to appear taller than expected in the screenshot test.(LoadAndVerifyGif, ThemeRelated feature tests) - PR dotnet#29144 changed the group-detection guard in ObservableGroupedSource (iOS and Android) from is IEnumerable to is ICollection to prevent strings (which implement IEnumerable<char>) from being treated as groups. While the intent was correct, the change was too broad. Custom group types that implement only IEnumerable<T> were also excluded. As a result, _groupCount became zero on iOS and _groups remained empty on Android, causing grouped CollectionView rendering failures and IndexOutOfRangeException during Add/Remove operations. - StepperHandler.iOS.cs compiles for both iOS and Mac Catalyst. On Mac Catalyst / macOS 26, OperatingSystem.IsIOS() and IsIOSVersionAtLeast(26) both return true, and the screen is always landscape. As a result, the 20pt glass pill compensation was incorrectly applied, inflating GetDesiredSize(1,1) to width = 21. (Native View Bounding Box is not empty - device Test failures) - In PR dotnet#33428, the WindowInsetsCompat.Builder block in MauiWindowInsetListener.ApplyDefaultWindowInsets was simplified to return insets; to standardize inset handling. This change was unrelated to the actual fix for dotnet#33344, which only required passing the bottom inset through as unconsumed. As part of that refactor, top inset consumption was also removed, changing the prior contract where the top inset was consumed when appBarHasContent = true. While normal safe-area scenarios worked correctly, transient layout states (e.g., temporary Height = 0 during keyboard dismissal, rotation, animation, or dynamic item generation) triggered a second inset dispatch. In those moments, the top inset satisfied the overlap condition and was applied to content before SafeAreaExtensions could normalize it, causing test failures. Bottom insets did not regress because their overlap condition cannot be met in the same transient state. (EntryScrollTest, HorizontalStackLayout_Spacing_WithLandscape, VerticalStackLayout_Spacing_WithLandscape and so on failures) - Handles the three Visibility cases the same way as ViewExtensions.UpdateVisibility: `Visible`: Calls Inflate() (restores layout size) and sets Hidden = false — identical to UpdateVisibility. Additionally starts/stops the animation based on IsRunning. `Hidden`: Calls Inflate() (preserves layout space) and sets Hidden = true — identical to UpdateVisibility. The indicator keeps its layout footprint but is invisible. `Collapsed`: Sets Hidden = true and calls Collapse() (zeros out layout size via constraints) — identical to UpdateVisibility. The indicator is both invisible and takes up no space. - Removed the else block entirely. When no custom color is applied, these properties should remain untouched. The system defaults work correctly on their own — there is no need to explicitly reset them to null - Removed the else block from UpdateThumbColor in SwitchExtensions.cs. The default thumb color is managed by SwitchCompat internally from the Material theme, so no explicit reset is needed. - Restored the correct logic in ImageViewExtensions.UpdateAspect to call SetAdjustViewBounds based on the image’s Aspect value: - The guard has been updated from is ICollection to is IEnumerable && not string in the ObservableGroupedSource implementations for both iOS and Android (GroupsCount(), UpdateGroupTracking(), Add(), Remove()). This change specifically excludes string while allowing legitimate custom group types that implement only IEnumerable<T>. The fix restores the behavior for Issue22320 while keeping all Issue29141 scenarios working correctly. -Added !OperatingSystem.IsMacCatalyst() to the guard condition in GetDesiredSize, restricting the 20pt compensation to real iOS 26+ only — as intended in the original PR comment. - Restored the original behavior by consuming the top inset when appBarHasContent = true, while continuing to pass the bottom inset through unconsumed (the only change required for dotnet#33344). This retains the Android edge-to-edge fix and restores deterministic safe-area handling. EditorNoOverlapAfterRotateToLandscape, EditorNoOverlapAfterRotateToPortrait, EntryFocusedShouldNotCauseGapAfterRotation Added cropLeft to remove the navbar on Android and re-saved the image due to entry text changes in this commit – dotnet@8d17a6d, dotnet@91047fb. DrawStringShouldDrawText – The automation ID set to GraphicsView was not found by Appium on the Windows platform, so a test condition was added for Windows to take the image directly instead of waiting for the GraphicsView. Added the base images for iOS 26 and Mac that were not added previously. Re-saved the images that failed due to the wrong iOS version image being added in the PR. Re-saved the test images that failed due to this fix PR: dotnet#31254 — e.g., GraphicsViewFeatureTests and others. Resaved the slider-related test images due to this fix PR — dotnet#34064. Fixes dotnet#34437 - [x] iOS - [x] Mac --------- Co-authored-by: SyedAbdulAzeem <syedabdulazeem.a@syncfusion.com> Co-authored-by: Vignesh-SF3580 <102575140+Vignesh-SF3580@users.noreply.github.com> Co-authored-by: TamilarasanSF4853 <tamilarasan.velu@syncfusion.com> Co-authored-by: LogishaSelvarajSF4525 <logisha.selvaraj@syncfusion.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!
Issue 1: Group header and footer templates not updating correctly at runtime on iOS.
Root Cause
The mapper for
GroupFooterTemplatePropertyandGroupHeaderTemplatePropertyinGroupableItemsViewHandlerwas conditionally compiled with#if WINDOWS || __ANDROID__ || TIZEN, meaning it was excluded from iOS builds. As a result, changing the template at runtime on iOS had no effect and templates were never displayed.Description of Change
Removed the
#if WINDOWS || __ANDROID__ || TIZENpreprocessor guard fromGroupableItemsViewHandler.cs, making theGroupFooterTemplatePropertyandGroupHeaderTemplatePropertymappers active on all platforms including iOS. Both mappers callMapIsGrouped, which triggersUpdateItemsSource()and refreshes the grouping state.Issue 2: Group header/footer templates incorrectly applied to all items in a flat ObservableCollection when
IsGrouped = true.Root Cause
In
ObservableGroupedSource.cs(iOS), theGroupsCount()method iterated over all items in_groupSourceand counted every item, regardless of whether it was anIEnumerable(i.e., an actual group). WhenIsGrouped = truebut the source was a flatObservableCollection<T>(non-grouped), each non-grouped item was counted as a section, causingNumberOfSectionsto be inflated. This led to header and footer templates being incorrectly applied to every item.Description of Change
Modified
GroupsCount()to only increment the count for items that implementIEnumerable. Non-IEnumerableitems are no longer counted as sections. As a result,NumberOfSectionsnow correctly reflects the number of actual groups, preventing header/footer templates from appearing for non-grouped items.Issues Fixed
Fixes #29141
Test Case
Tests for this fix are included in this PR:
src/Controls/tests/TestCases.HostApp/Issues/Issue29141.cs— HostApp page with aCollectionViewbound to a flatObservableCollection, with radio buttons to toggleIsGrouped,GroupHeaderTemplate, andGroupFooterTemplateat runtime.src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29141.cs— NUnit UI test verifying that group header/footer template views are NOT shown when the source collection is not grouped.Platforms Tested