diff --git a/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs b/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs index 17bcee08117d..a25ca718c0e9 100644 --- a/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs +++ b/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs @@ -40,6 +40,7 @@ void SetupBuilder() handlers.AddHandler(); handlers.AddHandler(); handlers.AddHandler(); + handlers.AddHandler(); handlers.AddHandler(); handlers.AddHandler(); handlers.AddHandler(); @@ -66,6 +67,7 @@ void SetupBuilder() [InlineData(typeof(Polyline))] [InlineData(typeof(RefreshView))] [InlineData(typeof(ScrollView))] + [InlineData(typeof(SearchBar))] [InlineData(typeof(SwipeView))] [InlineData(typeof(TimePicker))] [InlineData(typeof(WebView))] diff --git a/src/Core/src/Handlers/SearchBar/SearchBarHandler.iOS.cs b/src/Core/src/Handlers/SearchBar/SearchBarHandler.iOS.cs index 6b22b2ed7b20..6ae170ad491d 100644 --- a/src/Core/src/Handlers/SearchBar/SearchBarHandler.iOS.cs +++ b/src/Core/src/Handlers/SearchBar/SearchBarHandler.iOS.cs @@ -8,6 +8,7 @@ namespace Microsoft.Maui.Handlers public partial class SearchBarHandler : ViewHandler { UITextField? _editor; + readonly MauiSearchBarProxy _proxy = new(); public UITextField? QueryEditor => _editor; @@ -23,35 +24,14 @@ protected override MauiSearchBar CreatePlatformView() protected override void ConnectHandler(MauiSearchBar platformView) { - platformView.CancelButtonClicked += OnCancelClicked; - platformView.SearchButtonClicked += OnSearchButtonClicked; - platformView.TextSetOrChanged += OnTextPropertySet; - platformView.OnMovedToWindow += OnMovedToWindow; - platformView.ShouldChangeTextInRange += ShouldChangeText; - platformView.OnEditingStarted += OnEditingStarted; - platformView.EditingChanged += OnEditingChanged; - platformView.OnEditingStopped += OnEditingStopped; + _proxy.Connect(this, VirtualView, platformView); base.ConnectHandler(platformView); } - void OnMovedToWindow(object? sender, EventArgs e) - { - // The cancel button doesn't exist until the control has moved to the window - // so we fire this off again so it can set the color - UpdateValue(nameof(ISearchBar.CancelButtonColor)); - } - protected override void DisconnectHandler(MauiSearchBar platformView) { - platformView.CancelButtonClicked -= OnCancelClicked; - platformView.SearchButtonClicked -= OnSearchButtonClicked; - platformView.TextSetOrChanged -= OnTextPropertySet; - platformView.ShouldChangeTextInRange -= ShouldChangeText; - platformView.OnMovedToWindow -= OnMovedToWindow; - platformView.OnEditingStarted -= OnEditingStarted; - platformView.EditingChanged -= OnEditingChanged; - platformView.OnEditingStopped -= OnEditingStopped; + _proxy.Disconnect(platformView); base.DisconnectHandler(platformView); } @@ -166,56 +146,107 @@ public static void MapKeyboard(ISearchBarHandler handler, ISearchBar searchBar) handler.PlatformView?.UpdateKeyboard(searchBar); } - void OnCancelClicked(object? sender, EventArgs args) + void UpdateCancelButtonVisibility() { - if (VirtualView != null) - VirtualView.Text = string.Empty; + if (PlatformView.ShowsCancelButton != VirtualView.ShouldShowCancelButton()) + UpdateValue(nameof(ISearchBar.CancelButtonColor)); } - void OnSearchButtonClicked(object? sender, EventArgs e) + class MauiSearchBarProxy { - VirtualView?.SearchButtonPressed(); - } + WeakReference? _handler; + WeakReference? _virtualView; - void OnTextPropertySet(object? sender, UISearchBarTextChangedEventArgs a) - { - VirtualView.UpdateText(a.SearchText); + ISearchBar? VirtualView => _virtualView is not null && _virtualView.TryGetTarget(out var v) ? v : null; - UpdateCancelButtonVisibility(); - } + public void Connect(SearchBarHandler handler, ISearchBar virtualView, MauiSearchBar platformView) + { + _handler = new(handler); + _virtualView = new(virtualView); + + platformView.CancelButtonClicked += OnCancelClicked; + platformView.SearchButtonClicked += OnSearchButtonClicked; + platformView.TextSetOrChanged += OnTextPropertySet; + platformView.OnMovedToWindow += OnMovedToWindow; + platformView.ShouldChangeTextInRange += ShouldChangeText; + platformView.OnEditingStarted += OnEditingStarted; + platformView.EditingChanged += OnEditingChanged; + platformView.OnEditingStopped += OnEditingStopped; + } - bool ShouldChangeText(UISearchBar searchBar, NSRange range, string text) - { - var newLength = searchBar?.Text?.Length + text.Length - range.Length; - return newLength <= VirtualView?.MaxLength; - } + public void Disconnect(MauiSearchBar platformView) + { + _virtualView = null; + + platformView.CancelButtonClicked -= OnCancelClicked; + platformView.SearchButtonClicked -= OnSearchButtonClicked; + platformView.TextSetOrChanged -= OnTextPropertySet; + platformView.ShouldChangeTextInRange -= ShouldChangeText; + platformView.OnMovedToWindow -= OnMovedToWindow; + platformView.OnEditingStarted -= OnEditingStarted; + platformView.EditingChanged -= OnEditingChanged; + platformView.OnEditingStopped -= OnEditingStopped; + } - void OnEditingStarted(object? sender, EventArgs e) - { - if (VirtualView is not null) - VirtualView.IsFocused = true; - } + void OnMovedToWindow(object? sender, EventArgs e) + { + // The cancel button doesn't exist until the control has moved to the window + // so we fire this off again so it can set the color + if (_handler is not null && _handler.TryGetTarget(out var handler)) + { + handler.UpdateValue(nameof(ISearchBar.CancelButtonColor)); + } + } - void OnEditingChanged(object? sender, EventArgs e) - { - if (VirtualView == null || _editor == null) - return; + void OnCancelClicked(object? sender, EventArgs args) + { + if (VirtualView is ISearchBar virtualView) + virtualView.Text = string.Empty; + } - VirtualView.UpdateText(_editor.Text); + void OnSearchButtonClicked(object? sender, EventArgs e) + { + VirtualView?.SearchButtonPressed(); + } - UpdateCancelButtonVisibility(); - } + void OnTextPropertySet(object? sender, UISearchBarTextChangedEventArgs a) + { + if (VirtualView is ISearchBar virtualView) + { + virtualView.UpdateText(a.SearchText); + + if (_handler is not null && _handler.TryGetTarget(out var handler)) + { + handler.UpdateCancelButtonVisibility(); + } + } + } - void OnEditingStopped(object? sender, EventArgs e) - { - if (VirtualView is not null) - VirtualView.IsFocused = false; - } + bool ShouldChangeText(UISearchBar searchBar, NSRange range, string text) + { + var newLength = searchBar?.Text?.Length + text.Length - range.Length; + return newLength <= VirtualView?.MaxLength; + } - void UpdateCancelButtonVisibility() - { - if (PlatformView.ShowsCancelButton != VirtualView.ShouldShowCancelButton()) - UpdateValue(nameof(ISearchBar.CancelButtonColor)); + void OnEditingStarted(object? sender, EventArgs e) + { + if (VirtualView is ISearchBar virtualView) + virtualView.IsFocused = true; + } + + void OnEditingChanged(object? sender, EventArgs e) + { + if (_handler is not null && _handler.TryGetTarget(out var handler)) + { + handler.UpdateCancelButtonVisibility(); + } + } + + void OnEditingStopped(object? sender, EventArgs e) + { + if (VirtualView is ISearchBar virtualView) + virtualView.IsFocused = false; + } } } } diff --git a/src/Core/src/Platform/iOS/MauiSearchBar.cs b/src/Core/src/Platform/iOS/MauiSearchBar.cs index a02ddd153e6f..c5a69408b2f5 100644 --- a/src/Core/src/Platform/iOS/MauiSearchBar.cs +++ b/src/Core/src/Platform/iOS/MauiSearchBar.cs @@ -33,7 +33,7 @@ protected internal MauiSearchBar(NativeHandle handle) : base(handle) // Native Changed doesn't fire when the Text Property is set in code // We use this event as a way to fire changes whenever the Text changes // via code or user interaction. - [UnconditionalSuppressMessage("Memory", "MA0001", Justification = "FIXME: https://github.com/dotnet/maui/pull/16383")] + [UnconditionalSuppressMessage("Memory", "MA0001", Justification = "Proven safe in test: MemoryTests.HandlerDoesNotLeak")] public event EventHandler? TextSetOrChanged; public override string? Text @@ -52,9 +52,9 @@ public override string? Text } } - [UnconditionalSuppressMessage("Memory", "MA0001", Justification = "FIXME: https://github.com/dotnet/maui/pull/16383")] + [UnconditionalSuppressMessage("Memory", "MA0001", Justification = "Proven safe in test: MemoryTests.HandlerDoesNotLeak")] internal event EventHandler? OnMovedToWindow; - [UnconditionalSuppressMessage("Memory", "MA0001", Justification = "FIXME: https://github.com/dotnet/maui/pull/16383")] + [UnconditionalSuppressMessage("Memory", "MA0001", Justification = "Proven safe in test: MemoryTests.HandlerDoesNotLeak")] internal event EventHandler? EditingChanged; public override void WillMoveToWindow(UIWindow? window) diff --git a/src/Core/src/Platform/iOS/SearchBarExtensions.cs b/src/Core/src/Platform/iOS/SearchBarExtensions.cs index ccd13d9758a5..ddb0793e626a 100644 --- a/src/Core/src/Platform/iOS/SearchBarExtensions.cs +++ b/src/Core/src/Platform/iOS/SearchBarExtensions.cs @@ -10,9 +10,25 @@ public static class SearchBarExtensions internal static UITextField? GetSearchTextField(this UISearchBar searchBar) { if (OperatingSystem.IsIOSVersionAtLeast(13)) + { return searchBar.SearchTextField; - else - return searchBar.GetSearchTextField(); + } + + // Search Subviews up to two levels deep + // https://stackoverflow.com/a/58056700 + foreach (var child in searchBar.Subviews) + { + if (child is UITextField childTextField) + return childTextField; + + foreach (var grandChild in child.Subviews) + { + if (grandChild is UITextField grandChildTextField) + return grandChildTextField; + } + } + + return null; } internal static void UpdateBackground(this UISearchBar uiSearchBar, ISearchBar searchBar)