Building and Animating User Interfaces with Shapes on iOS
Since the release of iOS 7 application designs for iOS have dramatically changed. With the termination of skeuomorphism came solid colors. Icons and buttons are not trying to recreate look and feel of real objects, and are opting to be schematic.
Shadows, gradients, complex controls are all gone. Simplicity, clarity, efficiency - these are the new goals for iOS 7, iOS 8 and iOS 9. We are seeing more and more interfaces that are very simplistic, and instead of using complex imagery they often use simple geometric forms. Circles. Rectangles. Triangles. How do we implement this kind of designs? We'll try to answer this question with a new framework, called Shapes. Below is a detailed guide on its efficient use.
Shapes is a set of wrappers that allows a simple yet powerful way of drawing and changing shapes on iOS. CAShapeLayer and UIBezierPath are the two main keystones it is built on.
CAShapeLayer is one of CALayer subclasses, that allows drawing cubic Bezier spline in its coordinate space. And UIBezierPath is a class, that defines geometric Bezier path. These two classes are very similar yet they serve completely different purposes. UIBezierPath is used for defining a path, and CAShapeLayer is responsible for drawing it. Both classes have very similar properties, but it's not so easy to use them together since one is using QuartzCore framework and CoreFoundation and the other one comes from UIKit world. We are bridging the gap between these two and would like to show you how to animate UIBezierPath with Shapes.
DTShapeView is a UIView, that is backed by CAShapeLayer instead of CALayer. To set up its shape, simply create UIBezierPath, pass it to this class, and its properties will automatically be converted to underlying CAShapeLayer properties. Not only that, but it also provides a set of properties, that allows using CoreGraphics properties and enums, such as CGLineCap and CGLineJoin, completely bypassing QuartzCore and CoreFoundation.
Now, having geometrically shaped view is cool, but what can we do with it? The first thing that comes to mind is the progress view.
In a nutshell, what is a progress view? It's some kind of a geometric figure, most likely a bar, that is filling from start to the end. If you are familiar with CAShapeLayer, that should immediately ring a bell, because it has properties strokeStart and strokeEnd, which define exactly that. Meet DTProgressView, DTShapeView subclass, that builds on top of those.
By default, DTProgressView fills entire view bounds, displaying the progress gradually from left to right. And all you need to do to create a progress view is to drop it onto your view, set strokeColor and your progress view is ready!
self.simpleProgressView.strokeColor = [UIColor greenColor];
Whenever your progress changes, simply call
[self.simpleProgressView setProgress:progress animated:YES];
And progress change will be animated. Of course, duration and animation function are configurable. But let's not stop there! DTProgressView is a DTShapeView subclass, which means that it allows any geometric shape, that can be defined by UIBezierPath. And you can go crazy and make something like this:
What if we want to cut one shape from another? Sometimes you need a tutorial in your app, and you want to dim some parts of the interface to shift user attention to some element in the UI. Sometimes you may be building a photo picker, that picks a rounded photo of the user, and you want to hide everything else except this circle, that will be selected. This is where DTDimmingView comes in.
By default, DTDimmingView dims its entire bounds. So all you need to do is create UIBezierPath, that will define which part of the view should be visible, and set the border parameters to visiblePath property.
UIBezierPath * roundedPath = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(self.view.bounds, 100, 100)]; self.dimmingDynamicView.visiblePath = roundedPath;
Ok, drawing shapes is fun, but how about interacting with them?
DTShapeButton is a UIButton, that has a DTShapeView added as a subview. By default, this DTShapeView fills the entire button frame. And since any DTShapeView property can be animated, changing its path can be used to animate changing button geometric shape and size. But we should be careful here because these changes are not so simple to implement as it might sound. Why is that?
AppStore Download Button
Let's take a look at the AppStore download button. It has an initial shape of a rounded rectangle. After the user taps it, it animates to infinitely spinning circle. And you might think, ok, I will simply animate from rounded rectangle to circle, and that's it. But after trying to do so you will learn, that the animation is drawing unnecessary angles and animates not exactly how you would expect it to. And after looking at the docs for CAShapeLayer, you will immediately understand why.
If the two paths have a different number of control points or segments the results are undefined.
Here's how UIBezierPath draws a rounded rectangle:
It has much more control points and segments than a circle does. When changing paths, CAShapeLayer tries to interpolate between these lines and, obviously, fails because the number of lines and control points differs. You may encounter totally unexpected animations like swirling or partial drawing of views. This is not acceptable as they have to be smooth.
One obvious solution would be to draw shapes with the same number of control points. At its core, the circle can be drawn as a rounded square with a corner radius of half of its side. And this approach works fine, but only on iOS simulator, not on the actual device. This happens because of device optimizations, it seems like iOS is automatically estimating how shapes could be drawn. And even if you created shape as a rounded rectangle, if it can be drawn as a simple circle, it will be drawn as a circle with the same number of control points as a circle.
There are different solutions, that can help you here.
Our approach is to have 2 buttons instead of one, one in the form of a rounded rectangle, and the other in the form of a circle. Button animation consists of two parts. First part is to animate to a rounded rectangle with a very small width. When the first animation is completed, we hide the first button and show the second one as a CAShapeLayer circle. This way transition between two buttons is almost unnoticeable.
And here's result in motion:
Having two buttons is not a panacea, though. Sometimes you might go in a completely different direction. For example, if you want to build an iOS 7 Voice Memos record button, having two buttons is no longer going to cut it.
Voice Memos Record Button
The solution here is a little different. Instead of changing the button shape itself, we chose to animate the mask of CAShapeLayer. This is possible because CALayer mask can itself be a CAShapeLayer. To achieve implicit animations with it, we wrote DTAnimatableShapeLayer class. After that, animating the button state change is a matter of simply decreasing or increasing the mask around a button.
Shapes is a powerful tool for drawing shapes and controls on iOS. Though this framework is built with Objective-C, you can easily animate UIBezierPath in Swift. Moreover, with XCode 6 and Swift it became even more powerful because in Swift playgrounds you can watch how UIBezierPath builds itself, step by step, line by line. This tutorial is just a start for Shapes, and we can't wait to see, what you will be able to build with it!
P.S. You can find everything we've talked about here, in an example project for Shapes. And, of course, installation through CocoaPods is supported. Feel free to use!
Shapes on GitHub
Animating the Drawing of a CGPath With CAShapeLayer
Looking for a professional team of iOS developers?
At MLSDev, we have talented and skilled iOS developers who are ready to deal with the most challenging tasks and create great apps fot iOS. Contact us to discuss your project.