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).

Understanding Platform-Specifics Platform-specifics allow you to break out of the unified API and interact directly with the underlying native platform APIs or controls. This is crucial for achieving native look-and-feel or utilizing platform-exclusive features.

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:

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();
    }
}
Best Practice For UI customization, prefer using handlers. For non-UI platform-specific logic, use dependency injection with interfaces. Conditional compilation is best for small, isolated code snippets.

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.