Hacking Xamarin.Forms' Page.Appearing for iOS
11 Mar 2017Note At the time of writing, the latest version of Xamarin.Forms is
2.3.4-pre4
. I will endeavour to update this post when the lifecycle issues are resolved.
It’s no secret that Xamarin.Forms’ lifecycle is terribly broken. See here and here for details.
The biggest pain point I have day-to-day is that Page.Appearing
fires too late on iOS. Instead of being triggered by ViewWillAppear
as you’d expect, it is triggered by ViewDidAppear
. This is a problem if you do any kind of view set-up in response to Appearing
because your page will already be visible to the user. ReactiveUI users are particularly prone to this problem, since its activation-for-view-fetcher has no option other than to use Appearing
.
As an interim solution until this problem is fixed in Xamarin.Forms, I thought I’d try hacking around it. After a couple of false starts, I decided the only viable option - other than having your own custom build of XF, which leads to more painful problems - is to use reflection and knowledge of the default PageRenderer
implementation. Such an approach allows us to trigger Appearing
in response to ViewWillAppear
and also prevent the default PageRenderer
logic from triggering it again in response to ViewDidAppear
.
The code is as follows:
[assembly: Xamarin.Forms.ExportRenderer(typeof(Xamarin.Forms.Page), typeof(UI.iOS.Renderers.HackedPageRenderer))]
namespace UI.iOS.Renderers
{
using System.Reflection;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
// a hacky PageRenderer subclass that uses the correct hook (ViewWillAppear rather than ViewDidAppear) for the Page.Appearing event on iOS
// TODO: remove this once XF life cycle is fixed (see https://forums.xamarin.com/discussion/84510/proposal-improved-life-cycle-support)
public sealed class HackedPageRenderer : PageRenderer
{
private static readonly FieldInfo appearedField = typeof(PageRenderer).GetField("_appeared", BindingFlags.NonPublic | BindingFlags.Instance);
private static readonly FieldInfo disposedField = typeof(PageRenderer).GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance);
private IPageController PageController => this.Element as IPageController;
private bool Appeared
{
get { return (bool)appearedField.GetValue(this); }
set { appearedField.SetValue(this, value); }
}
private bool Disposed
{
get { return (bool)disposedField.GetValue(this); }
set { disposedField.SetValue(this, value); }
}
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
if (this.Appeared || this.Disposed)
{
return;
}
// by setting this to true, we also ensure that PageRenderer does not invoke SendAppearing a second time when ViewDidAppear fires
this.Appeared = true;
PageController.SendAppearing();
}
}
}
By including this in the iOS platform project for your Xamarin.Forms solution, your Page.Appearing
events will now trigger from ViewWillAppear
. Like I mentioned, this is particularly useful for ReactiveUI consumers, but frankly will help pretty much anyone doing XF+iOS.
Other renderers (e.g. NavigationRenderer
and TabbedRenderer
) suffer from the same issue, and can be hacked in much the same fashion on an as-need basis.