Building and Animating User Interfaces With Shapes on iOS
Building and Animating User Interfaces With Shapes on iOS
Shapes is a powerful tool for drawing shapes and controls on iOS. It's even more powerful with XCode 6 and Swift because in Swift playgrounds you can watch how UIBezierPath builds itself, step by step, line by line. Check it out here!
Join the DZone community and get the full member experience.Join For Free
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 these kinds of designs? We'll try to answer this question with a new framework, called Shapes.
Shapes is a set of wrappers that allows simple yet powerful ways 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. With Shapes we are bridging the gap between these two.
DTShapeView is a UIView, that is backed by CAShapeLayer instead of CALayer. To setup it's shape, simply create UIBezierPath, pass it to this class, and its properties will automatically be converted to underlying CAShapeLayer properties. Not only that, it also provides a set of properties that allow 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 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 that define exactly that. Meet DTProgressView, DTShapeView subclass that builds on top of those.
By default, DTProgressView fills entire view bounds, filling view 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 functions 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 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 it 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 entire button frame. And since any DTShapeView property is animatable, changing its path can be used to animate changing button geometric shape. But, we should be careful here because changing shape is not so simple as it might sound. Why is that?
AppStore Download Button
Let's take a look at AppStore download button. It has the initial shape of a rounded rectangle. After the user taps it, it animates to an infinitely spinning circle. And you might think, ok, I will simply animate from the rounded rectangle to the circle, and that's it. But after trying to do so you will learn that the animation is drawing unnecessary angles and doesn't animate 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 rounded rectangle:
It has many 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.
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 a form of a rounded rectangle, and the other in a form of a circle. Button animation consists of two parts. The 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. 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 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. And, it became even more powerful with XCode 6 and Swift because in Swift playgrounds you can watch how UIBezierPath builds itself, step by step, line by line. This is just a start for Shapes, and we can't wait to see, what you will be able to build with it!
Published at DZone with permission of Denys Telezhkin , DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.