Cocoa Touch Best Practices

Session 231 WWDC 2015

Small improvements can make a big impact on your iOS app's performance and launch time. Learn best practices to optimize your UIKit-based applications. See how to future-proof your app for what may be next, and walk through numerous real world examples for more responsive and dynamic collection and table views.

[Applause]

LUKE HIESTERMAN: Hello, everyone.

Thank you so much for coming to Cocoa Touch best practices.

I am Luke Hiesterman, an engineer on the UIKit team, and it will be my pleasure to walk you through today a collection of pieces of wisdom, practical bits of advice that you can apply directly to the applications that you are writing today and into the future.

So I am going to do this by walking across a collection of topics that will be of strong interest to any Cocoa Touch application, and in each of these topics, I will have sort of a series of best practice tips to give to you, and those topics are app life cycle, number one; views and view controllers; Auto Layout; and finally, table and collection views.

Now, as I go across these topics, I am going to have a series of goals that I want to impart to you with each of the bits of wisdom that I am giving because everything is sort of corralled towards accomplishing a few basic ideas that you definitely want to have in your app.

So you know, number one is going to be you want to have peak performance.

Your apps want to be silky smooth, so you look like rock star developers and everybody loves you.

You also want to have a top-notch user experience in your app.

That way everybody thinks that your app looks polished and it's awesome.

And finally, you want to write your code in such a way that it's as future-proof as possible so that as future versions of iOS come out, you're writing as little code as possible to adapt to those revisions of iOS.

So those are the goals we are going to have in mind as we go through these topics.

And I will start off by talking about app life cycle.

The very first best practice I want to impart upon you has to do with the very first experience that the user has with your app, and that's launching it.

So the first best practice is launch quickly.

That's how you appear responsive when the user taps on your icon and right away they get your app ready to interact with.

And the way that you launch quickly is extremely simple.

It's return quickly from the Application Did Finish Launching UI application delegate event.

That in itself is really simple.

I am sure you all already know how to do it.

Take all the long-running work you might have to do to set up an application and defer that out of Application Did Finish Launching because you want to return as fast as possible doing the minimum amount of work, set up a basic UI for your users to interact with, and return.

So if you are loading data, whatever you need to do, from a database, network, defer that out of Application Did Finish Launching.

If you take too much time, of course, your app will be killed because it just looks like it's hung to the system, so you really want to return as fast as possible from that.

Now, being a superresponsive app doesn't end at application launch.

We want to think beyond that.

We want to be superresponsive all the time.

So I want to delve deeper into this technique of what it means to be responsive in general so we can build a technique that works not just at app launch but throughout the life cycle of your app.

So even though I just talked about deferring all of this work out of Application Did Finish Launching, what we are really getting at for a best practice isn't just about asynchrony.

It's actually about taking long-running work and putting that on background queues.

If you need to load data from a database, if you are hitting the network, that's work that can be done in the background.

So if we revisit Application Did Finish Launching and we have this sort of very simple approach to a naive Application Did Finish Launching, you know, we see we load our data directly in Application Did Finish Launching, and I just said defer that.

Okay. So we can do that pretty easily.

We dispatch that, and it's gone.

It's out of Application Did Finish Launching.

We are able to launch quickly, and things are better at launch.

But that still introduces the possibility of blocking the main queue later on and, thus, blocking user interaction.

So really, the best practice is move that work onto a background queue so that whenever it does run, user interaction continues to happen, and your application seems responsive all the time.

So this technique, putting the work on background queues, can be applied anytime in your app, not just at launch.

Then, you take that work that you do in the background working with data, and when you are done with it, that's when you come back to the main queue to interact with UIKit elements like views and view launchers.

So that's being really responsive.

The next thing you want to do besides that first launch is be superresponsive on the second time the user launches your app, and the third time, and so on.

And this comes from the fact that when the user exits your app, the app doesn't just die, it goes into a suspended state on iOS.

And so to be superfast the second time the user goes to your app, you really just want to resume from that suspended state, and that's contingent upon you still being in memory.

