dotnet / maui

.NET MAUI is the .NET Multi-platform App UI, a framework for building native device applications spanning mobile, tablet, and desktop.
https://dot.net/maui
MIT License
22.28k stars 1.76k forks source link

[Switch] Switch Toggle: OnColor Not set correctly and ThumbColor Not Reset When Toggled Off #19883

Open MAUIoxo opened 10 months ago

MAUIoxo commented 10 months ago

Description

I have a Switch in a Grid within a DataTemplate of a CollectionView with my own ThumbColor defined like in the example below:

<CollectionView.ItemTemplate>
    <DataTemplate x:DataType="{x:Type databaseModels:Band}">
        <Grid ColumnDefinitions="*, Auto" RowDefinitions="Auto" Padding="{OnPlatform Default='5, 0, 5, 0', iOS='5, 0, 5, 15'}">
            <Label Grid.Row="0" Grid.Column="0" Text="{Binding Band.Name}" VerticalOptions="Center" />
            <Switch Grid.Row="0" Grid.Column="1" IsEnabled="True" IsToggled="{Binding IsSelected, Mode=TwoWay}" ThumbColor="{StaticResource Gray100}" OnColor="{StaticResource Orange}" VerticalOptions="Center" HorizontalOptions="Center" Margin="{OnPlatform Android='0, -5, 0, -5'}"/>
        </Grid>
    </DataTemplate>
</CollectionView.ItemTemplate>

Initially the Switch looks like below:

grafik

First of all, when the Switch is toggled, it gets its defined wrong OnColor (should be Orange):

grafik

After that when it is toggled off again, the defined ThumbColor="{StaticResource Gray100}" is not set back and the Switch looks as follows:

grafik

Steps to Reproduce

  1. Define a ThumbColor and an OnColor for a Switch
  2. See how the Color looks like when the Switch is not toggled initially
  3. Toggle the Switch and it will apply the wrong OnColor (should be Orange)
  4. Toggled again to state "Off" and it will not apply the defined ThumbColor again

In the linked example Project from repository:

  1. Launch project
  2. Click on the DotNetBot Icon
  3. Defined ThumbColor is visible
  4. Toggle switch on and color does not change to correct OnColor (should be Orange)
  5. Toggle switch off and color is changed to a system default color, but not the specified ThumbColor again

Hint: The View containing the Switch with the ThumbColor and OnColor is in ..\Pages\Views**BottomSheetContentView.xaml**

Link to public reproduction project repository

ThumbColorNotReset_19883

Version with bug

8.0.6-nightly.9863

Is this a regression from previous behavior?

No, this is something new

Last version that worked well

7.0.101

Affected platforms

iOS, Android

Affected platform versions

No response

Did you find any workaround?

By the time this issue was reported, I did not find a workaround.

On April 13th 2024, I found the simples way that solved it for me, by just using a CustomSwitch that derived from Switch and use this in my XAML which fixed it for me as you can see below.

Relevant log output

No response

ghost commented 10 months ago

Hi @MAUIoxo. We have added the "s/needs-repro" label to this issue, which indicates that we require steps and sample code to reproduce the issue before we can take further action. Please try to create a minimal sample project/solution or code samples which reproduce the issue, ideally as a GitHub repo that we can clone. See more details about creating repros here: https://github.com/dotnet/maui/blob/main/.github/repro.md

This issue will be closed automatically in 7 days if we do not hear back from you by then - please feel free to re-open it if you come back to this issue after that time.

jsuarezruiz commented 10 months ago

Could be related? https://github.com/dotnet/maui/issues/19882

MAUIoxo commented 10 months ago

I added an example project that shows this Bug: ThumbColorNotReset_19883

I don't think that #19882 is related. The Switch is within a Grid, but the other Bug is about Grid.Resources that overwrite globally defined Styles (e.g. for Labels) completely when a property is defined as a Grid.Resource (intention e.g. define a HorizontalOption for all Labels within this Grid) and this is not the same here

MAUIoxo commented 10 months ago

First of all, I fooled myself by not choosing a more distinct OnColor and did not see that also the OnColor is not set! The OnColor should be Orange, but is not assigned. Can also be seen in the same project I attached

XamlTest commented 9 months ago

