From 0384497cc41a39057ed25ef2a3ad84d3cdc8c3ba Mon Sep 17 00:00:00 2001 From: SyedAbdulAzeem Date: Wed, 22 Apr 2026 21:02:29 +0530 Subject: [PATCH 1/5] Fix: restore Entry clear button image when TextColor is reset to null on iOS --- .../src/Platform/iOS/TextFieldExtensions.cs | 33 ++++++++++- .../Handlers/Entry/EntryHandlerTests.iOS.cs | 58 +++++++++++++++++++ 2 files changed, 88 insertions(+), 3 deletions(-) diff --git a/src/Core/src/Platform/iOS/TextFieldExtensions.cs b/src/Core/src/Platform/iOS/TextFieldExtensions.cs index 107160fb73d9..831b0af431cb 100644 --- a/src/Core/src/Platform/iOS/TextFieldExtensions.cs +++ b/src/Core/src/Platform/iOS/TextFieldExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.CompilerServices; using CoreGraphics; using Foundation; using Microsoft.Maui.Graphics; @@ -9,6 +10,8 @@ namespace Microsoft.Maui.Platform { public static class TextFieldExtensions { + static readonly ConditionalWeakTable s_defaultClearButtonImages = new(); + public static void UpdateText(this UITextField textField, IEntry entry) { textField.Text = entry.Text; @@ -227,24 +230,48 @@ internal static void UpdateClearButtonColor(this UITextField textField, IEntry e { if (textField.ValueForKey(new NSString("clearButton")) is UIButton clearButton) { - UIImage defaultClearImage = clearButton.ImageForState(UIControlState.Highlighted); + var defaultClearImage = GetDefaultClearButtonImage(textField, clearButton); + + if (defaultClearImage is null) + { + return; + } if (entry.TextColor is null) { // Setting TintColor to null allows the system to automatically apply the appropriate color based on the current theme (light or dark mode) clearButton.TintColor = null; + clearButton.SetImage(defaultClearImage, UIControlState.Normal); + clearButton.SetImage(defaultClearImage, UIControlState.Highlighted); } else { - clearButton.TintColor = entry.TextColor.ToPlatform(); + var textColor = entry.TextColor.ToPlatform(); + clearButton.TintColor = textColor; - var tintedClearImage = GetClearButtonTintImage(defaultClearImage, entry.TextColor.ToPlatform()); + var tintedClearImage = GetClearButtonTintImage(defaultClearImage, textColor); clearButton.SetImage(tintedClearImage, UIControlState.Normal); clearButton.SetImage(tintedClearImage, UIControlState.Highlighted); } } } + static UIImage? GetDefaultClearButtonImage(UITextField textField, UIButton clearButton) + { + if (s_defaultClearButtonImages.TryGetValue(textField, out var defaultImage)) + { + return defaultImage; + } + + defaultImage = clearButton.ImageForState(UIControlState.Normal) + ?? clearButton.ImageForState(UIControlState.Highlighted); + + if (defaultImage is not null) + s_defaultClearButtonImages.Add(textField, defaultImage); + + return defaultImage; + } + internal static UIImage? GetClearButtonTintImage(UIImage image, UIColor color) { var size = image.Size; diff --git a/src/Core/tests/DeviceTests/Handlers/Entry/EntryHandlerTests.iOS.cs b/src/Core/tests/DeviceTests/Handlers/Entry/EntryHandlerTests.iOS.cs index 753c158cb38b..88e7d4070186 100644 --- a/src/Core/tests/DeviceTests/Handlers/Entry/EntryHandlerTests.iOS.cs +++ b/src/Core/tests/DeviceTests/Handlers/Entry/EntryHandlerTests.iOS.cs @@ -93,6 +93,61 @@ public async Task CharacterSpacingInitializesCorrectly() Assert.Equal(xplatCharacterSpacing, values.PlatformViewValue); } + [Fact(DisplayName = "Clear button image resets when TextColor is null")] + public async Task ClearButtonImageResetsWhenTextColorIsNull() + { + EntryStub entry = new EntryStub + { + Text = "MAUI", + ClearButtonVisibility = ClearButtonVisibility.WhileEditing, + TextColor = null + }; + + await AttachAndRun(entry, async (handler) => + { + await AssertEventually(() => handler.PlatformView.IsLoaded()); + handler.PlatformView.BecomeFirstResponder(); + + var clearButton = GetNativeClearButton(handler); + Assert.NotNull(clearButton); + + // Use ImageView.Image — reflects the actual displayed image (SetImage calls), + // unlike ImageForState which returns UIKit's internal cached value for this private button. + var defaultImage = clearButton.ImageView is not null + ? clearButton.ImageView.Image + : clearButton.ImageForState(UIControlState.Highlighted); + Assert.NotNull(defaultImage); + + // Apply purple text color — clear button should switch to the tinted (baked-in) image. + entry.TextColor = Colors.Purple; + handler.UpdateValue(nameof(IEntry.TextColor)); + + var tintedImage = clearButton.ImageView is not null + ? clearButton.ImageView.Image + : clearButton.ImageForState(UIControlState.Highlighted); + Assert.NotNull(tintedImage); + + // Guard: tinting must have changed the underlying bitmap, otherwise the final assertion would + // be vacuously true. CGImage.Handle compares the native Core Graphics object pointer, which + // is stable even when UIKit wraps the same native image in different managed objects. + Assert.NotEqual(defaultImage.CGImage?.Handle, tintedImage.CGImage?.Handle); + + // Reset TextColor to null — without the fix the tinted image stays; with the fix it restores. + entry.TextColor = null; + handler.UpdateValue(nameof(IEntry.TextColor)); + + var resetImage = clearButton.ImageView is not null + ? clearButton.ImageView.Image + : clearButton.ImageForState(UIControlState.Highlighted); + Assert.NotNull(resetImage); + + // Core assertion: the original default image's underlying CGImage must be restored. + // UIImage pointer identity (Handle) is unreliable after SetImage()/ImageView.Image + // round-trips — UIKit may vend a different managed wrapper for the same native object. + Assert.Equal(defaultImage.CGImage?.Handle, resetImage.CGImage?.Handle); + }); + } + [Fact] public async Task NextMovesToNextEntry() { @@ -832,6 +887,9 @@ bool GetNativeIsChatKeyboard(EntryHandler entryHandler) bool GetNativeClearButtonVisibility(EntryHandler entryHandler) => GetNativeEntry(entryHandler).ClearButtonMode == UITextFieldViewMode.WhileEditing; + static UIButton GetNativeClearButton(EntryHandler entryHandler) => + GetNativeEntry(entryHandler).ValueForKey(new NSString("clearButton")) as UIButton; + UITextAlignment GetNativeHorizontalTextAlignment(EntryHandler entryHandler) => GetNativeEntry(entryHandler).TextAlignment; From 1c7d73bb32dc8a6425856a664220be2fa2b70ff0 Mon Sep 17 00:00:00 2001 From: SyedAbdulAzeem Date: Mon, 27 Apr 2026 11:56:01 +0530 Subject: [PATCH 2/5] Fix clear button staying tinted after TextColor reset to null on iOS --- .../src/Platform/iOS/TextFieldExtensions.cs | 35 +++---------------- 1 file changed, 5 insertions(+), 30 deletions(-) diff --git a/src/Core/src/Platform/iOS/TextFieldExtensions.cs b/src/Core/src/Platform/iOS/TextFieldExtensions.cs index 831b0af431cb..d5e21b252635 100644 --- a/src/Core/src/Platform/iOS/TextFieldExtensions.cs +++ b/src/Core/src/Platform/iOS/TextFieldExtensions.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.CompilerServices; using CoreGraphics; using Foundation; using Microsoft.Maui.Graphics; @@ -10,8 +9,6 @@ namespace Microsoft.Maui.Platform { public static class TextFieldExtensions { - static readonly ConditionalWeakTable s_defaultClearButtonImages = new(); - public static void UpdateText(this UITextField textField, IEntry entry) { textField.Text = entry.Text; @@ -230,48 +227,26 @@ internal static void UpdateClearButtonColor(this UITextField textField, IEntry e { if (textField.ValueForKey(new NSString("clearButton")) is UIButton clearButton) { - var defaultClearImage = GetDefaultClearButtonImage(textField, clearButton); - - if (defaultClearImage is null) - { - return; - } + UIImage defaultClearImage = clearButton.ImageForState(UIControlState.Highlighted); if (entry.TextColor is null) { // Setting TintColor to null allows the system to automatically apply the appropriate color based on the current theme (light or dark mode) clearButton.TintColor = null; - clearButton.SetImage(defaultClearImage, UIControlState.Normal); - clearButton.SetImage(defaultClearImage, UIControlState.Highlighted); + clearButton.SetImage(null, UIControlState.Normal); + clearButton.SetImage(null, UIControlState.Highlighted); } else { - var textColor = entry.TextColor.ToPlatform(); - clearButton.TintColor = textColor; + clearButton.TintColor = entry.TextColor.ToPlatform(); - var tintedClearImage = GetClearButtonTintImage(defaultClearImage, textColor); + var tintedClearImage = GetClearButtonTintImage(defaultClearImage, entry.TextColor.ToPlatform()); clearButton.SetImage(tintedClearImage, UIControlState.Normal); clearButton.SetImage(tintedClearImage, UIControlState.Highlighted); } } } - static UIImage? GetDefaultClearButtonImage(UITextField textField, UIButton clearButton) - { - if (s_defaultClearButtonImages.TryGetValue(textField, out var defaultImage)) - { - return defaultImage; - } - - defaultImage = clearButton.ImageForState(UIControlState.Normal) - ?? clearButton.ImageForState(UIControlState.Highlighted); - - if (defaultImage is not null) - s_defaultClearButtonImages.Add(textField, defaultImage); - - return defaultImage; - } - internal static UIImage? GetClearButtonTintImage(UIImage image, UIColor color) { var size = image.Size; From 1aff203606817a72b7344c3fec27c14dd1c593b1 Mon Sep 17 00:00:00 2001 From: SyedAbdulAzeem Date: Tue, 28 Apr 2026 11:00:51 +0530 Subject: [PATCH 3/5] Add null guard in GetClearButtonTintImage for defensive safety --- src/Core/src/Platform/iOS/TextFieldExtensions.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Core/src/Platform/iOS/TextFieldExtensions.cs b/src/Core/src/Platform/iOS/TextFieldExtensions.cs index d5e21b252635..669a4dabf706 100644 --- a/src/Core/src/Platform/iOS/TextFieldExtensions.cs +++ b/src/Core/src/Platform/iOS/TextFieldExtensions.cs @@ -227,7 +227,7 @@ internal static void UpdateClearButtonColor(this UITextField textField, IEntry e { if (textField.ValueForKey(new NSString("clearButton")) is UIButton clearButton) { - UIImage defaultClearImage = clearButton.ImageForState(UIControlState.Highlighted); + UIImage? defaultClearImage = clearButton.ImageForState(UIControlState.Highlighted); if (entry.TextColor is null) { @@ -247,8 +247,13 @@ internal static void UpdateClearButtonColor(this UITextField textField, IEntry e } } - internal static UIImage? GetClearButtonTintImage(UIImage image, UIColor color) + internal static UIImage? GetClearButtonTintImage(UIImage? image, UIColor color) { + if (image is null) + { + return null; + } + var size = image.Size; var renderer = new UIGraphicsImageRenderer(size, new UIGraphicsImageRendererFormat() From 618a051066107dcfe4c804ff8846d15f2a7ea631 Mon Sep 17 00:00:00 2001 From: SyedAbdulAzeem Date: Wed, 29 Apr 2026 18:18:13 +0530 Subject: [PATCH 4/5] test: address review feedback on ClearButtonImageResetsWhenTextColorIsNull --- .../src/Platform/iOS/TextFieldExtensions.cs | 3 +- .../Handlers/Entry/EntryHandlerTests.iOS.cs | 34 +++++-------------- 2 files changed, 10 insertions(+), 27 deletions(-) diff --git a/src/Core/src/Platform/iOS/TextFieldExtensions.cs b/src/Core/src/Platform/iOS/TextFieldExtensions.cs index 669a4dabf706..1b80bc02b86c 100644 --- a/src/Core/src/Platform/iOS/TextFieldExtensions.cs +++ b/src/Core/src/Platform/iOS/TextFieldExtensions.cs @@ -227,8 +227,6 @@ internal static void UpdateClearButtonColor(this UITextField textField, IEntry e { if (textField.ValueForKey(new NSString("clearButton")) is UIButton clearButton) { - UIImage? defaultClearImage = clearButton.ImageForState(UIControlState.Highlighted); - if (entry.TextColor is null) { // Setting TintColor to null allows the system to automatically apply the appropriate color based on the current theme (light or dark mode) @@ -238,6 +236,7 @@ internal static void UpdateClearButtonColor(this UITextField textField, IEntry e } else { + UIImage? defaultClearImage = clearButton.ImageForState(UIControlState.Highlighted); clearButton.TintColor = entry.TextColor.ToPlatform(); var tintedClearImage = GetClearButtonTintImage(defaultClearImage, entry.TextColor.ToPlatform()); diff --git a/src/Core/tests/DeviceTests/Handlers/Entry/EntryHandlerTests.iOS.cs b/src/Core/tests/DeviceTests/Handlers/Entry/EntryHandlerTests.iOS.cs index 88e7d4070186..cb69c9e77923 100644 --- a/src/Core/tests/DeviceTests/Handlers/Entry/EntryHandlerTests.iOS.cs +++ b/src/Core/tests/DeviceTests/Handlers/Entry/EntryHandlerTests.iOS.cs @@ -106,45 +106,29 @@ public async Task ClearButtonImageResetsWhenTextColorIsNull() await AttachAndRun(entry, async (handler) => { await AssertEventually(() => handler.PlatformView.IsLoaded()); - handler.PlatformView.BecomeFirstResponder(); + Assert.True(handler.PlatformView.BecomeFirstResponder()); + await AssertEventually(() => handler.PlatformView.IsFirstResponder); var clearButton = GetNativeClearButton(handler); Assert.NotNull(clearButton); - // Use ImageView.Image — reflects the actual displayed image (SetImage calls), - // unlike ImageForState which returns UIKit's internal cached value for this private button. - var defaultImage = clearButton.ImageView is not null - ? clearButton.ImageView.Image - : clearButton.ImageForState(UIControlState.Highlighted); + var defaultImage = clearButton.ImageForState(UIControlState.Normal); Assert.NotNull(defaultImage); + Assert.Equal(UIImageRenderingMode.AlwaysOriginal, defaultImage.RenderingMode); - // Apply purple text color — clear button should switch to the tinted (baked-in) image. entry.TextColor = Colors.Purple; handler.UpdateValue(nameof(IEntry.TextColor)); - var tintedImage = clearButton.ImageView is not null - ? clearButton.ImageView.Image - : clearButton.ImageForState(UIControlState.Highlighted); + var tintedImage = clearButton.ImageForState(UIControlState.Normal); Assert.NotNull(tintedImage); + Assert.Equal(UIImageRenderingMode.Automatic, tintedImage.RenderingMode); - // Guard: tinting must have changed the underlying bitmap, otherwise the final assertion would - // be vacuously true. CGImage.Handle compares the native Core Graphics object pointer, which - // is stable even when UIKit wraps the same native image in different managed objects. - Assert.NotEqual(defaultImage.CGImage?.Handle, tintedImage.CGImage?.Handle); - - // Reset TextColor to null — without the fix the tinted image stays; with the fix it restores. entry.TextColor = null; handler.UpdateValue(nameof(IEntry.TextColor)); - - var resetImage = clearButton.ImageView is not null - ? clearButton.ImageView.Image - : clearButton.ImageForState(UIControlState.Highlighted); - Assert.NotNull(resetImage); - // Core assertion: the original default image's underlying CGImage must be restored. - // UIImage pointer identity (Handle) is unreliable after SetImage()/ImageView.Image - // round-trips — UIKit may vend a different managed wrapper for the same native object. - Assert.Equal(defaultImage.CGImage?.Handle, resetImage.CGImage?.Handle); + var resetImage = clearButton.ImageForState(UIControlState.Normal); + Assert.NotNull(resetImage); + Assert.Equal(UIImageRenderingMode.AlwaysOriginal, resetImage.RenderingMode); }); } From 3f378d6bf43db4a36c478c475f3c3bf118e5ee10 Mon Sep 17 00:00:00 2001 From: SyedAbdulAzeem Date: Thu, 30 Apr 2026 14:08:33 +0530 Subject: [PATCH 5/5] Add clarifying comments to fix and test for clear button image restoration --- src/Core/src/Platform/iOS/TextFieldExtensions.cs | 12 ++++++++++-- .../Handlers/Entry/EntryHandlerTests.iOS.cs | 11 +++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/Core/src/Platform/iOS/TextFieldExtensions.cs b/src/Core/src/Platform/iOS/TextFieldExtensions.cs index 1b80bc02b86c..7d1040697be5 100644 --- a/src/Core/src/Platform/iOS/TextFieldExtensions.cs +++ b/src/Core/src/Platform/iOS/TextFieldExtensions.cs @@ -231,17 +231,25 @@ internal static void UpdateClearButtonColor(this UITextField textField, IEntry e { // Setting TintColor to null allows the system to automatically apply the appropriate color based on the current theme (light or dark mode) clearButton.TintColor = null; + // SetImage(null) releases the custom tinted bitmap so UIKit restores its system default. + // The color path (else branch) reads ImageForState(.Highlighted) to get that original + // image as the source for tinting. Without these calls, TintColor=null has no visual effect. clearButton.SetImage(null, UIControlState.Normal); clearButton.SetImage(null, UIControlState.Highlighted); } else { + // On a null→color transition, UIKit restores the system image after SetImage(null), + // so ImageForState(Highlighted) returns the system clear button image as the tinting source. UIImage? defaultClearImage = clearButton.ImageForState(UIControlState.Highlighted); clearButton.TintColor = entry.TextColor.ToPlatform(); var tintedClearImage = GetClearButtonTintImage(defaultClearImage, entry.TextColor.ToPlatform()); - clearButton.SetImage(tintedClearImage, UIControlState.Normal); - clearButton.SetImage(tintedClearImage, UIControlState.Highlighted); + if (tintedClearImage is not null) + { + clearButton.SetImage(tintedClearImage, UIControlState.Normal); + clearButton.SetImage(tintedClearImage, UIControlState.Highlighted); + } } } } diff --git a/src/Core/tests/DeviceTests/Handlers/Entry/EntryHandlerTests.iOS.cs b/src/Core/tests/DeviceTests/Handlers/Entry/EntryHandlerTests.iOS.cs index cb69c9e77923..1cb5c3f8895e 100644 --- a/src/Core/tests/DeviceTests/Handlers/Entry/EntryHandlerTests.iOS.cs +++ b/src/Core/tests/DeviceTests/Handlers/Entry/EntryHandlerTests.iOS.cs @@ -126,9 +126,20 @@ await AttachAndRun(entry, async (handler) => entry.TextColor = null; handler.UpdateValue(nameof(IEntry.TextColor)); + // UIKit restores the original AlwaysOriginal system image when SetImage(null) is called + // on this private clearButton — ImageForState(.Highlighted) must return non-null for re-tinting to work. var resetImage = clearButton.ImageForState(UIControlState.Normal); Assert.NotNull(resetImage); Assert.Equal(UIImageRenderingMode.AlwaysOriginal, resetImage.RenderingMode); + + entry.TextColor = Colors.Blue; + handler.UpdateValue(nameof(IEntry.TextColor)); + + // Verify re-tinting works after reset (null→color→null→color) + // Confirms ImageForState(.Highlighted) returns the original after SetImage(null) + var retintedImage = clearButton.ImageForState(UIControlState.Normal); + Assert.NotNull(retintedImage); + Assert.Equal(UIImageRenderingMode.Automatic, retintedImage.RenderingMode); }); }