So if we take a look at a picture of what system memory looks like, you know, we know that some of it is taken up by the kernel and the operating system processes.

A good chunk is going to be taken up by the foreground app.

And then a bunch of it is going to be taken up by background apps.

Now, you'll notice that there's one sort of hog of a background app in this picture that's using up more memory than everyone else.

You don't want your app to be that app, and the reason you don't want that is because that app is the first one that's going to die when the foreground app needs additional memory.

So you want your app to be the one that's using UI application delegate methods to know when it's going into the background, get rid of unneeded memory resources, take its memory footprint and get it as small as possible when it's going into the background.

This is even more important in the world when we have split view and there can be multiple foreground apps.

You know when a second foreground app comes, that big hog of an app isn't going to survive.

So you don't want to be that.

So that's being superresponsive and thinking about performance throughout the life cycle of your app.

The next best practices I want to talk about with respect to general application programming is leveraging frameworks.

Now, this is maybe the most basic best practice I can give to you, but that's: do it!

Leverage frameworks that Apple provides.

We spend our lives throughout the year building great frameworks for you to build on top of, and doing so comes with several basic advantages that I am sure you are familiar with.

It reduces your maintenance burden.

You know? If you use UI Navigation Controller, for example, then you don't have to maintain the Navigation Controller across releases as you would if you built your own Navigation Controller.

And as we make improvements, you get those improvements for free.

For example, you know, Navigation Controller gained a swipe gesture for going back a couple of releases ago.

Everyone who built on top of it got that improvement for free.

If you had your own, you would have had to go implement it or not have a feel that fit with the rest of the system.

So that's what you want because you really want to be able to spend your time focusing on what makes your app special.

That's what we all want.

We want you to write fantastic apps and spend your time on that rather than things that you could leave to our framework.

So that's what we want to encourage you to do.

And of course, while you are doing that, something that you're going to have to keep in mind is how you deal with versioning.

So one of the biggest questions we get is: How many versions should apps deploy against?

And our advice to you is going to be, target the two most major releases of iOS.

So starting this fall when iOS 9 comes out and going forward, that's going to mean iOS 8 and 9.

This technique will give you the best mix of getting a whole lot of users while not taking on the maintenance burden of deploying back several iOS releases and having to deal with that.

Now, in this process, you might find yourself at times needing specific logic to check the version of what system you're on.

And another best practice for that is going to be to make sure that you include fallbacks for your logic that is based on system version.

So that means definitely don't write code that looks like this, where you check for a specific version like iOS 9 before doing something.

If you make a check like this, it is almost certainly going to cause a bug in your program when iOS 9.1, for example, is released that causes this check to fail.

Instead, you want to think about anything that's in iOS 9 is going to be in future releases, so any logic that you put in iOS 9 you'd want for versions greater than or equal to iOS 9.

And even better, if you are writing your app in Swift, you can take advantage of the new pound availability syntax to put all your version-specific code into a block that the compiler can understand and reason about and let you know if you are doing anything wrong for a particular version.

Whichever technique that you end up using, think through whether you need to have an Else clause because you don't want to make the mistake of putting some specific logic in that handles a core piece of your application for some system version but fails to do other work that it needs to do if it's not that version, and you get a bug on versions that aren't what you expected.

So that's some basic best practices for general application life cycle.

Let's talk about view and view controller best practices.

And the first idea I want to hit there is how we think about layout on modern devices.

You all know that last fall we introduced iPhone 6 and iPhone 6 Plus, and I am sure you are aware that along with this we had four new dimensions that had never been seen on iOS devices for your apps to lay out in.

When you add that to the dimensions of various iPhones that we already had plus iPad, now the matrix for possible dimensions that your app needs to lay out in, especially when you throw in split view on iPad Air 2, that matrix is fairly large.

So it no longer makes sense to build layouts that are built specific for a particular dimension that your view controller expects to be in.

Instead, layout in general wants to think of itself as being done to proportions, and we do that by specifically avoiding hard-coded values in the layout of our views and view controllers.

