iOS Animations in Xamarin

« Return to Our Notebook

iOS Animations in Xamarin

We recently launched the app TaxChat, "tax preparation for people who have better things to do." The iOS app saves you from having to do your taxes by yourself; instead you just answer a few questions, snap a couple of photos and a certified tax professional will take care of your tax return for you. All through a beautiful & intuitive interface. You can read more about it at tax.chat.

Since we built TaxChat using Xamarin, I figure this is a great time to write a post on iOS animations in Xamarin and detail some of the animations seen in the app. If you don't already know about Xamarin, check out this introduction to Xamarin by our resident Xamarin MVP, Sean Sparkman. Essentially, Xamarin allows you to build native apps for multiple platforms all in C#, which is pretty sweet.

Why animate?

Animations are an important part of any user interface. Subtle animations can turn a static layout into an engaging experience by showing movement when an action occurs or informing the user that something is happening. Not only can animations be helpful, but when done properly, they create a very polished look and feel. There are quite a few ways to animate in iOS, some of which are mentioned in Xamarin's CoreAnimation intro. In this post I'll discuss AnimateNotify, AnimateKeyframes and AddKeyframeWithRelativeStartTime, since that's what I used in TaxChat. I'll show the animation comps for a few screens, go over what's happening and then discuss the code.

Simple position animation

This video comp shows the launch screen and four onboarding screens visible when you first open the app. I'm going to focus on the second screen with the yellow background first since it's the simplest animation. Here we have six icons, when the screen appears the second and third row icons slide in from the right to line up horizontally with the first row icons. When the screen disappears the icons slide back over to the right. The six icons are inside a container UIView which has the necessary constraints placed on it, so all we have to worry about is updating the positioning of those icons inside the container.

To set the stage, let me say that this screen is fully laid out in the storyboard with the icons lined up in their final position. On load we'll adjust the second row icons to have a 10pt offset and the third row a 20pt offset. To update the positioning we'll use the elements' center CGPoint.

public override void ViewDidLoad()
{
    base.ViewDidLoad();

    // set icon offsets
    iconThree.Center = new CGPoint(iconThree.Center.X + 10, iconThree.Center.Y);
    iconFour.Center = new CGPoint(iconFour.Center.X + 10, iconFour.Center.Y);
    iconFive.Center = new CGPoint(iconFive.Center.X + 20, iconFive.Center.Y);
    iconSix.Center = new CGPoint(iconSix.Center.X + 20, iconSix.Center.Y);
}

When the view is going to appear we'll animate those icons back to their base position. We'll be using AnimateNotify with a one second duration, no time delay, an optional easing and since we don't need anything to happen on completion we'll make that null. The Xamarin documentation has a full list of all the UIViewAnimationOptions. It should also be noted that you can use multiple UIViewAnimationOptions by separating them with a pipe operator.

public override void ViewWillAppear(bool animated)
{
    base.ViewWillAppear(animated);

    // AnimateNotify ( duration in seconds, delay in seconds, options, animations, completion )
    UIView.AnimateNotify(1, 0, UIViewAnimationOptions.CurveEaseOut,
        ()=> {
        iconThree.Center = new CGPoint(iconThree.Center.X - 10, iconThree.Center.Y);
        iconFour.Center = new CGPoint(iconFour.Center.X - 10, iconFour.Center.Y);
        iconFive.Center = new CGPoint(iconFive.Center.X - 20, iconFive.Center.Y);
        iconSix.Center = new CGPoint(iconSix.Center.X - 20, iconSix.Center.Y);
    }, null);
}

When the view is going to disappear, we'll animate the icons over to their offset position again.

public override void ViewWillDisappear(bool animated)
{
    base.ViewWillDisappear(animated);

    UIView.AnimateNotify(1, 0, UIViewAnimationOptions.CurveEaseOut,
        ()=> {
        iconThree.Center = new CGPoint(iconThree.Center.X + 10, iconThree.Center.Y);
        iconFour.Center = new CGPoint(iconFour.Center.X + 10, iconFour.Center.Y);
        iconFive.Center = new CGPoint(iconFive.Center.X + 20, iconFive.Center.Y);
        iconSix.Center = new CGPoint(iconSix.Center.X + 20, iconSix.Center.Y);
    }, null);
}

Grouped animations

TaxChat Onboarding Chat Screen

Alright, that first one was simple enough. Let's move on to the third onboarding screen (light gray background) now. Here we have three images, one of a tax return and two chat bubbles. While fading in the tax return slides up, the first chat bubble slides right, and the second chat bubble slides left. There are a couple differences with this screen and the last. First, the animations are staggered slightly. This means we can't use one AnimateNotify block, so instead we'll use AnimateKeyframes and AddKeyframeWithRelativeStartTime. Second, we have constraints set on each of these images. This means we can't adjust their positioning like before, instead we'll have to update the corresponding constraints placed on each image.

