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