Best way to use SafeAreaInsets

Using a safe area is a great way to support the iPhone X* and safely avoid overlaps with the virtual home button or the notch.

In Xamarin.Forms, the easy way is to set a safe area using the platform specific API of a Page:

using Xamarin.Forms.PlatformConfiguration;
using Xamarin.Forms.PlatformConfiguration.iOSSpecific;

public MyPage() 
{
    InitializeComponent();
    On<iOS>().SetUseSafeArea(true);
}

Ok, pretty straightforward. But suppose you want to apply a gradient background underneath the status bar and the virtual home button:

What we want.
What we get with SetUseSafeArea(true).

To achieve that, we’d rather play with the On<iOS>().SafeAreaInsets() API. Following the documentation, we should use the Page.OnAppearing method:

<ContentPage>
    <controls:GradientView StartColor="#2196F3"
                           EndColor="#E10AEB" />
    <Grid x:Name="MainContainer">
        <!-- Content -->
    </Grid>
</ContentPage>
protected override void OnAppearing()
{
    base.OnAppearing();

    MainContainer.Margin = On<Xamarin.Forms.PlatformConfiguration.iOS>().SafeAreaInsets();
}

Simple, right? Let’s see it in action:

It does work but it is not perfect because the margin gets applied after the navigation animation. Let’s review a few solutions!

Option #1: LayoutChanged

LayoutChanged is an event raised when bounds have changed. It is usely first raised when the view is mounted, way before the OnAppearing.

public MyPage() 
{
    InitializeComponent();

    LayoutChanged += MyPage_LayoutChanged;
}

private void MyPage_LayoutChanged(object sender, EventArgs e) 
{
    // We only need the first call, so we unsubscribe immediately
    LayoutChanged -= MyPage_LayoutChanged;

    MainContainer.Margin = On<Xamarin.Forms.PlatformConfiguration.iOS>().SafeAreaInsets();
}

Nice try, but not good enough:

  • The inset is correctly set for the first Page appearing, but for some reason, the following pages get an empty inset.
  • You don’t get notified when the value changes (e.g. orientation).

Option #2: Property changed

We have a way to be notified when a value changes: PropertyChanged. Let’s use it for the Page!

public MyPage() 
{
    PropertyChanged += ContentPageBase_PropertyChanged;
}

private void ContentPageBase_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    if (e.PropertyName == "SafeAreaInsets")
    {
        MainContainer.Margin = On<Xamarin.Forms.PlatformConfiguration.iOS>().SafeAreaInsets();
    }
}

If the phone is rotated or a split window is created, the page would be notified. We can then create a ContentPageBase and use this technique from every page.

But what if we need this value from a ContentView somewhere else in the app?

Option #3: Property changed + Dynamic resource

There is an easy way to propagate a value in the app: the DynamicResource markup. It is just like an usual StaticResource, except we can change the value and any XAML bound to its value is changed.

public MyPage() 
{
    PropertyChanged += ContentPageBase_PropertyChanged;
}

private void ContentPageBase_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    if (e.PropertyName == "SafeAreaInsets")
    {
        var safeAreaInsets = On<Xamarin.Forms.PlatformConfiguration.iOS>().SafeAreaInsets();
        Application.Current.Resources["SafeAreaInsets"] = safeAreaInsets;
    }
}
<Application>
    <Application.Resources>
        <Thickness x:Key="SafeAreaInsets"/>
    </Application.Resources>
</Application>
<Grid>
<controls:GradientView StartColor="{StaticResource PrimaryColor}"
                       EndColor="{StaticResource SecondaryColor}" />
    <Grid Margin="{DynamicResource SafeAreaInsets}">
        <!-- ... -->
    </Grid>
</Grid>

Nice & clean! Let’s review the result:

Conclusion

We saw a few tricks along the way. Some can feel quite “hacky”, but the right solution seems to be a combination of PropertyChanged and DynamicResource.


© 2019. All rights reserved.

Powered by Hydejack v8.4.0