This screen is also fully laid out in the storyboard with all the images in their final position. When the view loads we'll set the starting points for our animations.

public override void ViewDidLoad()
{
    base.ViewDidLoad();

    chatBubbleOne.Alpha = 0f;
    chatBubbleTwo.Alpha = 0f;
    taxReturnImage.Alpha = 0f;

    // shift horizontal center constraint to the left 10
    chatBubbleOneCenter.Constant = -15f;

    // shift leading constraint to the right 10
    chatBubbleTwoLeading.Constant = 80f;

    // shift top constraint down 10
    taxReturnImageTop.Constant = 70f;

    View.SetNeedsLayout(); // specify the layout needs updating
    View.LayoutIfNeeded(); // update layout
}

When the view is going to appear we'll animate things back into place. We'll be using AnimateKeyframes with a one second duration, no time delay, an optional easing and since we don't need anything to happen on completion we'll leave that empty (null throws an error). The Xamarin documentation also has a full list of all the UIViewKeyframeAnimationOptions.

public override void ViewWillAppear(bool animated)
{
    base.ViewWillAppear(animated);

    // AnimateKeyframes ( duration in seconds, delay in seconds, options, animations, completion )
    UIView.AnimateKeyframes(1, 0, UIViewKeyframeAnimationOptions.CalculationModeLinear, () => {

        // AddKeyframeWithRelativeStartTime ( start time, duration, animations )
        // start time and duration are 0-1 as percentage of wrapping AnimateKeyframes duration
        UIView.AddKeyframeWithRelativeStartTime (0, 0.4, () => {
            taxReturnImageTop.Constant = 60f;
            taxReturnImage.Alpha = 1f;
        });
        UIView.AddKeyframeWithRelativeStartTime (0.2, 0.5, () => {
            chatBubbleOneCenter.Constant = -5f;
            chatBubbleOne.Alpha = 1f;
        });
        UIView.AddKeyframeWithRelativeStartTime (0.4, 0.6, () => {
            chatBubbleTwoLeading.Constant = 70f;
            chatBubbleTwo.Alpha = 1f;
        });
        View.SetNeedsLayout();
        View.LayoutIfNeeded();
    }, (finished) => {});
}

Let's go over the keyframe animations above. AnimateKeyframes and AddKeyframeWithRelativeStartTime work together. AnimateKeyframes is the container that controls the overall settings of the animation group. As the name suggests, AddKeyframeWithRelativeStartTime adds the ability to adjust the start time and duration of animations inside the group relative to the duration specified in AnimateKeyframes. Above we have our entire group of animations lasting 1 second. The tax return image animation starts immediately and lasts 0.4 or 40% of the overall duration, which in this case is 0.4 seconds. The second chat bubble animation has a start time delay of 0.4 or 40% of the overall duration and lasts 0.6 or 60% of the overall duration. Notice in that scenerio both the delay and duration add up to 1.0 or 100%. If the total combined value of delay and duration in AddKeyframeWithRelativeStartTime is greater than 1.0 your animation will get cut short and jump right to the final specified settings when the overall duration is reached.

Something else to point out from the above animation block is the call to View.SetNeedsLayout() and View.LayoutIfNeeded(). Both of these are necessary because we are updating constraint values. SetNeedsLayout gets called on a view to specify that the view's layout needs updating. LayoutIfNeeded is then called to update the layout for any items that need it. If you do not call these you will not see the animation, just the end result. Also rememeber to make sure you're not creating any conflicts when updating constraints.

Chained animations with spring

In this screen we are also relying on information coming back from the back-end API that will signal when to move forward in the onboarding process, so during these animations, in addition to moving things around, we also need to handle any errors or possible delays appropriately. Let's break this down and see what we're doing here.

The elements on this screen are being laid out entirely in the code based on screen size, since the app is locked in portrait mode, we don't have to worry about constraints or screen orientation changes. We have five elements being animated. Three that you can easily identify: the text label and both chevrons; two that are less noticeable, which are cover layers behind each chevron matching the background. The cover layers hide the text label when the chevrons close while allowing the two chevrons to overlap each other.

For animations we have: the chevrons opening, closing, and the final animation where the elements disperse and fade out. We will need to set up each of these animations as a separate block and call the next one on completion if all is well. There are a lot of variables used in the following animation blocks. These are being set earlier in the code so the specific values are not shown here, but they should be self explanatory. Our chevrons start closed, so when the screen appears we'll call our method to open the chevrons.

