diff --git a/src/Controls/tests/DeviceTests/Elements/CollectionView/CollectionViewTests.iOS.cs b/src/Controls/tests/DeviceTests/Elements/CollectionView/CollectionViewTests.iOS.cs index 1a9dfd28b7ce..23c9282477d7 100644 --- a/src/Controls/tests/DeviceTests/Elements/CollectionView/CollectionViewTests.iOS.cs +++ b/src/Controls/tests/DeviceTests/Elements/CollectionView/CollectionViewTests.iOS.cs @@ -319,5 +319,77 @@ Rect GetCollectionViewCellBounds(IView cellContent) return cellContent.ToPlatform().GetParentOfType().GetBoundingBox(); } + + [Fact(DisplayName = "CarouselView ScrollBar Visibility should Update")] + public async Task CheckCarouselViewScrollBarVisibilityUpdates() + { + EnsureHandlerCreated(builder => + { + builder.ConfigureMauiHandlers(handlers => + { + handlers.AddHandler(); + handlers.AddHandler(); + }); + }); + + var carouselView = new CarouselView + { + ItemsSource = new List { "Item 1", "Item 2", "Item 3", "Item 4", "Item 5" }, + ItemTemplate = new DataTemplate(() => new Label { WidthRequest = 200 }) + }; + + await CreateHandlerAndAddToWindow(carouselView, async handler => + { + await Task.Delay(100); // Allow layout to complete + var nativeCollectionView = handler.Controller?.CollectionView; + Assert.NotNull(nativeCollectionView); + + // CarouselView should use CompositionalLayout + Assert.IsType(nativeCollectionView.CollectionViewLayout); + + // Test ScrollBarVisibility.Always + carouselView.HorizontalScrollBarVisibility = ScrollBarVisibility.Always; + carouselView.VerticalScrollBarVisibility = ScrollBarVisibility.Always; + await Task.Delay(100); // Allow BeginInvokeOnMainThread callbacks to drain + + // Poll for the internal scroll view (it may not be created synchronously) + UIScrollView internalScrollView = null; + for (int attempt = 0; attempt < 10 && internalScrollView == null; attempt++) + { + internalScrollView = FindInternalScrollView(nativeCollectionView); + if (internalScrollView == null) + await Task.Delay(100); + } + Assert.NotNull(internalScrollView); // Must exist for the test to be valid + + Assert.True(internalScrollView.ShowsHorizontalScrollIndicator); + Assert.True(internalScrollView.ShowsVerticalScrollIndicator); + Assert.True(nativeCollectionView.ShowsHorizontalScrollIndicator); + Assert.True(nativeCollectionView.ShowsVerticalScrollIndicator); + + // Test ScrollBarVisibility.Never + carouselView.HorizontalScrollBarVisibility = ScrollBarVisibility.Never; + carouselView.VerticalScrollBarVisibility = ScrollBarVisibility.Never; + await Task.Delay(100); + + Assert.False(internalScrollView.ShowsHorizontalScrollIndicator); // Key assertion for this bug! + Assert.False(internalScrollView.ShowsVerticalScrollIndicator); + Assert.False(nativeCollectionView.ShowsHorizontalScrollIndicator); + Assert.False(nativeCollectionView.ShowsVerticalScrollIndicator); + }); + } + + private static UIScrollView FindInternalScrollView(UICollectionView collectionView) + { + // In CV2 the scroll indicators are managed by an internal UIScrollView. + foreach (var subview in collectionView.Subviews) + { + if (subview is UIScrollView scrollView && scrollView != collectionView) + { + return scrollView; + } + } + return null; + } } } \ No newline at end of file diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/CarouselViewShouldScrollToRightPosition.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/CarouselViewShouldScrollToRightPosition.png index c222cebf4219..b68113c0fdcf 100644 Binary files a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/CarouselViewShouldScrollToRightPosition.png and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/CarouselViewShouldScrollToRightPosition.png differ diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/IndicatorViewWithTemplatedIcon.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/IndicatorViewWithTemplatedIcon.png index 1f9f5a22436b..dc3f59913a8a 100644 Binary files a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/IndicatorViewWithTemplatedIcon.png and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/IndicatorViewWithTemplatedIcon.png differ diff --git a/src/Core/src/Platform/iOS/CollectionViewExtensions.cs b/src/Core/src/Platform/iOS/CollectionViewExtensions.cs index 3bfefae7b65e..23665fad44b3 100644 --- a/src/Core/src/Platform/iOS/CollectionViewExtensions.cs +++ b/src/Core/src/Platform/iOS/CollectionViewExtensions.cs @@ -8,11 +8,63 @@ public static class CollectionViewExtensions public static void UpdateVerticalScrollBarVisibility(this UICollectionView collectionView, ScrollBarVisibility scrollBarVisibility) { collectionView.ShowsVerticalScrollIndicator = scrollBarVisibility == ScrollBarVisibility.Always || scrollBarVisibility == ScrollBarVisibility.Default; + + // In CV2 the scroll indicators are managed by an internal UIScrollView. + if (collectionView.CollectionViewLayout is UICollectionViewCompositionalLayout) + { + UpdateInternalScrollView(collectionView, true); + } } public static void UpdateHorizontalScrollBarVisibility(this UICollectionView collectionView, ScrollBarVisibility scrollBarVisibility) { collectionView.ShowsHorizontalScrollIndicator = scrollBarVisibility == ScrollBarVisibility.Always || scrollBarVisibility == ScrollBarVisibility.Default; + + // In CV2 the scroll indicators are managed by an internal UIScrollView. + if (collectionView.CollectionViewLayout is UICollectionViewCompositionalLayout) + { + UpdateInternalScrollView(collectionView, false); + } + } + + static void UpdateInternalScrollView(UICollectionView collectionView, bool isVertical) + { + if (TryApplyToInternalScrollView(collectionView, isVertical)) + { + return; + } + + // One deferred retry only; if the scroll view still doesn't exist at that point, + // the setting is not applied (acceptable: layout should always be complete by then). + collectionView.BeginInvokeOnMainThread(() => + { + TryApplyToInternalScrollView(collectionView, isVertical); + }); + } + + // NOTE: This relies on UICollectionViewCompositionalLayout's internal implementation — + // it creates a UIScrollView subview to host scroll indicators. If this changes in a + // future iOS release, this method will return false and scrollbar visibility will fall + // back to the outer UICollectionView only. + static bool TryApplyToInternalScrollView(UICollectionView collectionView, bool isVertical) + { + foreach (var subview in collectionView.Subviews) + { + if (subview is UIScrollView scrollView && scrollView != collectionView) + { + if (isVertical) + { + scrollView.ShowsVerticalScrollIndicator = collectionView.ShowsVerticalScrollIndicator; + } + else + { + scrollView.ShowsHorizontalScrollIndicator = collectionView.ShowsHorizontalScrollIndicator; + } + return true; + } + } + + return false; } } }