If we imagine a view that simply puts a label into a superview, we might have a couple of years ago thought of the layout of this as being done as a label described as 260 points wide with a 30-point margin from the left.

We don't want to do that because we want to think about the dimension scaling.

Either or both of them might scale.

In this case, if the width scales, this layout breaks, it just doesn't work because the offset no longer makes sense.

So if we had instead thought of this as a centered label, then that makes sense as the dimensions scale.

And we will revisit this example a bit when I talk about Auto Layout best practices.

I want to talk a little bit about an API that we introduced in iOS 8 to help you with this idea of laying out to proportions because part of the goal was to get rid of the idea of orientation.

You know, we no longer want you to ever think about orientation.

In fact, I am going to tell you if, when you are designing your app, you have the thought in your head that thinks about portrait or landscape or you have that conversation with your designer where the word "portrait" or "landscape" comes out, you are already thinking about it wrong.

We only think about things in terms of size.

And so size classes are here to help us think about things in terms of size, do proportional layout, while also recognizing and embracing that there are certain size thresholds where the fundamental UI we have changes.

As an example, Settings on iPhone 4S is a simple one-column table view.

When we go to the iPhone 5, it's still a table view.

It's just a little taller.

On iPhone 6, it's taller and wider, still basically a table view.

iPhone 6 Plus, bigger still.

However, when we transition to iPad, we cross a certain width threshold where now this view changes fundamentally how it appears.

It's now two columns of scrolling content.

So we've crossed some threshold there.

And in fact, you find that that same threshold has been crossed when we view in iPhone 6, and I will use the dirty word, landscape mode.

And size classes are the API that's there.

For Apple to communicate to your app where those fundamental thresholds are crossed so that you can then react to those thresholds and think about having a fundamentally changed UI according to those thresholds.

And you get notified of those thresholds changing as size classes are packaged in UITraitCollection objects, which your view controller will have access to.

So that's layout.

The next best practice I want to impart upon you is to use properties in your classes as an alternative to tags on UIView.

So what I mean here is if you are using View With Tag or Set Tag UIView API and shipping code, I am going to encourage you to move away from that.

Reasons are this is just [Applause]

Thank you.

I am really, really glad that somebody is happy about that.

Yes. So I mean, the reasons for this should be obvious.

It's just an integer, and it has collisions potentially with other code.

Maybe it's other code that you write.

Maybe it's the new guy on your team who doesn't know about your carefully managed integers.

Maybe it's a framework that you use that you have no visibility into.

And whenever these collisions happen, you get no compiler warnings about them.

The compiler has no way to reason about your integer management.

And when you not only do not get a compiler warning, but any runtime errors you get will not say anything about your use of View With Tags.

At best, you'll get a crash for unrecognized selector.

You won't know what happened.

As a replacement to this, declare properties on your classes, and then you will have real connections to those views that you need later.

As a simple code example, imagine that I wrote some code that creates an image view, and I keep track of it with a tag of 1,000 because I'm sure in all my cleverness nobody else will ever use a tag of 1,000.

But then I watch my own talk, and I say no, no, let me create an actual property that declares it a UI image view.

Then I keep a real reference to that view that also has better type information because View With Tag only is a type UI view.

Now that I use a property-typed UI image view, the compiler can actually reason about what I do and help me out if I make mistakes.

So please heed that.

The last best practice for view and view controllers is about making timing deterministic.

This is, for those of you who may have been in the position of doing something alongside a system animation or you have some work that you want to fire off when an animation is complete, and so you are left in the position of trying to make a guess about how long that animation is going to take and perhaps implementing an NSTimer to take care of that time for you.

Well, you don't want to do that because that introduces indeterminism into your app, especially with the possibility that animation timings can change from release to release.

You are really the opposite of future-proof if that's what you are doing.

Instead, leverage UIViewTransitionCoordinator, an API on UIViewController, to know what the timings are for the animations that you have.

This has the capability to let you do any animation you want alongside of view controller transition.

You know for sure when that transition is completed.

And it has built-in support for cancelable animations and interactive animations.