void ChevronsOpen()
{
    // AnimateNotify ( duration, delay, spring damping, spring velocity, options, animations, completion )
    UIView.AnimateNotify(0.5, 0.5, 0.85, 1.0, 0,
        () => {
            leftChevronView.Center = new CGPoint (chevronOpenX, chevronY);
            rightChevronView.Center = new CGPoint (screenWidth - chevronOpenX, chevronY);

            leftCover.Frame = new CGRect (0, 0, coverWidth, screenHeight);
            rightCover.Frame = new CGRect (screenWidth - coverWidth, 0, coverWidth, screenHeight);
        },
        (finished) => {

            if(passes >= 4) {

                // if at least 4 passes and expert is found, go to expert screen
                if(preparerId > 0 ) {
                    ShowExpertScreen();
                }
                // if we've made 12 passes and haven't connected to a preparer
                else if(totalPasses >= 12 && preparerId == 0) {
                    FailedToFindPreparerError(); // shows alert and returns to previous screen
                    return;
                }
                // give it another run
                else {
                    passes = 0;
                    ChevronsClose();
                }
            }
            else {
                ChevronsClose();
            }
        }
    );
}

In the above method we're using AnimateNotify with the optional spring damping and velocity parameters to add a slight bounce to the chevrons at the end of the animation. The spring damping ratio is a value between 0 and 1, where the oscillation increases with a smaller value, so 1 is no oscillation and 0 is max oscillation. The initial spring velocity is points per second. We want a very slight oscillation, so we'll use a 0.85 damping and 1.0 velocity.

In the completion call we'll check the number of passes we've made, which we're tracking in the ChevronsClose method to follow. If we are at less than four passes we'll call the method for the closing animation. If we are at four or more passes then we'll check to see if we have a TaxChat expert lined up. If so, we'll call ShowExpertScreen which will run our final animation and move on to the next screen. If not, we'll either give it some more time or go back after 12 passes.

Let's take a look at our close animation method. Here we're animating our chevrons back to their center positions using the same spring damping and velocity values. On completion we update the label text, attempt to match the user with a tax preparer, update the pass count and then call the ChevronsOpen method.

void ChevronsClose()
{
    // AnimateNotify ( duration, delay, spring damping, spring velocity, options, animations, completion )
    UIView.AnimateNotify(0.5, 0.5, 0.85, 1.0, 0,
        () => {
            leftChevronView.Center = leftChevronCenter;
            rightChevronView.Center = rightChevronCenter;

            leftCover.Frame = leftCoverFrame;
            rightCover.Frame = rightCoverFrame;
        },
        (finished) => {
            // swap out text on even passes
            if(passes % 2 == 0) {
                titleLabel.Text = "We're finding your TaxChat Expert";
            }
            else {
                titleLabel.Text = "Just a moment";
            }

            // match user with a preparer
            AsyncHelper.RunSync<bool> (GetPreparerId);

            passes += 1;
            totalPasses += 1;

            ChevronsOpen();
        }
    );
}

When we're ready to move on we call the ShowExpertScreen method. Here we're animating our chevrons and text label out with no spring effect. Then when the animation is complete we move on to our next screen.

void ShowExpertScreen()
{
    // AnimateNotify ( duration, delay, options, animations, completion )
    UIView.AnimateNotify(0.5, 0.5, UIViewAnimationOptions.CurveEaseInOut,
        () => {
            leftChevronView.Alpha = 0;
            leftChevronView.Center = new CGPoint (20 + 11, chevronY);
            rightChevronView.Alpha = 0;
            rightChevronView.Center = new CGPoint (screenWidth - (20 + 11), chevronY);
            titleLabel.Alpha = 0;
            titleLabel.Center = new CGPoint (titleLabel.Center.X, titleLabel.Center.Y - 50);
        },
        (finished) => {
            // go to next screen
            NavigationController.PushViewController(expertController, true);
        }
    );
}

To be continued...

So those are a few ways to animate in Xamarin.iOS. There are many more. It may seem complicated, but it's really not that bad. If you're thinking about adding animations to your app, things are going to go a lot smoother if you have a designer who can provide animation comps. For the animations in TaxChat we were given video comps, as shown, and Flinto prototypes, which were also helpful. Having a set visual guide for your animations will allow you to plan and implement the best solution.

There are still a couple more animations from TaxChat that I'd like to share with you, but to keep things a bit more digestable I'm going to split those off into a second post that'll be coming up soon. The next post will discuss animating a CGAffineTransform and using CABasicAnimation. Thanks for reading, I hope you enjoyed this post and I look forward to having you return for part two. Cheers!

We solve problems with technology. What can we solve for you?

Reach Out

t: 800.646.0188