7 ways to to speed up your UI on iOS

Daniel Larsson
6 min readMar 18, 2017

--

A user interface that lags and freezes can often lead to uninstalls, no matter how brilliant the app’s use case is. As we add features and beautiful animations, some degree of unresponsiveness seems inevitable.

I am generally opposed to doing premature optimization, as I believe it is rarely worth the time spent improving something if you have no framework for measuring the results of said improvement.

Premature optimization is the root of all evil — Sir Tony Hoare

Even though Sir Tony Hoare was most likely thinking of the impact of Moore’s law on application development when writing this legendary quote, it holds true to this day. I try to base most of my optimization efforts on cold hard facts that I get from profiling my code in Instruments. Optimizing without measuring results seems inefficient and unjustifiable.

However, when it comes to UI there are a few things that I always keep in mind while writing code. None of them are particularly hard to either grasp nor follow, but can help ensure your app stays responsive.

1. Learn how to use Instruments

This is a prerequisite that you should learn before doing any optimization. Instruments is a part of the Xcode toolset, and provides powerful performance-analysis and profiling. Without it, you are blind.

Firing up Instruments every time you make code changes is overkill. Instead, make it a habit to lay an eye on the Debug navigator gauges while running the app in debug mode. They provide information about some of the most important performance areas, such as CPU load, memory, and networking. By checking this every now and then, it’s easy to tell when one of these values reaches higher peaks or stays active for longer periods of time than normally. That means it’s time to dive into Instruments.

Xcode’s Debug navigator

I mostly keep an eye on memory usage and CPU, though looking at disk- and network usage while engaging with the app can sometimes reveal unintended I/O work.

I suggest learning more about how to become efficient in Instruments either from this Ray Wenderlich tutorial or the official guide from Apple.

2. Use opaque views where possible

You know that opaque checkbox in the Attributes Inspector that seems to do absolutely nothing? It can make a significant improvement to performance if used correctly. By making a UIView opaque, you are telling the drawing system that the view has a solid background color - without any transparency. With this information, iOS can make performance optimisations that will render the view more efficiently.

If you leave opaque off, the view will have to blend with the view underneath it. Blending two views is not a very expensive operation per se, but the complexity can grow exponentially as additional layers containing gradients or moving elements come into the equation.

3. Minimise work done in continuously called methods

By continuously called methods, I am first and foremost talking about scrollViewDidScroll: and delegate methods on UIGestureRecognizers. It’s not always easy to estimate how much work a method is actually doing, and this is where Instruments come in.

iOS can render 60 frames per second, and this is by no means hard to reach. Every Hello World app runs at 60 fps, and will keep doing so unless you start performing tasks that overload the GPU/CPU.

Let’s break 60 fps down to see how it actually impacts our code. 60 frames in one second translates into 16.6ms per frame. This is how much time you have between frames to figure out what to draw next. When writing delegate methods for scroll views and gesture recognizers, keep this in mind. Every time iOS redraws your views, it will need to complete all of your autolayout calculations, view blends, layer animations, and so on, in less than 16.6ms. If a frame render takes 25ms instead of 16.6ms, the fps count drops from 60 to 40 and your responsiveness will take a hit.

4. Use Grand Central Dispatch

Using GCD might be an obvious one, but I think it deservers a mention. GCD is fairly easy to use, but its C-based API can look daunting for someone used to Objective-C or Swift.

Grand Central Dispatch is a prettified name for libdispatch, and provides concurrency support by moving work to dispatch queues managed by the system.

Any work that doesn’t have to run synchronously (meaning the program waits until execution finishes before moving on) and that does not interact with UIKit, can and often should run on a background thread. This prevents the UI from becoming unresponsive while waiting for work to finish.

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Running on background thread, don't call UIKit here
dispatch_async(dispatch_get_main_queue(), ^{
// Running on main thread, feel free to call UIKit here
});
});

Notice the queue priority in the code above. Use this to tell GCD how important it is that the tasks run as soon as possible. All high priority tasks will complete before running a low priority task, so it’s a good thing to make a habit out of specifying a priority whenever using dispatch_asynch().

Keep in mind that attempting to synchronously execute a work item on the main queue will result in a dead-lock.

Use dispatch groups to aggregate synchronization. Let’s say that you have multiple tasks that need to finish before updating the UI. If we put all these tasks into a dispatch group, we can get notified when they have all been completed, and can then update the UI as planned. Dispatch groups are thread safe, so tasks can even run on different queues. Here’s an example of waiting for two asynchronous tasks to complete before moving on:

dispatch_group_t imageGroup = dispatch_group_create();dispatch_group_enter(imageGroup);
[uploadImage executeWithCompletion:^(NSURL *result, NSError* error){
// Image successfully uploaded to S3
dispatch_group_leave(imageGroup);
}];
dispatch_group_enter(imageGroup);
[setImage executeWithCompletion:^(NSURL *result, NSError* error){
// Image url updated
dispatch_group_leave(imageGroup);
}];
dispatch_group_notify(imageGroup,dispatch_get_main_queue(),^{
// We get here when both tasks are completed
});

Note that every enter must be balanced with a corresponding leave to allow the group to finish.

A tip on using GCD: set the queue as early as possible in the call chain. Most popular libraries, such as AFNetworking and Alamofire, call their completion blocks on the main thread. You should too. Doing so can prevent concurrency issues that might be hard to spot down the track. There are exceptions for this, but only when working with objects that go no where near UIKit, and that can deal with concurrency. If calling a delegate method or a completion block on anything other than the main thread, make that clear to your fellow developers and your future self.

5. Use AsynchDisplayKit

In an effort to achieve smoother UI, Facebook built AsynchDisplayKit to power their Paper app back in 2014. It has since then been open sourced, and is today used by many apps (including Pinterest, read the story about their transition here).

Its main use is retaining 60fps while scrolling/animating through list views or cells, no matter how complex they are. I won’t go too deep into showing how to use ASDK, as there are some brilliant tutorials from Ray Wenderlich and AppCoda out there.

ASDK adds support for rendering views off of the main thread via a thread safe abstraction of UIView. It supports most of the common UIKit components, such as UIView (called ASDisplayNode), UIImageView (ASImageNode/ASNetworkImageNode), UICollectionView (ASCollectionView) and UITableView (ASTableView).

6. Rasterize layers when necessary

CALayer has a property called shouldRasterize that, when enabled, indicates that the layer should be rendered as a bitmap before compositing.

view.layer.shouldRasterize = YES;

This can give significant performance improvements, since the layer does not have to be constantly re-rendered. Keep in mind that this will actually increase the memory footprint of the view/layer, so make sure to reuse cells if using this inside of a UITableViewCell or UICollectionViewCell.

To make sure it rasterizes correctly on Retina screens, you should also match the rasterizationScale to that of the screen:

view.layer.rasterizationScale = [UIScreen mainScreen].scale;

7. Round layer corners, not view corners

I got this tip from iOS Dev Nuggets, and have been following it ever since. Enabling masksToBounds is not something you should be afraid of doing, but rendering many views with masksToBounds enabled can come with quite significant performance losses (we keep coming back to UITableViews and UICollectionViews here).

When you want to round the corners of a view - instead of enabling masksToBounds, let the view be unmasked and simply round the background!

view.layer.backgroundColor = [UIColor blueColor].CGColor;
view.layer.cornerRadius = 10;

--

--