dotnet / dotnet/maui

[iOS] ButtonHandler now clears platform-view styling set by custom handlers when Button.Background/TextColor are null (regression in 10.0.80)

Open
#36,749 1 comment 0 reactions 0 assignees View on GitHub
area-controls-button i/regression partner/syncfusion platform/ios regressed-in-10.0.80 s/triaged s/verified
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.