Verified this on Visual Studio Enterprise 17.10.0 Preview 1(8.0.6). Repro on Android 14.0-API34 and iOS 17.2 with below Project: ThumbColorNotReset.zip

BlueRaja commented 9 months ago

Related: https://github.com/dotnet/maui/issues/20278 https://github.com/dotnet/maui/issues/19380

It doesn't appear to be related to Grids. Even the most basic example fails 100% of the time:

<Switch ThumbColor="Red" OnColor="Green"/>

Do you guys not have any integration tests? Or use MAUI internally? This wouldn't have made it past even cursory testing.

MAUIoxo commented 9 months ago

That’s also one of my biggest blames that things that used to work „somehow“ got broken and obviously nobody detected it - just the „Clients“ (we). What does that tell us? 😉

kubaflo commented 8 months ago

@MAUIoxo this will probably be fixed in this PR https://github.com/dotnet/maui/pull/20346

MAUIoxo commented 7 months ago

As also described in #20346 I worked on a solution with a CustomSwitchHandler for iOS which you find below. While doing that, I found out that I just have to use a trivial thing and it worked for me just simply by using a CustomSwitch that derived from Switch. By doing that, I could simply use this CustomSwitch in my XAML and then use the Color settings from OnColor and ThumbColor and it worked.

Here are both ways which were tested with <MauiVersion>8.0.40-nightly.10485</MauiVersion>.


This is the implementation of the CustomSwitchHandler for iOS:


CustomSwitch:

namespace OptimizerApp.Pages.Views.Controls.CustomSwitch
{
    public class CustomSwitch : Switch
    {

    }
}


Integration in XAML Code:

...
<constrols:CustomSwitch ... IsToggled="{Binding IsSelected, Mode=TwoWay}" ThumbColor="{StaticResource Gray100}" OnColor="{StaticResource DarkOrange1}" />
...


CustomSwitchHandler:

#if IOS

using OptimizerApp.Pages.Views.Controls.CustomSwitch;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;
using UIKit;

namespace OptimizerApp.Platforms.iOS.ControlHandlers
{
    public partial class CustomSwitchHandler : ViewHandler<CustomSwitch, UISwitch>
    {
        /// <summary>
        /// Configures property mappings for the CustomSwitch and binds UI properties to platform-specific handlers.
        /// This mapper directs changes in MAUI's CustomSwitch properties to appropriate methods that
        /// update the native iOS UISwitch, ensuring UI consistency and responsiveness:
        /// - 'IsToggled' updates the switch's active state.
        /// - 'OnColor' adjusts the color when the switch is on.
        /// - 'ThumbColor' changes the color of the switch thumb.
        /// These mappings ensure that the visual state of the UISwitch is synchronized with the CustomSwitch.
        /// </summary>
        public static PropertyMapper<CustomSwitch, CustomSwitchHandler> CustomSwitchMapper = new PropertyMapper<CustomSwitch, CustomSwitchHandler>
        {
            [nameof(CustomSwitch.IsToggled)] = MapIsToggled,
            [nameof(CustomSwitch.OnColor)] = MapOnColor,
            [nameof(CustomSwitch.ThumbColor)] = MapThumbColor
        };

        public CustomSwitchHandler() : base(CustomSwitchMapper)
        {
        }

        /// <summary>
        /// Creates the native UISwitch view
        /// </summary>
        /// <returns>The newly created UISwitch instance</returns>
        protected override UISwitch CreatePlatformView()
        {
            return new UISwitch();
        }

        /// <summary>
        /// Connects this handler to the UISwitch, setting up event handlers and initial state
        /// </summary>
        /// <param name="platformView">The UISwitch to connect</param>
        protected override void ConnectHandler(UISwitch platformView)
        {
            base.ConnectHandler(platformView);

            platformView.ValueChanged += OnSwitchValueChanged;
            platformView.On = VirtualView.IsToggled;    // Synchronize the UISwitch's state with the IsToggled property of the MAUI CustomSwitch

            SetSwitchColors(platformView, VirtualView);
        }

        /// <summary>
        /// Disconnects this handler from the UISwitch and clean up the event handlers
        /// </summary>
        /// <param name="platformView">The UISwitch to disconnect</param>
        protected override void DisconnectHandler(UISwitch platformView)
        {
            platformView.ValueChanged -= OnSwitchValueChanged;
            base.DisconnectHandler(platformView);

            // Reset UI properties
            ResetSwitchColors(platformView);
        }

