Fly-through effect iOS View Controller transitions with Xamarin

A recent project I worked on involved creating a showcase iPad app that would impress users with creative animations and transitions between screens.

We were inspired by Callum Boddy’s Twitter-style splash view. We wanted to achieve a similar effect in our own app: tapping on a button would trigger an animated transition, giving the user the impression of “flying through” a logo, revealing the next screen as the animation ran.

In this post, I will be showing you how to achieve this effect in your own Xamarin iOS app.

All the code samples in this article can be found in a working example on this github repository. Note the project on github is written using the Xamarin unified API. You can read more about that here: http://developer.xamarin.com/guides/cross-platform/macios/unified/

Part 1: The Animation

Callum’s code was written in Objective-C. Since we were creating our own app using Xamarin, the first step was to re-write it in C# against the Monotouch API. Callum’s version supports using raster images as well as a Bezier paths, but as we were only going to be using Beziers, our implementation is a bit more simplified.

The animation is done in FlyThroughView.cs, which is a simple UIView. An instance is obtained from a static factory method, by passing in a bezier path representation of a logo:

public static FlyThroughView SplashViewWithBezierPath(UIBezierPath bezier, UIColor backgroundColor, CGRect rect) 
      {
          	var flyThrough = new FlyThroughView ();
          	flyThrough.Frame = rect;

          	flyThrough.backgroundViewColor = backgroundColor;
          	flyThrough.iconLayer = flyThrough.CreateShapeLayerWithBezierPath (bezier);

          	flyThrough.Layer.AddSublayer (flyThrough.iconLayer);

          	flyThrough.BackgroundColor = flyThrough.IconColor;

          	return flyThrough;
      }

We created our bezier paths using an awesome designer tool called Paint Code, which can export vector shapes into Objective-C code as well as C#. We will be posting another blog on how to use PaintCode soon.

The bulk of the icon animation is performed by the IconAnimation property of FlyThroughView.cs:

public CAAnimation IconAnimation
	{
		get 
		{
			if (iconAnimation == null) 
			{
				var animation = CAKeyFrameAnimation.GetFromKeyPath ("transform.scale");

				animation.Values = new NSNumber[] { 1, 0.9, 300 };
				animation.KeyTimes = new NSNumber[] { 0, 0.4, 1 };
				animation.Duration = this.AnimationDuration;
				animation.RemovedOnCompletion = false;
				animation.FillMode = CAFillMode.Forwards;
				animation.TimingFunctions = new CAMediaTimingFunction[] {
					CAMediaTimingFunction.FromName(CAMediaTimingFunction.EaseOut),
					CAMediaTimingFunction.FromName(CAMediaTimingFunction.EaseIn)
				};

				iconAnimation = animation;
			}

			return iconAnimation;
		}
	} 

It uses the Core Animation library to define a key frame animation which applies a scale transform effect, which gives the impression of the icon expanding and moving towards the user’s perspective.

Later on, when the animation is invoked, another animation block makes the white background colour of the UIView transparent as it begins to move toward the user, revealing the view behind the logo and giving the impression that we are moving through the porthole created by the bezier shape:

public void startAnimationWithCompletionHandler (Action completionHander)
	{
		// snip...

		this.iconLayer.AddAnimation (this.IconAnimation, "VectorSplashViewIconAnimation");

		UIView.Animate (0.4f, this.AnimationDuration - 1.3f, UIViewAnimationOptions.AllowAnimatedContent, () => {
			this.BackgroundColor = UIColor.Clear;
		}, null);

		UIView.Animate (0.2f, this.AnimationDuration - 1f, UIViewAnimationOptions.AllowAnimatedContent, () => {
			this.Alpha = 0f;
			this.BackgroundColor = UIColor.Clear;
		}, null);
	}

The animation is kicked off by calling the startAnimationWithCompletionHandler method above. We can pass in an optional callback delegate to be invoked after the animation has completed:

this.IconAnimation.Delegate = new AnimationCompleteDelegate (this.animationCompletionHandler, this);

Our delegate class inherits from CAAnimationDelegate:

public class AnimationCompleteDelegate : CAAnimationDelegate
{
    private Action onCompleteHandler;
    private FlyThroughView flyThrough;

    public AnimationCompleteDelegate (Action onCompleteHandler, FlyThroughView flyThrough)
    {
        this.flyThrough = flyThrough;
        this.onCompleteHandler = onCompleteHandler;
    }

    public override void AnimationStopped (CAAnimation anim, bool finished)
    {
        if (this.onCompleteHandler != null) {
            this.onCompleteHandler ();
        }

        flyThrough.RemoveFromSuperview ();
    }
}

Part 2: The transition to the target View Controller

Normally, transitions between view controllers are handled by calling the PresentViewController() method from the current controller. The default animation usually takes the form of the target View Controller sliding in over the current view, from somewhere off-screen.

We want to override this default behaviour and trigger our own custom view animation. To do that we need to create a Delegate that implements UIViewControllerTransitioningDelegate.

I will cover the basic steps here. There seems like a lot going on here, and lots of sub-classing happening, so it can be a little confusing initially. You can also refer to the Xamarin documentation for more information.

In our example, we have controllers for the coming “from” view and "target" views. We will set the TransitioningDelegate property of our “target” controller to use our own custom delegate:

this.btnPerformTransition.TouchUpInside += delegate {
    var toVC = new ToViewController ();
    
    toVC.ModalPresentationStyle = UIModalPresentationStyle.Custom;
    toVC.TransitioningDelegate = new TransitioningDelegate ();
    
    this.PresentViewController (toVC, true, null);
}; 

Our transitioning delegate will now take over the transition by overriding the PresentingController() method of UIViewControllerTransitioningDelegate.

public override IUIViewControllerAnimatedTransitioning PresentingController (UIViewController presented, UIViewController presenting, UIViewController source)
    {
        return new FlyThroughTransitionAnimator ();
    } 

Here, we simply return an instance of an animator class which overrides the AnimateTransition method of UIViewControllerAnimatedTransitioning:

public override void AnimateTransition (IUIViewControllerContextTransitioning transitionContext)
    {
        var fromVC = transitionContext.GetViewControllerForKey (UITransitionContext.FromViewControllerKey);
        var fromView = fromVC.View;
        var flyThroughView = fromVC.View.Subviews.FirstOrDefault () as FlyThroughView;
        var toVC = transitionContext.GetViewControllerForKey (UITransitionContext.ToViewControllerKey);
        var toView = toVC.View;

        // add the "to" view behind the animation
        fromView.InsertSubview (toView, 0);

        flyThroughView.startAnimationWithCompletionHandler (() => {
            // now move "to" view to front, so buttons etc work
            transitionContext.ContainerView.InsertSubview (toView, 0);

            transitionContext.CompleteTransition (true);
        });
    } 

From here, we can access our “from” and “target” view controllers, locate the instance of our FlyThroughView and invoke the animation as described in Part 1, and we pass in a delegate for the animation complete callback.

Eagle-eyed readers will notice the one minor hack here (nothing is ever straight-forward!): The default behaviour for the target view is to appear over the top of the existing view. However, we want the target view to appear behind to give the desired effect of moving “through” the icon towards it.

So, we initially insert the target view at the back of the view stack, and inside the animation complete handler, we move it back to the front, so all of our interactions still work once the transition is complete.

The Final Effect

Here is the final result, in all its glory:

All the code samples in this article can be found in a working example on this github repository.