Dark Mode - Using Xamarin Forms Effects

I have always been a fan of dark mode in apps and after WWDC 2019, I wanted to make sure my apps were ready for iOS 13.

With Xamarin Forms, it is relatively easy to support changing themes by using DynamicResources for Color declarations in separate ResourceDictionary. You can then switch out the dictionaries by accessing them in Application.Current.Resources.MergedDictionaries I could go into more details setting up resource dictionaries however I’d like to detail an issue I had with images and how I switched their Color for dark mode using Effects.

In my application I have used PNG image files for use on various buttons. These images are usually of a black outline, and therefore make it unsuitable for dark mode. I could switch out the image itself, however I would rather apply a “Tint” to the image. This is not available by default on the Xamarin Forms Button or ImageButton controls. To solve this issue I have created the following Xamarin Forms Effect.

This ButtonEffects class will need to go in you shared code.

public static class ButtonEffects
    {
        public static readonly BindableProperty TintColorProperty = BindableProperty.CreateAttached("TintColor", typeof(Color?), typeof(ButtonEffects), null, propertyChanged: OnTintColorChanged);

        private static void OnTintColorChanged(BindableObject bindable, object oldValue, object newValue)
        {
            if (!(bindable is Button) && !(bindable is ImageButton))
                return;

            var view = bindable as Element;
            

            if (newValue is Color tintColor)
            {
                view.Effects.Add(new ButtonEffect());
            }
            else
            {
                var toRemove = view.Effects.FirstOrDefault(e => e is ButtonEffect);
                if (toRemove != null)
                    view.Effects.Remove(toRemove);
            }
        }

        public static void SetTintColor(BindableObject view, Color? color)
        {
            view.SetValue(TintColorProperty, color);
        }

        public static Color? GetTintColor(BindableObject view)
        {
            return view.GetValue(TintColorProperty) as Color?;
        }

        class ButtonEffect : RoutingEffect
        {
            public ButtonEffect() : base("Rodda.ButtonEffect")
            {

            }
        }
    }

The below ButtonEffect class goes in the iOS project

public class ButtonEffect : PlatformEffect
{
	protected override void OnAttached()
	{
        if (Control is UIButton button)
        {
            UpdateTintColor();
        }
	}

	protected override void OnDetached()
	{
        if(Control is UIButton button && button.ImageView?.Image != null)
        {
            button.SetImage(button.ImageView.Image.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal), UIControlState.Normal);
        }
	}

		protected override void OnElementPropertyChanged(PropertyChangedEventArgs args)
		{
			if (args.PropertyName == ButtonEffects.TintColorProperty.PropertyName)
			{
                UpdateTintColor();
			}
		}

		private void UpdateTintColor()
		{
            var color = ButtonEffects.GetTintColor(Element);
            if (Control is UIButton button && button.ImageView?.Image != null && color != null)
            {
                button.SetImage(button.ImageView.Image.ImageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate), UIControlState.Normal);
                button.TintColor = color.Value.ToUIColor();
            }
        }
    }

I have currently only applied dark mode fully to my app in iOS and will update this blog post with the Android code when implemented.

You can use this ‘Effect’ in code by calling MyButton.SetDynamicResource(ButtonEffects.TintColorProperty, "PrimaryTextColor");

or by setting the property in XAML like

<Button effects:ButtonEffects.TintColor="{DynamicResource PrimaryTextColor}"/>

comments powered by Disqus