[iOS] ButtonHandler now clears platform-view styling set by custom handlers when Button.Background/TextColor are null (regression in 10.0.80)
- Dominant language
- C#
- Stars
- 23.3k
- Forks
- 2k
- Avg merge
- 1d 10h
- Merged PRs (30d)
- 297
Description
# Description
A supported customization pattern is to subclass `ButtonHandler` and return a self-styled `UIButton` subclass from `CreatePlatformView()` — the native subclass sets its own `BackgroundColor`, title colors, shadow, etc. in its constructor, and the cross-platform `Button` deliberately leaves `Background`/`TextColor` unset.
Up to and including 10.0.71, the iOS `ButtonHandler` had no platform `MapBackground`, so background mapping fell through to `ViewExtensions.UpdateBackground(UIView, Paint?)`, which returns early for a null/empty paint on non-layout views. Native styling set in the platform view's constructor was therefore preserved.
#33346 (shipped in 10.0.80) added an iOS `MapBackground` that routes to a new (internal) `ButtonExtensions.UpdateBackground(UIButton, Paint?)`, which does:
```csharp
if (paint.IsNullOrEmpty())
{
// Reset to clear background for buttons when paint is null.
platformButton.BackgroundColor = UIColor.Clear;
return;
}
```
So immediately after `CreatePlatformView()`, the initial property mapping wipes the platform view's constructor-set `BackgroundColor` to `UIColor.Clear`. The button renders invisible (in our app: only its `Layer.Shadow` remains visible) until something re-applies the color — e.g. a `Highlighted` round-trip on first tap.
The same PR gave `UpdateTextColor` a sibling behavior: when `ITextStyle.TextColor` is null and the button is attached to a window, it now clears the `SetTitleColor(...)` values for Normal/Highlighted/Disabled and re-tints from the window, which likewise discards title colors a self-styled platform view set natively.
This is a breaking behavior change for any `ButtonHandler` subclass with a self-styled platform view: "no cross-platform value set" used to mean "leave the platform view alone" and now means "reset to defaults".
A secondary papercut: the new iOS `ButtonHandler.MapBackground` and `ButtonExtensions.UpdateBackground(UIButton, Paint?)` are `internal` ("TODO: Make this public in .NET 11"), so a derived handler cannot delegate to the stock mapping to compose its own behavior — the workaround has to re-implement the non-null path via the public `ViewExtensions.UpdateBackground`.
# Steps to Reproduce
1. Create a `UIButton` subclass that styles itself in its constructor:
```csharp
public class NativeStyledButton : UIButton
{
public NativeStyledButton()
{
BackgroundColor = UIColor.Cyan;
SetTitleColor(UIColor.DarkGray, UIControlState.Normal);
}
}
```
2. Register a `ButtonHandler` subclass that creates it:
```csharp
public class NativeStyledButtonHandler() : ButtonHandler(Mapper)
{
protected override UIButton CreatePlatformView() => new NativeStyledButton();
}
// in MauiProgram:
handlers.AddHandler();
```
3. Put `` on a page **without** setting `Background`/`BackgroundColor`/`TextColor` on the control or through an applicable style.
4. Run on iOS.
## Expected Behavior
The button renders with the cyan background and dark-gray title set by the platform view's constructor (behavior up to 10.0.71).
## Actual Behavior
The button's background is reset to `UIColor.Clear` by the initial `Background` mapping, so it renders transparent. Tapping the button (or any `Highlighted`/state round-trip that happens to re-apply colors in the subclass) restores the expected look, which makes the symptom look like "button doesn't render its normal state until pressed once".
# Link to public reproduction project repository
No repro project attached: the mechanism is fully traceable in source (the #33346 code path quoted above), and the snippets in "Steps to Reproduce" are a complete minimal repro. Happy to provide a project if needed.
# Version with bug
10.0.80 SR8
# Last version that worked well
10.0.60 SR6 (verified; 10.0.71 expected unaffected — #33346 first shipped in 10.0.80 per the release notes)
# Affected platforms
iOS (the same PR also reworked the Android `ButtonHandler`; not tested there)
# Affected platform versions
Reproduced on iOS 26.2 and 26.5 simulators (net10.0-ios, Xcode 26.6); the mechanism is in the managed handler code, so it is not tied to an OS/SDK version.
# Did you find any workaround?
Chain a mapper that only applies non-null values, restoring the pre-10.0.80 contract:
```csharp
internal static class SelfStyledButtonMapper
{
internal static readonly IPropertyMapper Mapper =
new PropertyMapper(ButtonHandler.Mapper)
{
[nameof(IView.Background)] = MapBackground,
[nameof(ITextStyle.TextColor)] = MapTextColor,
};
static void MapBackground(IButtonHandler handler, IButton button)
{
if (button.Background is not null)
handler.PlatformView?.UpdateBackground(button.Background); // public ViewExtensions overload
}
static void MapTextColor(IButtonHandler handler, IButton button)
{
if (button is ITextStyle { TextColor: not null } textStyle)
ButtonHandler.MapTextColor(handler, textStyle);
}
}
public class NativeStyledButtonHandler() : ButtonHandler(SelfStyledButtonMapper.Mapper) { ... }
```
# Relevant log output
None (no exception — purely a rendering/state issue).
Contributor guide
Research direction
Start by locating the iOS ButtonHandler.MapBackground and ButtonExtensions.UpdateBackground(UIButton, Paint?) entry points described in the issue, then compare them with the prior ViewExtensions.UpdateBackground behavior. Reproduce the NativeStyledButton example and verify that null Background and TextColor values preserve constructor-set native styling while non-null values still map normally.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp, ios
- Domain
- mobile
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100