From acbdf0e877d458f7bd8ae65077a41c673e1c23fb Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Sat, 1 Jun 2024 18:32:56 +0200 Subject: [PATCH 1/5] Implemented ios:Slider.UpdateOnTap --- .../Core/Hosting/AppHostBuilderExtensions.cs | 1 + src/Controls/src/Core/Slider/Slider.Mapper.cs | 16 +++++++++ src/Controls/src/Core/Slider/Slider.iOS.cs | 19 +++++++++++ .../TestCases.HostApp/Issues/Issue20098.xaml | 10 ++++++ .../Issues/Issue20098.xaml.cs | 15 +++++++++ .../Tests/Issues/Issue20098.cs | 30 +++++++++++++++++ .../src/Handlers/Slider/SliderHandler.iOS.cs | 33 +++++++++++++++++++ 7 files changed, 124 insertions(+) create mode 100644 src/Controls/src/Core/Slider/Slider.Mapper.cs create mode 100644 src/Controls/src/Core/Slider/Slider.iOS.cs create mode 100644 src/Controls/tests/TestCases.HostApp/Issues/Issue20098.xaml create mode 100644 src/Controls/tests/TestCases.HostApp/Issues/Issue20098.xaml.cs create mode 100644 src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue20098.cs diff --git a/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs b/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs index 28bb3894488a..ecd58db7cae4 100644 --- a/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs +++ b/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs @@ -289,6 +289,7 @@ internal static MauiAppBuilder RemapForControls(this MauiAppBuilder builder) ContentPage.RemapForControls(); ImageButton.RemapForControls(); + Slider.RemapForControls(); return builder; } } diff --git a/src/Controls/src/Core/Slider/Slider.Mapper.cs b/src/Controls/src/Core/Slider/Slider.Mapper.cs new file mode 100644 index 000000000000..65f51e9b0820 --- /dev/null +++ b/src/Controls/src/Core/Slider/Slider.Mapper.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Maui.Controls.Compatibility; + +namespace Microsoft.Maui.Controls +{ + public partial class Slider + { + internal static new void RemapForControls() + { + // Adjust the mappings to preserve Controls.Slider legacy behaviors +#if IOS + SliderHandler.Mapper.ReplaceMapping(PlatformConfiguration.iOSSpecific.Slider.UpdateOnTapProperty.PropertyName, MapUpdateOnTap); +#endif + } + } +} \ No newline at end of file diff --git a/src/Controls/src/Core/Slider/Slider.iOS.cs b/src/Controls/src/Core/Slider/Slider.iOS.cs new file mode 100644 index 000000000000..489374e0eb6c --- /dev/null +++ b/src/Controls/src/Core/Slider/Slider.iOS.cs @@ -0,0 +1,19 @@ +using Microsoft.Maui.Controls.Platform; +using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific; + +namespace Microsoft.Maui.Controls +{ + public partial class Slider + { + internal static void MapUpdateOnTap(SliderHandler handler, Slider slider) => + MapUpdateOnTap((ISliderHandler)handler, slider); + + internal static void MapUpdateOnTap(ISliderHandler handler, Slider slider) + { + if (handler is SliderHandler sliderHandler) + { + sliderHandler.MapUpdateOnTap(slider.OnThisPlatform().GetUpdateOnTap()); + } + } + } +} \ No newline at end of file diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue20098.xaml b/src/Controls/tests/TestCases.HostApp/Issues/Issue20098.xaml new file mode 100644 index 000000000000..d66dcf039f0a --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue20098.xaml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue20098.xaml.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue20098.xaml.cs new file mode 100644 index 000000000000..dc4d9cf189b5 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue20098.xaml.cs @@ -0,0 +1,15 @@ +using Microsoft.Maui.Controls; +using Microsoft.Maui.Controls.Xaml; + +namespace Maui.Controls.Sample.Issues +{ + [XamlCompilation(XamlCompilationOptions.Compile)] + [Issue(IssueTracker.Github, 20098, "iOS-specific Slider.UpdateOnTab", PlatformAffected.iOS)] + public partial class Issue20098 : ContentPage + { + public Issue20098() + { + InitializeComponent(); + } + } +} \ No newline at end of file diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue20098.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue20098.cs new file mode 100644 index 000000000000..191ecc331ff9 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue20098.cs @@ -0,0 +1,30 @@ +#if IOS +using NUnit.Framework; +using NUnit.Framework.Legacy; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue20098: _IssuesUITest +{ + public Issue20098(TestDevice device) : base(device) + { + } + + public override string Issue => "iOS-specific Slider.UpdateOnTab"; + + [Test] + [Category(UITestCategories.Picker)] + public void UpdateOnTabShouldWork() + { + _ = App.WaitForElement("slider"); + var pickerRect = App.FindElement("slider").GetRect(); + var label = App.FindElement("label"); + App.Click(pickerRect.X + pickerRect.Width / 2, pickerRect.Y); + var text = label.GetText(); + + ClassicAssert.AreNotEqual("0", text); + } +} +#endif \ No newline at end of file diff --git a/src/Core/src/Handlers/Slider/SliderHandler.iOS.cs b/src/Core/src/Handlers/Slider/SliderHandler.iOS.cs index b0853a3ab68c..1ee6512c8f86 100644 --- a/src/Core/src/Handlers/Slider/SliderHandler.iOS.cs +++ b/src/Core/src/Handlers/Slider/SliderHandler.iOS.cs @@ -7,6 +7,7 @@ namespace Microsoft.Maui.Handlers public partial class SliderHandler : ViewHandler { readonly SliderProxy _proxy = new(); + UITapGestureRecognizer? _sliderTapRecognizer; protected override UISlider CreatePlatformView() { @@ -30,6 +31,38 @@ protected override void DisconnectHandler(UISlider platformView) _proxy.Disconnect(platformView); } + internal void MapUpdateOnTap(bool isMapUpdateOnTapEnabled) + { + if (isMapUpdateOnTapEnabled) + { + if (_sliderTapRecognizer == null) + { + _sliderTapRecognizer = new UITapGestureRecognizer((recognizer) => + { + var control = PlatformView; + if (control != null) + { + var tappedLocation = recognizer.LocationInView(control); + if (tappedLocation != default) + { + var val = tappedLocation.X * control.MaxValue / control.Frame.Size.Width; + VirtualView.Value = val; + } + } + }); + PlatformView.AddGestureRecognizer(_sliderTapRecognizer); + } + } + else + { + if (_sliderTapRecognizer != null) + { + PlatformView.RemoveGestureRecognizer(_sliderTapRecognizer); + _sliderTapRecognizer = null; + } + } + } + public static void MapMinimum(ISliderHandler handler, ISlider slider) { handler.PlatformView?.UpdateMinimum(slider); From 63e37ec9cf2d779c2f48ce69023d96515a761a94 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Tue, 4 Mar 2025 15:16:04 +0100 Subject: [PATCH 2/5] Added a device test --- .../Elements/Slider/SliderTests.cs | 30 +++++++++++++++ .../Elements/Slider/SliderTests.iOS.cs | 38 +++++++++++++++++++ .../tests/DeviceTests/TestCategory.cs | 1 + .../src/Handlers/Slider/SliderHandler.iOS.cs | 1 + 4 files changed, 70 insertions(+) create mode 100644 src/Controls/tests/DeviceTests/Elements/Slider/SliderTests.cs create mode 100644 src/Controls/tests/DeviceTests/Elements/Slider/SliderTests.iOS.cs diff --git a/src/Controls/tests/DeviceTests/Elements/Slider/SliderTests.cs b/src/Controls/tests/DeviceTests/Elements/Slider/SliderTests.cs new file mode 100644 index 000000000000..179cf95d5729 --- /dev/null +++ b/src/Controls/tests/DeviceTests/Elements/Slider/SliderTests.cs @@ -0,0 +1,30 @@ +using System; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Controls.Handlers.Compatibility; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Handlers; +using Microsoft.Maui.Hosting; +using Microsoft.Maui.Platform; +using Xunit; + +namespace Microsoft.Maui.DeviceTests +{ + [Category(TestCategory.Slider)] + public partial class SliderTests : ControlsHandlerTestBase + { + void SetupBuilder() + { + EnsureHandlerCreated(builder => + { + builder.ConfigureMauiHandlers(handlers => + { + handlers.AddHandler(); + }); + }); + } + } +} \ No newline at end of file diff --git a/src/Controls/tests/DeviceTests/Elements/Slider/SliderTests.iOS.cs b/src/Controls/tests/DeviceTests/Elements/Slider/SliderTests.iOS.cs new file mode 100644 index 000000000000..be3b68fcb259 --- /dev/null +++ b/src/Controls/tests/DeviceTests/Elements/Slider/SliderTests.iOS.cs @@ -0,0 +1,38 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Handlers; +using Xunit; +using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific; +using Slider = Microsoft.Maui.Controls.Slider; +using Microsoft.Maui.Controls.PlatformConfiguration; + +namespace Microsoft.Maui.DeviceTests +{ + public partial class SliderTests + { + [Fact("Slider Does Not Leak")] + public async Task DoesNotLeak() + { + SetupBuilder(); + WeakReference platformViewReference = null; + WeakReference handlerReference = null; + + await InvokeOnMainThreadAsync(() => + { + var layout = new VerticalStackLayout(); + var slider = new Slider(); + //slider.On().SetUpdateOnTap(true); + layout.Add(slider); + + var handler = CreateHandler(layout); + handlerReference = new WeakReference(slider.Handler); + platformViewReference = new WeakReference(slider.Handler.PlatformView); + }); + + await AssertionExtensions.WaitForGC(handlerReference, platformViewReference); + } + } +} \ No newline at end of file diff --git a/src/Controls/tests/DeviceTests/TestCategory.cs b/src/Controls/tests/DeviceTests/TestCategory.cs index 926c0bfa1cb3..85300e927b9d 100644 --- a/src/Controls/tests/DeviceTests/TestCategory.cs +++ b/src/Controls/tests/DeviceTests/TestCategory.cs @@ -42,6 +42,7 @@ public static class TestCategory public const string SearchBar = "SearchBar"; public const string Shape = "Shape"; public const string Shell = "Shell"; + public const string Slider = "Slider"; public const string SwipeView = "SwipeView"; public const string TabbedPage = "TabbedPage"; public const string TextInput = "TextInput"; diff --git a/src/Core/src/Handlers/Slider/SliderHandler.iOS.cs b/src/Core/src/Handlers/Slider/SliderHandler.iOS.cs index 1ee6512c8f86..f3b30bf8fbc8 100644 --- a/src/Core/src/Handlers/Slider/SliderHandler.iOS.cs +++ b/src/Core/src/Handlers/Slider/SliderHandler.iOS.cs @@ -23,6 +23,7 @@ protected override void ConnectHandler(UISlider platformView) { base.ConnectHandler(platformView); _proxy.Connect(VirtualView, platformView); + _sliderTapRecognizer = null; } protected override void DisconnectHandler(UISlider platformView) From 61d15b8005fc4fe6ea789b7c9c52c9f14ca3a83e Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Wed, 5 Mar 2025 13:39:06 +0100 Subject: [PATCH 3/5] Fixes --- .../tests/DeviceTests/Elements/Slider/SliderTests.iOS.cs | 2 +- src/Core/src/Handlers/Slider/SliderHandler.iOS.cs | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Controls/tests/DeviceTests/Elements/Slider/SliderTests.iOS.cs b/src/Controls/tests/DeviceTests/Elements/Slider/SliderTests.iOS.cs index be3b68fcb259..b8829b33eba1 100644 --- a/src/Controls/tests/DeviceTests/Elements/Slider/SliderTests.iOS.cs +++ b/src/Controls/tests/DeviceTests/Elements/Slider/SliderTests.iOS.cs @@ -24,7 +24,7 @@ await InvokeOnMainThreadAsync(() => { var layout = new VerticalStackLayout(); var slider = new Slider(); - //slider.On().SetUpdateOnTap(true); + slider.On().SetUpdateOnTap(true); layout.Add(slider); var handler = CreateHandler(layout); diff --git a/src/Core/src/Handlers/Slider/SliderHandler.iOS.cs b/src/Core/src/Handlers/Slider/SliderHandler.iOS.cs index f3b30bf8fbc8..1884516a0519 100644 --- a/src/Core/src/Handlers/Slider/SliderHandler.iOS.cs +++ b/src/Core/src/Handlers/Slider/SliderHandler.iOS.cs @@ -23,13 +23,18 @@ protected override void ConnectHandler(UISlider platformView) { base.ConnectHandler(platformView); _proxy.Connect(VirtualView, platformView); - _sliderTapRecognizer = null; } protected override void DisconnectHandler(UISlider platformView) { base.DisconnectHandler(platformView); _proxy.Disconnect(platformView); + if (_sliderTapRecognizer is not null) + { + PlatformView.RemoveGestureRecognizer(_sliderTapRecognizer); + _sliderTapRecognizer.Dispose(); + _sliderTapRecognizer = null; + } } internal void MapUpdateOnTap(bool isMapUpdateOnTapEnabled) From e972161c9a16383fe758ebda7de0266bea368057 Mon Sep 17 00:00:00 2001 From: Jakub Florkowski Date: Sun, 23 Mar 2025 02:16:06 +0100 Subject: [PATCH 4/5] Fixed a memory leak --- .../src/Handlers/Slider/SliderHandler.iOS.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Core/src/Handlers/Slider/SliderHandler.iOS.cs b/src/Core/src/Handlers/Slider/SliderHandler.iOS.cs index 1884516a0519..74f9c1037b04 100644 --- a/src/Core/src/Handlers/Slider/SliderHandler.iOS.cs +++ b/src/Core/src/Handlers/Slider/SliderHandler.iOS.cs @@ -31,7 +31,8 @@ protected override void DisconnectHandler(UISlider platformView) _proxy.Disconnect(platformView); if (_sliderTapRecognizer is not null) { - PlatformView.RemoveGestureRecognizer(_sliderTapRecognizer); + _sliderTapRecognizer.ShouldReceiveTouch = null; + platformView.RemoveGestureRecognizer(_sliderTapRecognizer); _sliderTapRecognizer.Dispose(); _sliderTapRecognizer = null; } @@ -41,18 +42,19 @@ internal void MapUpdateOnTap(bool isMapUpdateOnTapEnabled) { if (isMapUpdateOnTapEnabled) { - if (_sliderTapRecognizer == null) + if (_sliderTapRecognizer is null) { + var weakPlatformSlider = new WeakReference(PlatformView); + var weakVirtualSlider = new WeakReference(VirtualView); _sliderTapRecognizer = new UITapGestureRecognizer((recognizer) => { - var control = PlatformView; - if (control != null) + if (weakPlatformSlider.TryGetTarget(out var platformSlider) && weakVirtualSlider.TryGetTarget(out var slider)) { - var tappedLocation = recognizer.LocationInView(control); + var tappedLocation = recognizer.LocationInView(platformSlider); if (tappedLocation != default) { - var val = tappedLocation.X * control.MaxValue / control.Frame.Size.Width; - VirtualView.Value = val; + var val = tappedLocation.X * platformSlider.MaxValue / platformSlider.Frame.Size.Width; + slider.Value = val; } } }); From b92db33ec82bdfb8f0cf6f82fa77cd1abbd1e399 Mon Sep 17 00:00:00 2001 From: Gerald Versluis Date: Wed, 26 Nov 2025 15:58:10 +0100 Subject: [PATCH 5/5] Don't interfere with other gestures --- src/Core/src/Handlers/Slider/SliderHandler.iOS.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Core/src/Handlers/Slider/SliderHandler.iOS.cs b/src/Core/src/Handlers/Slider/SliderHandler.iOS.cs index 74f9c1037b04..b6b79cd05f0a 100644 --- a/src/Core/src/Handlers/Slider/SliderHandler.iOS.cs +++ b/src/Core/src/Handlers/Slider/SliderHandler.iOS.cs @@ -57,7 +57,12 @@ internal void MapUpdateOnTap(bool isMapUpdateOnTapEnabled) slider.Value = val; } } - }); + }) + { + CancelsTouchesInView = false, + ShouldRecognizeSimultaneously = (gesture, otherGesture) => true + }; + PlatformView.AddGestureRecognizer(_sliderTapRecognizer); } }