Platform-Specifics in MAUI
This tutorial explores how to implement platform-specific functionality and customization in your .NET MAUI applications. While MAUI provides a unified API for common UI elements and behaviors, there are often scenarios where you need to leverage unique capabilities or appearance of a specific platform (Android, iOS, macOS, Windows).
Methods for Platform-Specific Implementations
There are several approaches to handle platform-specific requirements in MAUI:
1. Conditional Compilation
The simplest way to include platform-specific code is by using conditional compilation directives. MAUI defines preprocessor symbols for each target platform:
#if ANDROID
#if IOS
#if MACCATALYST
#if WINDOWS
#if TIZEN
(if Tizen is enabled)
Example using conditional compilation in C#:
public partial class MyPage : ContentPage
{
public MyPage()
{
InitializeComponent();
#if ANDROID
// Code specific to Android
MyAndroidSpecificView.IsVisible = true;
#elif IOS
// Code specific to iOS
MyIOSSpecificView.IsVisible = true;
#elif WINDOWS
// Code specific to Windows
MyWindowsSpecificView.IsVisible = true;
#endif
}
}
You can also use conditional compilation in XAML, though it's less common:
<Grid>
<!-- Android specific control -->
<maps:Map x:Name="MyMap" IsVisible="{OnPlatform Android=true, Default=false}" />
<!-- iOS specific control -->
<CollectionView IsVisible="{OnPlatform iOS=true, Default=false}" />
</Grid>
2. Platform-Specific Implementations with Handler (Recommended for UI Customization)
For more complex UI customizations or when you need to replace or extend existing controls, MAUI's handler architecture is the recommended approach. This involves creating platform-specific renderers (handlers) for your custom controls.
Creating a Custom Control
First, define your custom control that inherits from a built-in MAUI control or creates a new one.
// MyCustomButton.cs
public class MyCustomButton : Button
{
public static readonly BindableProperty HighlightColorProperty =
BindableProperty.Create(nameof(HighlightColor), typeof(Color), typeof(MyCustomButton), Colors.Yellow);
public Color HighlightColor
{
get => (Color)GetValue(HighlightColorProperty);
set => SetValue(HighlightColorProperty, value);
}
}
Creating Platform-Specific Handlers
Now, create handlers for each target platform. These handlers will map the custom control's properties to native platform control properties.
Android Handler
// MyCustomButtonRenderer.Android.cs
public class MyCustomButtonHandler : ButtonHandler
{
protected override MauiButton CreateNativeControl()
{
var button = base.CreateNativeControl();
button.SetHighlightColor(Android.Graphics.Color.Yellow); // Default
return button;
}
public override void ConnectHandler(object nativeView)
{
base.ConnectHandler(nativeView);
UpdateHighlightColor();
}
private void UpdateHighlightColor()
{
if (MauiContext != null && VirtualView is MyCustomButton myButton)
{
var nativeButton = (Android.Widget.Button)Control;
if (myButton.HighlightColor != null)
{
nativeButton.SetHighlightColor(myButton.HighlightColor.ToAndroid());
}
}
}
protected override void ConnectMappingCbs()
{
base.ConnectMappingCbs();
((IMyCustomButton)VirtualView).HighlightColorChanged += UpdateHighlightColor;
}
}
iOS Handler
// MyCustomButtonRenderer.iOS.cs
public class MyCustomButtonHandler : ButtonHandler
{
protected override UIKit.UIButton CreateNativeControl()
{
var button = base.CreateNativeControl();
button.SetTitleColor(UIKit.UIColor.Yellow, UIKit.UIControlState.Highlighted); // Default
return button;
}
public override void ConnectHandler(object nativeView)
{
base.ConnectHandler(nativeView);
UpdateHighlightColor();
}
private void UpdateHighlightColor()
{
if (MauiContext != null && VirtualView is MyCustomButton myButton)
{
var nativeButton = (UIKit.UIButton)Control;
if (myButton.HighlightColor != null)
{
nativeButton.SetTitleColor(myButton.HighlightColor.ToUIColor(), UIKit.UIControlState.Highlighted);
}
}
}
protected override void ConnectMappingCbs()
{
base.ConnectMappingCbs();
((IMyCustomButton)VirtualView).HighlightColorChanged += UpdateHighlightColor;
}
}
Remember to register your custom handlers in your MauiProgram.cs
file:
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
})
.ConfigureMauiHandlers(handlers =>
{
handlers.AddHandler(typeof(MyCustomButton), typeof(MyCustomButtonHandler));
});
return builder.Build();
}
}
3. Platform-Specific Services using Dependency Injection
For non-UI platform-specific functionality (like accessing sensors, location services, or platform-specific storage), you can define an interface in your shared project and provide platform-specific implementations using dependency injection.
Define an Interface
// IPlatformInfo.cs (in your shared project)
public interface IPlatformInfo
{
string GetPlatformName();
}
Implement the Interface on Each Platform
// PlatformInfo.Android.cs (in your Android project)
using YourAppNamespace.Interfaces; // Assuming your interface is here
namespace YourAppNamespace.Android
{
public class PlatformInfo : IPlatformInfo
{
public string GetPlatformName()
{
return "Android";
}
}
}
// PlatformInfo.iOS.cs (in your iOS project)
using YourAppNamespace.Interfaces; // Assuming your interface is here
using UIKit;
namespace YourAppNamespace.iOS
{
public class PlatformInfo : IPlatformInfo
{
public string GetPlatformName()
{
return UIDevice.CurrentDevice.SystemName; // e.g., "iOS"
}
}
}
Register the implementations in your MauiProgram.cs
:
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
})
.ConfigureServices(services =>
{
#if ANDROID
services.AddTransient<IPlatformInfo, YourAppNamespace.Android.PlatformInfo>();
#elif IOS
services.AddTransient<IPlatformInfo, YourAppNamespace.iOS.PlatformInfo>();
#elif WINDOWS
services.AddTransient<IPlatformInfo, YourAppNamespace.Windows.PlatformInfo>(); // Assuming Windows implementation exists
#endif
});
return builder.Build();
}
}
Then, inject and use the service in your shared code:
public partial class MyViewModel : ObservableObject
{
private readonly IPlatformInfo _platformInfo;
[ObservableProperty]
string platformName;
public MyViewModel(IPlatformInfo platformInfo)
{
_platformInfo = platformInfo;
PlatformName = _platformInfo.GetPlatformName();
}
}
Platform-Specific UI Elements
While MAUI aims for consistency, some UI elements might have subtle differences or require platform-specific styling.
Example: Native Title Bar Customization
The title bar appearance can be customized per platform:
Windows Title Bar
On Windows, you can customize the title bar appearance by setting properties on the Application.Current.Windows.First().ExtendsContentIntoTitleBar = true;
and then styling the title bar content.
iOS/macOS Navigation Bar
iOS and macOS use UINavigationController
and NSViewController
respectively. MAUI's NavigationPage
abstracts this. For deeper customization, you might need to access the underlying native navigation controller.
Conclusion
Leveraging platform-specific capabilities in .NET MAUI empowers you to create richer, more native-feeling applications. By understanding and applying conditional compilation, handlers, and dependency injection, you can effectively bridge the gap between your shared code and the unique features of each target platform.