So if you imagine that navigation swipe gesture again, the user may move his or her hand back and forth, changing the speed, direction, and even decide not to pop the view controller and cancel it altogether.

If you use the Transition Coordinator, you are prepared to handle all of that.

Let's talk about Auto Layout best practices.

Auto Layout is a tool that I am sure many of you know and love, and it's kind of built there designed to help you be adaptable and future-proof in your code.

And of course, we are going to talk about that.

Future-proofing is one of the goals with Auto Layout.

But first I want to hit on some best practices for high performance in your Auto Layout.

And that's going to start with managing your constraints in the most efficient way possible.

So the way that you do that is imagining all the constraints that will be in your view and identify those constraints that might change throughout the lifetime of the view.

What that does when you identify what changes is you'll be able to make targeted changes and not change the things that don't need to change because when you keep some things constant, you allow the Auto Layout engine to optimize for those things that don't change and, therefore, it doesn't have to make certain calculations over again, and your app lays out faster, which is especially important if you are doing re-layout during scrolling or something else user interactive.

So part of that, a definite best practice while this is a worst practice and the best practice is to avoid the worst practice is removing all the constraints from a view.

This is bad not only in terms of the performance of your app enforcing the Auto Layout engine to do the most work possible, but it's also actually a potential compatibility issue because future versions of iOS may have additional constraints that the framework has added, which you'll be removing when you call Remove All Constraints.

So you want to avoid calling Remove All Constraints on a view as much as possible.

So the way that you sort of tie this together and are able to manage your constraints efficiently is by having explicit references to them using the same strategy that we just talked about by replacing view tags, having actual properties that point out the views or the constraints that you might need to change throughout the lifetime of the view.

So we can look at a very simple example of how you might write your update view constraints code, and this does the most naive thing possible, and that is it says hey, I need to update my constraints.

Let me just remove all of them, and then I will recalculate them and add them back.

We don't want to do this.

This is not the best practice.

The best practice is if we have one constraint, for example, that needs to be changed, we can remove that constraint, rebuild just that constraint, and add it back.

The Auto Layout engine again knows what didn't change and is able to optimize for us around that.

So the next set of best practices I have for you around constraints are around this idea of how specific you are when you describe your constraints.

In general, you want your constraints to describe your layout exactly as precisely as is necessary.

That is, you want to say what is needed to get the layout you desire, and you don't want to say any more, and you don't want to say any less.

And there are potential problems that can happen on both sides of this specificity problem, and I am going to talk about each of these now.

The first one is a performance problem.

So the first one is about adding duplicate constraints to your views.

Duplicate constraints are those ones that if you removed them, the layout would be exactly the same because they're just implied by what's already there.

And when you have that, it causes the layout engine to do more work than it needs to because it's solving for these constraints because they are there, but it didn't actually need to solve for them.

An example of this can be seen in this sort of simple layout.

I've got a couple of views inside a superview, and I might describe the layout first by doing the vertical axis and say, hey, there's some margin between my top and bottom view, and I give it an alignment option to say that the left edges of both of those views are also aligned.

Then I say, okay, let me go to the horizontal dimension.

I provide some spacing for the top view, so now I know what its left margin is.

Then I think, well, I've got to specify that bottom view as well, so I specify a margin for that bottom view.

But what I've just done is provided a margin that I really didn't need in that bottom view left margin.

Since I already knew what the top view's margin was and I also knew that the left edges of the bottom and the top view were going to align to each other, this view hierarchy would have laid out exactly the same if I hadn't specified the left margin of the bottom view.

So that extra constraint just causes the engine to do work that it doesn't need to do.

Get rid of that, we'll be faster.

The next problem that can happen as a result of overspecifying your constraints actually isn't a performance problem, but it's an adaptability problem.

It's a future problem for your app.

And that's when your constraints simply aren't flexible enough.

So if we think about hard-coded values, we know we hate them, and let's go back to this example that I promised we'd come back to about a label in a view.

Again, if we think about this in terms of it's a 30-point margin from the left and it's 260 points wide, we might describe its constraints in terms of those hard values.

