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.
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
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 --
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.
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.
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.
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
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.
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.