iOS 7 Style Progress Meter in C#

Microsoft

On iOS 7, when you download or update an app from the App Store, there's a small circular progress indicator that shows how complete the download is. It's simple, compact, and provides information well.

Progress Indicator Example when downloading the vndr app

Making a version of this is incredibly easy using C# and Xamarin on your iOS app.

Layers, Everywhere

Every UIView has a property called Layer exposed that is responsible for what's rendered inside that view. It's of type CALayer and is a tree hierarchy, just like the view heirarchy. There's a subclass called CAShapeLayer which is a layer type specifically made for drawing shapes. You can read more about layers in this article
. Layers, Layers Everywhere Meme

We're going to use CAShapeLayer to draw a UIBezierPath which just happens to be a circle. A very important property of CAShapeLayer is StrokeEnd which tells iOS to draw the path all the way up until that percent of the path is reached. If you only want draw the first 50% of a line, you'd specify a StrokeEnd value of 0.5.

So, if you're creating a progress indicator, which by definition is showing how complete something is (from 0% to 100%), it's not a big leap to see how we can simply update the StrokeEnd property as our progress... uh... progresses.

Here's the general shape of our code:

var center = new PointF (this.Bounds.Width / 2, this.Bounds.Height / 2);
UIBezierPath circlePath = UIBezierPath.FromArc (center, this.Bounds.Width * .7f, (float)(-.5 * Math.PI), (float)(1.5 * Math.PI), true);

var _progressCircle = new CAShapeLayer ();
_progressCircle.Path = circlePath.CGPath;
_progressCircle.StrokeColor = UIColor.Green.CGColor;
_progressCircle.FillColor = UIColor.Clear.CGColor;
_progressCircle.LineWidth = 1.5f;
_progressCircle.StrokeStart = 0f;
_progressCircle.StrokeEnd = 0f;

this.Layer.AddSublayer (_progressCircle);

It starts with a center point, from which we create a UIBezierPath that is a circle. There's some funky math in there using PI. These two parameters define the start and end points of the arc, in radians. I've started my arc at "North" so it's at -½ π. The end point is 360 degrees away over at 1½ π. You can start your arc wherever you want. For more information about creating an arc using UIBezierPath, see the documentation.

In the middle of that line, I've set the radius to 70% of the width of the containing view. Once we've got our path, we create the CAShapeLayer, set the path, some colors, line width, and where we want to stroke the line. Since our progress starts at 0%, the StrokeStart is set to 0 and StrokeEnd is also set to 0. Then, to add it to our view, we access the powerful Layer property on the view and add our circle to it.

But if this is all we did, we'd see nothing as it's not stroking any of the circle; it starts and stops in the same point. Since we're making a progress indicator, to update the displayed progress, all we have to do is this:

_progressCircle.StrokeEnd = 0.83f;

That's it. The circle will get drawn to 83% of the end point. It looks something like this:

Screenshot of progress circle at 83$

Of course, a progress indicator changes over time, so make sure you're updating the StrokeEnd as the value changes.

One thing to note: if you are updating status on a background thread, you might need to do this on the main thread:

InvokeOnMainThread(() => { _progressCircle.StrokeEnd = 0.83f; });

Sample Code

I've created a sample Xamarin iOS project that extracts the code in this post into a custom UIView. The code is up on GitHub, so check it out. I hope it helps!

Music for this post: Saint-Saƫns: Symphony No. 3