But those hard values cause us to be rigid and unchanging, which kind of defeats the entire purpose of Auto Layout as a future-proofing tool.

What we really want to do is describe our constraints using the bounds of the views that they're in.

So this should have been something that used the bounds of the superview and described minimum margins around that view.

So let's talk about the other side of constraint specificity, which is underspecifying your constraints.

You don't want to do that either.

You want to make sure you've specified everything you need.

If you think about this view here and imagine what happens if we underspecified, we'd be introducing ambiguity.

So focus in on constraints I might have specified here.

If I set left and right margins around this label and I set a top margin as well, there's something missing, and it should be pretty obvious to you that there's no bottom margin, and there needs to be.

And there needs to be because if it's ambiguous, it's undefined, and that means my view might come out different ways different times I run my app.

Maybe if this is a table cell, it changes when I call Reload Data, and I am mystified by that.

Maybe it will change on the next version of iOS because, you know, the cosmic rays hit the phone differently.

Who knows.

We don't want undefined behavior.

We don't want our view to come out looking like this.

We want it to be the height that we want it to be.

So make sure you fully specify your constraints.

I want to give you a best practice in terms of testing and debugging your Auto Layout code.

You can use a method on UIView called Has Ambiguous Layout.

If you are in a debugger, you are trying to figure out why your view isn't laying out the way that you expect it to, call Has Ambiguous Layout.

It will let you know if there's ambiguity in your view.

Moreover, you call this method on a UIWindow, it will tell you if any view in the window tree has ambiguous layout.

So that's pretty handy.

You can call UIView Auto Layout Trace, then, to get a picture of all the constraints throughout your entire view tree and use those constraints to go find ambiguity.

A really interesting best practice is to take these methods as they are, put them into a unit test.

You can imagine for each view tree, each basic UI in your app, you could call UIWindow Has Ambiguous Layout, and if it does have ambiguous layout, then you could call UIView Auto Layout Trace to find where the ambiguous constraints are.

Just package that up into a report, and then you have a test which both lets you know when there's ambiguity and provides debugging information for whoever comes along and sees that there is a failure in the test.

So that's a great best practice you can use with your Auto Layout apps.

[Applause]

All right.

So I will transition now to our last topic for best practices, and that's table and collection views.

I know that this is something that is important to almost every iOS app out there, and it's certainly important to me, too.

So the first best practice is use self-sizing cells when you have content that needs to change or you have cells that need to change size based on the content.

I am sure most all of you have at some point in your iOS development life been in this situation where you have a basic table view with some content in there, and you realize, oh, each cell needs to be a different height based on the content that's in it.

I can't just have one height for every cell.

And self-sizing cells introduced in iOS 8 make it easier than ever to transition to what you really want, which is a table view where all the cells are the height they need for the content.

So I'll run through the best practice mechanism for how you get self-sizing cells in your app.

And it starts just like we talked about in the Auto Layout section by fully specifying your constraints.

You want to use all those tips that I just talked about, thinking about this idea of your Auto Layout system is this machine that's taking width in as an input because the table view has a fixed width, and so your cell is going to be that wide.

And then it's producing as an output the height of the cell.

So any ambiguity in there, if you haven't fully specified your constraints, comes out as the height isn't what you want it to be.

If we use the simple example of a table view cell, here it's really easy.

We can just put margins around all of our content, which in this case is just a label, and it has an intrinsic content size.

So when we put margins around it, we fully specified the constraints of this particular cell, and we'll get the size that we want.

You, however, might have some more complex cells than this.

I understand that this is an easy case.

And if you are in the position where you find, you know, hey, I've specified all my constraints, but I am not getting the height that I thought I should get, I want to give you a tip, which is try adding a constraint to your content view that specifies the height of the content view.

So you are using, in fact, a height constraint on the content view.

Then you can specify that in terms of your content.

Here I can say hey, content view height should be equal to the height of the label plus my top and bottom margins.

In this case, that's repeating work.

I don't really need to do that here, and I'll get the same thing.