        /// <summary>
        /// Sets the ThumbColor and OnTintColor of the UISwitch based on the properties of the CustomSwitch
        /// </summary>
        /// <param name="uiSwitch">The native UISwitch</param>
        /// <param name="customSwitch">The MAUI CustomSwitch</param>
        private static void SetSwitchColors(UISwitch uiSwitch, CustomSwitch customSwitch)
        {
            // Setting the null-values will set it to default values
            uiSwitch.ThumbTintColor = customSwitch.ThumbColor != default(Color) ? customSwitch.ThumbColor.ToPlatform() : null;
            uiSwitch.OnTintColor = customSwitch.OnColor != default(Color) ? customSwitch.OnColor.ToPlatform() : null;
        }

        /// <summary>
        /// Resets the ThumbColor and OnTintColor properties of the UISwitch to defaults
        /// </summary>
        /// <param name="uiSwitch">The UISwitch to reset</param>
        private void ResetSwitchColors(UISwitch uiSwitch)
        {
            uiSwitch.ThumbTintColor = null;
            uiSwitch.OnTintColor = null;
        }

        /// <summary>
        /// Map changes in the OnColor property to the UISwitch
        /// </summary>
        public static void MapOnColor(CustomSwitchHandler handler, CustomSwitch customSwitch)
        {
            if (handler.PlatformView != null)
            {
                handler.PlatformView.OnTintColor = customSwitch.OnColor.ToPlatform();
            }
        }

        /// <summary>
        /// Map changes in the ThumbColor property to the UISwitch
        /// </summary>
        public static void MapThumbColor(CustomSwitchHandler handler, CustomSwitch customSwitch)
        {
            if (handler.PlatformView != null)
            {
                handler.PlatformView.ThumbTintColor = customSwitch.ThumbColor.ToPlatform();
            }
        }

        /// <summary>
        /// Map changes in the IsToggled property to the UISwitch
        /// </summary>
        public static void MapIsToggled(CustomSwitchHandler handler, CustomSwitch customSwitch)
        {
            if (handler.PlatformView != null)
            {
                handler.PlatformView.On = customSwitch.IsToggled;                
                handler.UpdateSwitchColors(handler.PlatformView, customSwitch);     // Update colors when switch is toggled/untoggled
            }
        }

        private void OnSwitchValueChanged(object sender, EventArgs e)
        {
            var uiSwitch = (UISwitch)sender;
            VirtualView.IsToggled = uiSwitch.On;

            UpdateSwitchColors(uiSwitch, VirtualView);
        }

        /// <summary>
        /// Update the OnTintColor and ThumbColor with the colors defined in the customSwitch
        /// </summary>
        private void UpdateSwitchColors(UISwitch uiSwitch, CustomSwitch customSwitch)
        {
            if (uiSwitch.On)
            {
                if (customSwitch.OnColor != default(Color))
                {
                    uiSwitch.OnTintColor = customSwitch.OnColor.ToPlatform();
                }
            }

            if (customSwitch.ThumbColor != default(Color))
            {
                uiSwitch.ThumbTintColor = customSwitch.ThumbColor.ToPlatform();
            }
        }
    }
}

#endif


MauiProgram.cs:

...
#if IOS
using OptimizerApp.Platforms.iOS.ControlHandlers;
#endif
...

var builder = MauiApp.CreateBuilder();
builder
    .UseMauiApp<App>()
    ...
    .ConfigureMauiHandlers(handlers =>
    {
        // The handler will only be called if the target platform is iOS
#if IOS
        handlers.AddHandler<CustomSwitch, CustomSwitchHandler>();
#endif
    });
...


Result:

CustomSwitchHandler



BUT: As mentioned above, it even does not have to be this full blown implementation. As I found out, it was sufficient to just use the simple CustomSwitch above which simply derives from Switch and use this in XAML with corresponding Color settings.

Here are the before and after versions on iOS and Android :


Simple Switch iOS:

SwitchiOS


CustomSwitch iOS:

CustomSwitchiOS



Simple Switch Android:

SwitchAndroid


CustomSwitch Android:

CustomSwitchHandlerAndroid


pierre01 commented 2 weeks ago

I have the same issue wit Android