Check for Dark Mode in Xamarin.Forms
Both iOS 13 and Android Q now let the user enable Dark Mode for the operating system. Let's explore how to check whether the user has enabled Dark Mode in our Xamarin.Forms apps.
Update (10 April 2020): Custom Renderers are no longer required to implement theming. Xamarin.Forms has introduced the following APIs in Xamarin.Forms Xamarin.Forms v4.6.0.616-pre4:
Xamarin.Forms.Application.Current.RequestedTheme
Xamarin.Forms.Application.Current.RequestedThemeChanged
You can find the completed solution that uses these updated APIs in this app:
Both iOS 13 and Android Q now let the user enable Dark Mode for the operating system. Let's explore how to check if the user has enabled Dark Mode in our Xamarin.Forms apps!
You can find the completed solution in the following apps:
Xamarin.Forms
In our Xamarin.Forms project, let's create an interface called IEnvironment
and an enum called Theme
that will be used by the platform-specific libraries
using System.Threading.Tasks;
namespace MyNamespace
{
public interface IEnvironment
{
Theme GetOperatingSystemTheme();
Task<Theme> GetOperatingSystemThemeAsync();
}
public enum Theme { Light, Dark }
}
After we've added the platform-specific code (in the Xamarin.iOS and Xamarin.Android sections below), we'll be able to detect the Theme from our Xamarin.Forms code. I recommend checking/setting the theme in Application.OnStart
and Application.OnResume
to ensure that our app always adhere's to the user's preference.
using Xamarin.Forms;
namespace MyNamespace
{
public App : Application
{
// ...
protected override async void OnStart()
{
base.OnStart();
Theme theme = DependencyService.Get<IEnvironment>().GetOperatingSystemTheme();
SetTheme(theme);
}
protected override async void OnResume()
{
base.OnResume();
Theme theme = DependencyService.Get<IEnvironment>().GetOperatingSystemTheme();
SetTheme(theme);
}
void SetTheme(Theme theme)
{
//Handle Light Theme & Dark Theme
}
}
}
Xamarin.iOS
In our Xamarin.iOS project, we will retrieve the TraitCollection.UserInterfaceStyle
value from the current UIViewController
and check whether it is UIUserInterfaceStyle.Light
or UIUserInterfaceStyle.Dark
.
These APIs were introduced in iOS 12.0, so we'll first check whether the device is running iOS 12 (or higher). If it is on a earlier version of iOS, we will return Theme.Light
.
using System;
using UIKit;
using Xamarin.Forms;
using MyNamespace;
using MyNamespace.iOS;
[assembly: Dependency(typeof(Environment_iOS))]
namespace MyNamespace.iOS
{
public class Environment_iOS : IEnvironment
{
public Theme GetOperatingSystemTheme()
{
//Ensure the current device is running 12.0 or higher, because `TraitCollection.UserInterfaceStyle` was introduced in iOS 12.0
if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
{
var currentUIViewController = GetVisibleViewController();
var userInterfaceStyle = currentUIViewController.TraitCollection.UserInterfaceStyle;
switch (userInterfaceStyle)
{
case UIUserInterfaceStyle.Light:
return Theme.Light;
case UIUserInterfaceStyle.Dark:
return Theme.Dark;
default:
throw new NotSupportedException($"UIUserInterfaceStyle {userInterfaceStyle} not supported");
}
}
else
{
return Theme.Light;
}
}
// UIApplication.SharedApplication can only be referenced by the Main Thread, so we'll use Device.InvokeOnMainThreadAsync which was introduced in Xamarin.Forms v4.2.0
public async Task<Theme> GetOperatingSystemThemeAsync() =>
Device.InvokeOnMainThreadAsync(GetOperatingSystemTheme);
static UIViewController GetVisibleViewController()
{
UIViewController viewController = null;
var window = UIApplication.SharedApplication.KeyWindow;
if (window.WindowLevel == UIWindowLevel.Normal)
viewController = window.RootViewController;
if (viewController is null)
{
window = UIApplication.SharedApplication
.Windows
.OrderByDescending(w => w.WindowLevel)
.FirstOrDefault(w => w.RootViewController != null && w.WindowLevel == UIWindowLevel.Normal);
viewController = window?.RootViewController ?? throw new InvalidOperationException("Could not find current view controller.");
}
while (viewController.PresentedViewController != null)
viewController = viewController.PresentedViewController;
return viewController;
}
}
}
Xamarin.Android
In our Xamarin.Android project, we'll retrieve Configuration.UiMode
from the current Context
and check whether it is UiMode.NightYes
or UiMode.NightNo
.
Note: This code requires the Plugin.CurrentActivity NuGet Package. Make sure to first add this NuGet package to your Xamarin.Android project.
using System;
using System.Threading.Tasks;
using Android.Content.Res;
using Plugin.CurrentActivity;
using Xamarin.Forms;
using MyNamespace;
using MyNamespace.Android;
[assembly: Dependency(typeof(Environment_Android))]
namespace MyNamespace.Android
{
public class Environment_Android : IEnvironment
{
public Task<Theme> GetOperatingSystemThemeAsync() =>
Task.FromResult(GetOperatingSystemTheme());
public Theme GetOperatingSystemTheme()
{
//Ensure the device is running Android Froyo or higher because UIMode was added in Android Froyo, API 8.0
if(Build.VERSION.SdkInt >= BuildVersionCodes.Froyo)
{
var uiModeFlags = CrossCurrentActivity.Current.AppContext.Resources.Configuration.UiMode & UiMode.NightMask;
switch(uiModelFlags)
{
case UiMode.NightYes:
return Theme.Dark;
case UiMode.NightNo:
return Theme.Light;
default:
throw new NotSupportedException($"UiMode {uiModelFlags} not supported");
}
}
else
{
return Theme.Light;
}
}
}
}