But if in your app you are not getting what you expect and you add a height constraint to your view, and then that causes the height of the cell to change, then that's a great indication that your constraints aren't giving you quite the logic that you expected them to.

So that's a great tool that you can use to figure that out.

Now, once you have that, you might want to think about how you animate the height changes of your cells, you know, even using self-sizing cells and Auto Layout.

Now, you can imagine if you had some cell in here that you want to change its content, you might take the very naive approach and update your model and then call Reload Data.

And if you do that, it's going to look like this, where it snaps to the new position of the table, and it gets the job done, but it just isn't the user experience that you wanted out of your app.

It doesn't quite look as polished as it should be.

What you wanted was to have that cell animate its height and the cells around it animate their positions smoothly so everything dropped into place and the user understood what was happening.

So let's walk through how you do that.

Thankfully, it's pretty simple.

Whenever you want to specify a geometry change is to be animated in table view, you use a begin update and update block with the Table View API.

So first step is to call Table View Begin Updates.

This is true whether you are using self-sizing cells or not.

This is the general way you animate geometry changes in table view.

Then you update your model.

That's easy.

Third step is if you're changing the height of an onscreen cell, you can just reach into that cell, get a reference by calling Table View Cell For Row At Index Path, and change the contents of that cell, even changing the constraints as needed.

Sometimes people think they need to call Reload Rows Of Index Path.

You don't actually need to do that, and it won't get you quite the optimal experience.

You actually can just reach into the cell and change its contents.

Then when you are done with that, you say Table View End Updates, and table view at that time recalculates the geometry of all the rows, including asking all the onscreen rows for their Auto Layout information to get their height, and everything animates into place as you saw.

So begin updates, end updates is the key to that.

So the last best practice that I want to give you is how to implement custom collection view layouts that invalidate themselves and are very fast as they do it.

So I know this comes up a lot, people write custom collection view layouts, and they're doing something, they are changing something about themselves as the user is scrolling, and they have a hard time keeping up with that layout.

Well, I am going to tell you exactly how the Photos app in iOS does this job so that you can take that technique and put it into your custom layout that you have.

So the Photos layout has this header, which is expressed as a supplementary view in collection view terms, and when the user scrolls, even though the cells move with the scrolling, that header view stays in place on screen.

This is the same basic idea that I know many people want to implement in their collection views, and the Photos layout is able to do this by using a UICollectionView invalidation context instance.

This is API that you can find in UICollectionView.

So the steps to this are just a few.

Number one is the most obvious.

The Photos layout is invalidated on every bounds change.

So every frame as the user is scrolling, the Photos layout gets invalidated.

Piece of cake.

That's the easy part.

The question is, how do we make that fast?

And the answer is, the Photos layout builds a targeted invalidation context that is specified to invalidate just that header view so that the collection view is able to optimize, understanding that the only view that is being invalidated is the header view and none of the cells are.

That allows the collection view to do the entire operation as fast as possible, and it's so fast that this can just be repeated as necessary at frame rate scrolling as fast, even though the layout is being invalidated on every frame.

So you can use that same technique in any layout if, in general, if you have performance concerns using a custom layout.

Generally speaking, UICollectionView invalidation context is the key to overcoming those performance concerns.

So I encourage you to check out that API.

Okay. So I've talked about a whole collection of best practices here.

We talked about performance and how to make your apps superresponsive at launch and throughout its life cycle and how to make your Auto Layout as fast as possible.

We've discussed user experience as one of your great goals and animating your table views around and laying out properly across the myriad of iOS devices.

And of course, I've given you tips for how to write your code in the most future-proof way so that it's running on versions of iOS for generations to come.

Now, I encourage you to use this entire talk as a reference, something you can come back, watch the video as you are building your future apps.

There's a lot of best practices here that you can use this as a launching board to then go into the documentation, look up the specific APIs that I've referenced, and you will be able to put that to well best practice in all of your future apps.

So you know, with that, I thank you, and I am glad that you have come here to WWDC, and I hope you have a best rest of your afternoon possible.

[Applause]

Apple, Inc. AAPL
1 Infinite Loop Cupertino CA 95014 US