Controlando el modo oscuro de iOS 13 en Xamarin.Forms

Antes de comenzar, comentaos que este post forma parte de una gran iniciativa ideada por Luis Beltrán (mi agradecimiento desde aquí). Y no es otra que un calendario de adviento en el que colaboramos la comunidad de Xamarin escribiendo un post o haciendo un video, cada día desde el 1 hasta el 28 de Diciembre. En este enlace, podéis ver todo el contenido que se ha generado para este calendario https://www.luisbeltran.mx/2019/11/06/primer-calendario-de-adviento-de-xamarin-en-espanol/
Un vez hecha la introducción, vamos a por el post que nos ocupa.
Con la llegada de iOS 13, ha llegado a nuestros dispositivos el modo oscuro, por lo que, si queremos que nuestra app quede integrada con este nuevo modo, tenemos que hacer ciertas modificaciones. Hay controles q cambian su aspecto sin tener que hacer nada. Por ejemplo, un entry tiene su fondo de color blanco cuando nuestro dispositivo está en modo claro y, en cambio, su fondo es negro con el modo oscuro.
A continuación os muestro el ejemplo de una aplicación de ejemplo que he realizado.

Como se puede apreciar, tiene varios elementos que cambian dependiendo del modo (una image, un label y un entry) y otros que no se ven alterados por el cambio de modo (un botón que siempre se ve de color rojo).
Para conseguirlo, he creado un archivo de estilo DefaultTheme, que es un diccionario de recursos en el que se almacenan los estilos comunes tanto en el modo claro como el oscuro.
<?xml version="1.0" encoding="UTF-8"?>
<ResourceDictionary xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="XamarinDarkMode.Styles.DefaultTheme">
<Color x:Key="ButtonColor">#FF0000</Color>
</ResourceDictionary>
Para definir los estilos de cada modo, se han creado dos archivos, DarkTheme y LightTheme. Ambos son diccionarios de recursos cuyo source es DefaultTheme.
<?xml version="1.0" encoding="UTF-8"?>
<?ResourceDictionary xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="XamarinDarkMode.Styles.DarkTheme"
Source="DefaultTheme.xaml">
<?Color x:Key="BackgroundColor">#000000<?/Color>
<?Color x:Key="LabelColor">#FFFFFF<?/Color>
<?FileImageSource x:Key="CloudImageSource">lightCloud.png<?/FileImageSource>
<?/ResourceDictionary>
<?xml version="1.0" encoding="UTF-8"?>
<ResourceDictionary xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="XamarinDarkMode.Styles.LightTheme"
Source="DefaultTheme.xaml">
<Color x:Key="BackgroundColor">#FFFFFF</Color>
<Color x:Key="LabelColor">#000000</Color>
<FileImageSource x:Key="CloudImageSource">darkCloud.png</FileImageSource>
</ResourceDictionary>
Una vez añadidos los recursos a los archivos anteriores, estamos en disposición de utilizarlos en nuestras páginas. Los recursos que sean comunes en ambos modos, se puede usar como StaticResource, en cambio, los que varíen por cada modo, hay que usarlos como DynamicResource, ya que pueden cambiar en tiempo de ejecución si el usuario cambia el modo del dispositivo. En nuestro ejemplo, la página que tenemos sería la siguiente.
<?xml version="1.0" encoding="utf-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="XamarinDarkMode.MainPage"
BackgroundColor="{DynamicResource BackgroundColor}">
<StackLayout VerticalOptions="Center" Orientation="Vertical"
Margin="20,0">
<Image Source="{DynamicResource CloudImageSource}" HeightRequest="200" Aspect="AspectFit"/>
<Label Text="Welcome to Xamarin.Forms!" TextColor="{DynamicResource LabelColor}"/>
<Entry Placeholder="Entry placeholder" />
<Button Text="Button" TextColor="{StaticResource ButtonColor}"/>
</StackLayout>
</ContentPage>
En el ejemplo anterior, el color del botón es siempre rojo, para los dos modos, el resto de elementos variarán.
Por otro lado, es necesario modificar el App.xaml para indicarle el estilo que se tiene que aplicar. Para iOS, no importa demasiado el estilo que le indiquemos aquí ya que, tal y como se muestra más adelante, esté se aplica para cada página, al cargarse y al cambiar el modo.
<?xml version="1.0" encoding="utf-8"?>
<Application xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="XamarinDarkMode.App">
<Application.Resources>
<ResourceDictionary Source="/Styles/LightTheme.xaml"/>
</Application.Resources>
</Application>
Por último, para aplicar el estilo que corresponda al arrancar la aplicación y detectar cambios en el modo, realizados por el usuario mientras nuestra app está abierta, tenemos que extender la funcionalidad de PageRenderer, tal y como se muestra a continuación.
[assembly: Xamarin.Forms.ExportRenderer(typeof(Xamarin.Forms.ContentPage), typeof(XamarinDarkMode.iOS.Renderers.PageRenderer))]
namespace XamarinDarkMode.iOS.Renderers
{
using System;
using UIKit;
using Xamarin.Forms.Platform.iOS;
using XamarinDarkMode.Styles;
public class PageRenderer : Xamarin.Forms.Platform.iOS.PageRenderer
{
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
return;
if (Element == null)
return;
try
{
SetTheme();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error changing theme. {ex?.Message}");
}
}
public override void TraitCollectionDidChange(UITraitCollection previousTraitCollection)
{
base.TraitCollectionDidChange(previousTraitCollection);
if (TraitCollection.UserInterfaceStyle != previousTraitCollection.UserInterfaceStyle)
SetTheme();
}
private void SetTheme()
{
if (TraitCollection.UserInterfaceStyle == UIUserInterfaceStyle.Dark)
App.Current.Resources = new DarkTheme();
else
App.Current.Resources = new LightTheme();
}
}
}
Como es habitual, he creado un proyecto en GitHub para ofrecer más detalles de la implementación que he realizado. Espero que os sea de ayuda https://github.com/jorgediegocrespo/XamarinDarkMode