2011 • 19:58
Smooth animation and fast graphics performance are key to creating a great app on iOS. In this video, you will learn tips and tricks for compositing views with UIKit on iOS to help you achieve these smooth animations.
Speaker: Bill Dudney
Unlisted on Apple Developer site
Transcript
This transcript was generated using Whisper, it has known transcription errors. We are working on an improved version.
Hi, I'm Bill Dudney, the Application Frameworks Evangelist. Smooth animation and fast graphics performance are key to creating a great app on iOS. In this video, you're going to learn about compositing views with UIKit on iOS and see some code that will help you achieve smooth animation in your apps. So let's get started.
Animation is intrinsic to good user experience on iOS. 60 frames per second is the gold standard. That's where the application feels natural under the user's finger. It's where the elements on the screen will move according to the way the user wants them to. But that doesn't give your application much time. It's about 16.67 milliseconds per frame.
And we have a lot of work to do. First, your application has to do all the calculations that are required to get whatever data you're displaying on the screen ready. You might be showing users first names and last names. You might be showing icons. All of those calculations that go into creating or coming up with which data should be displayed have to be done first.
Next, you have to lay out the content and draw whatever data you've decided needs to be drawn. And then that frame has to be rendered. And that has to be done, again, in 16 milliseconds. So the bottom line is your application cannot waste any time. You want to prepare your content quickly. If you're using a table view, it's so important that you reuse the table view cells. We still see examples of people forgetting to do this. In 16 milliseconds, you don't have time to be allocating a new table view cell every time one's required.
Also, if you have more than one kind of table view cell that's going to be displayed, make sure that you just use a different cell identifier. When you go to dequeue your reusable cell, you can pass in different identifier names and use a different identifier for each different type of cell instead of trying to lay each cell out according to whatever the format is. You also want to make sure that you render quickly.
You want to simplify the structure of your view hierarchy. So if you have eight views on screen and you can get by with four, then please get by with four because those extra views will add time to each of those render passes. Also, make sure to remove unnecessary or invisible views.
If you have views that are off-screen or aren't adding visual content to the user interface, they still will take time to render, so make sure that we get rid of those as well. There are a lot of other things that your application can do, though, to make your performance even better. Before we go there, we need to talk about how compositing works. This is a screenshot of a sample application that I wrote to help explore these ideas.
It's a typical table view application. On the left-hand side, we see some image views. On the right, we see two text fields. And here we see one of these typical table view cells blown out in a view where we can see exactly what's going on. We've got four views here, the background view, the image view, and those two text field cells. Each one of those cells has to be composited together and placed into the table view. And then each of the table view cells have to be composited together into the table view.
So here's another view of what that looks like. On the top, we have the four views again, and they composite together. And then each of those cells composite together to make up the full table view. Now, we sort of slowed it down here in Keynote, but every single frame of your table view, as you flick it, has to go through that same set of operations, compositing each frame of that animation.
Let's look at the table view cell in a little more detail. Again, we have the image view off to the left. And the two text fields on the right. All three of these sub views are marked as opaque. And if you remember back when we saw the animation of the table view cell blown out, there were some cutout areas in the underlying UITableView cell. Those cutouts don't have to be written because we have these opaque views over the top. We'll look at that in more detail in just a minute. Let's talk about what it takes to generate a frame.
Each frame of your application has to go through these three steps. It has to read pixels, it has to write pixels, and then it has to go through at least one rendering pass. The important thing for you to keep in mind is that only a certain number of operations can be achieved in that magical 16 milliseconds that we have to achieve our 60 frames per second.
Reading pixels requires that we take every pixel from the source before we can write it to the destination. And if you try to read too many pixels, your frame rate is going to suffer. So here's an example. We have this beautiful photo of aspens with snow on the ground. It's about a million pixels. It's 1152 by 768. We want to draw that in an area that's roughly 70,000 pixels at 240 pixels by 320 pixels.
That's essentially wasting 925,000 pixels worth of read bandwidth. That's going to cost our frame rate. Writing pixels requires that every pixel on the screen has to be written at least once. If your application tries to write too many pixels, your frame rate again will suffer. So we're back to our tree picture here, and we have the word trees over the top of our image. And there's opacity around that text field cell.
Every pixel that's inside that orange boundary there has to be written twice. Once to write the image that's underneath it, and then another time to do the math to figure out how that transparent orange color blends with the background. The last thing your application has to do is go through a rendering pass. Some content needs to be rendered into a separate buffer before it can be rendered on screen. And if you switch buffers too often, your frame rate will suffer.
So we see here we've got this image that has the beautiful rounded rectangle corners on it. It's a great effect. If you use masking though, that's going to require that image to be rendered into an off-screen buffer. We'll look at some strategies to make this fast in just a moment.
In order to generate a shadow, your application has to go through another rendering pass. To do that, essentially what your application has to do is render that image as a gray square or whatever color you have set as your shadow color. It then causes a blur around the edge of the image that it drew. Then it has to place that behind your image and composite those two things together. That gets done in that off-screen buffer, which then gets composited into the final screen. So generating a frame has a certain set of costs.
How many source pixels are you reading? How many destination pixels do you have to write out? And how many buffers do you have to render to? The things to watch out for to see if you're doing too much of any of those three things is large source images, alpha blended content, and finally masking and shadowing effects. So let's talk about some strategies to get your application to write fewer pixels. So here's our trees image again. We have the trees text, and remember each one of those pixels that's inside that trees text field has to be written twice.
If we go back to our table view cell, we had each of those sub views marked as opaque. Since the underlying pixels don't have to be written, each of those pixels only have to be written out once. To reduce our writing bandwidth, we need to go into instruments and check that color blended layers checkbox.
When you do that, you'll notice the UI of your iPhone switch to this green and red overlay. That shows you opaque regions colored in green and transparent regions colored in red. The deeper the color of red, the more blending is going on. Fixing the problem is easy. In Interface Builder, you can just select your view and then you check that opaque checkbox. If you're building your user interface via code, you'd simply set your opaque property on your view to be yes, and that will cause that view to be marked as opaque.
So once we mark everything as opaque, we see we have a pure green screen, except for the stuff that's showing up on the bottom and on the top. Since those pieces aren't animated when we're flicking our table view, they matter a lot less in terms of the compositing performance. Here's an example that I put together just to see how far I could tax the iPhone 4. We see here on the right, there are several text fields that are marked as transparent. Even with all this transparency, the iPhone 4 still isn't very taxed.
But in your application, if you do this, you'll be wasting power and CPU. So you need to go through and check your application and make sure that all of the views that can be marked as opaque are marked as opaque. And that's writing bandwidth. You want to minimize the number of alpha-blended pixels. It's really easy to do that. You just set your Views Opaque flag to Yes.
Only use transparency if you really need it. Now, I don't want you to leave here thinking that you shouldn't use transparency, because you should. It adds great effects to your applications. But in places like we saw in that table view cell where the background color is constant, it's all white, we can easily mark our text field cells as opaque and set their background color to white as well.
The user won't be able to tell the difference if we have opaque or not, other than the fact that it will use more processor. So you want to go into Instruments and use that Color Blended Layers checkbox to find out where you're using transparency that you don't need to be.
Also, make sure that your opaque images don't have an alpha channel, because that alpha channel is what Core Animation uses to determine whether or not it should do alpha blending with your images. You can consider, if performance is still an issue, cutting up your views so that you have opaque regions and non-opaque regions. You would divide your view into several subviews in order to do that.
If your view hierarchy is still too complex or needs transparency, consider drawing your content with Quartz. Now let's talk about reading fewer pixels. So each pixel that ends up on the screen has to be red. And here again we have our beautiful Aspen photo. And we're going to draw this roughly a million pixels into this tiny little box.
We talked about earlier how that's wasting read bandwidth. There's also another issue of quality. You notice here on the left hand side where we've gone through the default core animation scaling, you see both in the image that's got the sign on it and the image with the Aspens, that both of those images look rather pixelated. Now I have blown them up so that I could emphasize that effect. But in your application, your users will still be able to see that. It might not particularly register what the issue is.
On the right hand side, we see those same images drawn with the same scale, but using the quartz filtering instead of the default core animation one. And you notice that we can actually tell that on that sign there's words. And the Aspens also look really nice too. They have a nice blurring effect. From the user's perspective, that will look a lot more natural than the one that's pixelated. On the left.
To identify your resized images, you'll just go into Core Animation Instrument again, in the Instruments application, and check this color misaligned images. When you do that, you'll see all your scaled images, as we see here on the right, colored with a yellow overlay. To fix that, we're going to use Quartz, also known as Core Graphics.
Core graphics sits underneath UIKit along with core animation. UIKit has most of what you need to be able to build an application. Core graphics though has a lot of features that allow us to do things like scaling images, which we need to do now. Here's some essential concepts that you need to know about the way core graphics works to be able to draw these images. It's a full 2D graphic engine. It's based on the PDF spec. So just about anything you can imagine being in a PDF can be drawn programmatically using core graphics.
It's resolution and device independent, meaning that we draw on a scale based on points, not on pixels. Each of those units is a CG float, so we can get very accurate in terms of where we want things to be. Keep in mind that a point is not necessarily a pixel.
The origin on our canvas is in the bottom left on iOS. And that canvas is known as a CG context. That context is the destination for all of our drawing. It holds a bunch of state like the path that we want to draw, how we want to fill that path, what the stroke looks like, what color we want to stroke it with, even things like the dot pattern that we want to use for our lines.
The number one take home point from this is I want you to remember that UIKit has most of what you need. So you're going to be using UIKit most of the time. When you need to drop down into core graphics, you're going to probably want to use UIKit objects to do the heavy lifting and then get the CG objects from those UIKit objects. So for example, a UI color, it has a method called CG color that will return the core graphics color object that that UI color wraps. And the same thing with UI images.
If you have an instance of UI image, you can ask it for its CG image and that will return you the underlying core graphics image that you need to draw in core graphics. In UIKit, we use views to contain our drawing. Behind those sit layers and layers get their content in any number of ways. The way we're going to use in our example here is we're going to use delegation. So we're going to become the delegate of a CA layer and draw our content with this method draw layer in context.
We could also subclass the layer by creating a subclass called my layer and implement the drawn context method. The context that's passed into this method, draw layer in context, is a CG context ref, that destination for all of our drawing. So here's the code that draws our image.
Initially, what we're going to do is load the image, potentially even in the background. So we have a method. It's abstract behind the scenes. It might be doing all sorts of crazy stuff. We're just going to call that method and say load the image. Assuming we got an image back, we're going to calculate our new size that we want to draw that image at.
[Transcript missing]
You might be doing this in your application. What's happening behind the scenes though is that's forcing Core Animation to send your application to an off-screen buffer, render it in that off-screen buffer, and then bring that back into the compositing scene. To continue to get those beautiful rounded corners but not pay the hit rendering our content off-screen, we're going to go back to Core Graphics.
In our Draw Layer in Context method, we can take the rectangle that defines the bounds of our layer and use the UI Bezier Path class to generate a path that has those rounded corners for us and then set that as the clipping path for our context. Then when we tell that image, "Hey, draw in this context," that the context will take care of rounding those corners for us and it won't have to render it into an off-screen buffer. So we'll continue to get that beautiful look with the rounded edges. But not pay that hit. You could also decompose your rounded corners into separate views or layers.
The point is to find a creative solution. Look at what's going on in instruments, figure out where your performance problem lies, and then look for ways to fix that. Drop shadows also cause us to go into an off-screen rendering pass. We need a more efficient way to generate those shadows. One approach is to use the shadow path property on CA layers.
That defines the opaque region of the layer in which we want to use to generate the shadow. That also lets the compositor cache the shadow bitmap in your application so it can use it again for each frame that it has to render. And here's the code to do that. You set up your layer, you set the shadow properties on your layer, and then you use a UI Bezier path and set that as the shadow path on your layer. And that's all there is to it.
So in summary, you want to use this algorithm to optimize your application. While the frame rate is less than 60, you want to first eliminate additional rendering passes. Remember the goal is to have one rendering pass per frame for your application. Next, you want to reduce the read bandwidth that you're using by using the appropriately sized images, and where you can't, you want to use core graphics to draw them at the smaller scale.
You also want to reduce your write bandwidth by eliminating unnecessary opacity. So use your knowledge about the system to build a better performing application. Come up with creative solutions. Look at what's going on with instruments to find out where your application is suffering, and then apply the knowledge that you've gained from this talk to help you come up with those creative solutions. Remember, always use instruments to measure a baseline so that you can tell what a great difference you've made.
So there you have it. We've covered compositing on iOS. You've learned some tips and tricks on how to achieve smooth animation in your apps. Now go fire up instruments and start looking at the things we've talked about. I'm looking forward to seeing how you'll apply this information to make your apps even better.