Transcript
[ Music ]
[ Applause ]
>> Good afternoon
everyone and welcome
to Architecting Performance
on watchOS 3.
My name is Tyler McAtee
and shortly you'll be meeting
my colleague, Todd Grooms.
Today we'll be discussing the
way we at Apple have thought
about performance and where that
took us when building watchOS 3.
We'll start by talking about
2-Second tasks, what that is,
how it helped influence
the design of watchOS 3,
and what that means for
your app's architecture.
I'll then talk a bit about how
design strategies influenced
performance and, showcase
a new detail paging API
that will help reduce
unnecessary navigation time.
that will help reduce
unnecessary navigation time.
Finally, Todd will come on stage
and show how we've taken
these ideas and applied
to them the stock's
watch application.
So let's start with
2-Second tasks.
We've focused on this idea
as a good rule of thumb
for what an interaction with the
Apple Watch should feel like.
So what is a 2-Second task?
A 2-Second task is something
the user wants to accomplish
or learn by looking
at their Apple Watch.
These tasks should only
take a couple seconds.
And these seconds should be
measured from the very beginning
of the interaction
until the very end,
from the moment the user
raises their wrist to look
at their Apple Watch, to the
point where they've lowered it.
Some examples of a 2-Second task
may be, checking a notification,
setting a timer or
starting a workout.
Today I'm going to
walk through some
of the key changes
we've made to the system
and explain how these
will affect the way you
as a developer should
think about performance
in your WatchKit application.
Now, one of the first
bottlenecks
in accomplishing a task on
the Apple Watch is the amount
of navigation it can take to get
to the appropriate application.
of navigation it can take to get
to the appropriate application.
The quickest way to
launch an application
on the Apple Watch is by
tapping a complication.
We only encourage developers
to implement a complication
if they had relevant
data to display.
A lot of our apps,
such as Messages,
Mail and Phone had
no complication.
In order to increase
navigatability
on the Apple Watch as well as
present users with more options
to customize, now
on watchOS 3 all
of our applications
have complications.
These launcher complications
are useful for quick access
to your very favorite
applications right
from the watch face.
We encourage you to
adopt this policy
for your application as well.
Implement a complication whether
or not you have data to display.
Additionally, new in watchOS
3 we've brought you the dock.
Just by pressing the side button
users will be able to bring
up their dock and
quickly look through all
of their favorites applications.
Navigating to and from these
applications is extremely quick
and easy.
Now we want our users
to be able to go
to these favorite applications
and have them already ready
and loaded, instead of having to
wait for an activity indicator
as the application
is brought up.
as the application
is brought up.
In order to address this, in
watchOS 3 all the applications
that a user chooses to
put on their watch face
or in their dock will be
kept alive and suspended
in memory by the system.
That way when they go to
interact with the applications,
they only have to wait for
resume, instead of a launch.
But the system still has
a fixed amount of memory,
and as an application
in the system,
you'll need to be
a good citizen.
Because there can be up
to 10 dock applications,
up to 5 complications, as well
as the system application,
processes and more.
You have to remember
that you're just one part
of a large ecosystem, so
you have to only use as much
as you absolutely need.
Now the system, because of
the nature of this ecosystem,
our system does impose a fixed
ceiling on the amount of memory
that a WatchKit application
can use.
If you exceed this limit,
our system will terminate you
abruptly with no chance to tear
down so that the memory can be
reclaimed for other processes.
This limit isn't a goal, and you
shouldn't feel the need to use
up all this memory
and realistically,
it should be nowhere
near the limit.
The current limit, as of watchOS
3 is 30 megabytes per WatchKit
The current limit, as of watchOS
3 is 30 megabytes per WatchKit
application, but this
may change in the future.
So what are some good tips for
keeping your memory usage down?
Use appropriately sized
images for the watch screen,
not only does this
keep down memory usage
but will help increase
overall performance,
because the watch won't have
to do the extra work
to resize the images.
Use appropriately sized data
sets, don't download a giant set
of data if you're only
displaying a few records
on screen.
And if you're only displaying
one property of a data object,
don't download or keep
around all the other
properties as well.
If you have control over
the API you're using
to download the data
it may make sense
to build separate end points
for the phone and for the watch
since the watch will probably
display a more condensed version
of the information.
This will help save on the
amount of network traffic
that your watch has to
process as well as the amount
of transient data and memory.
Finally, it's important
to release objects
you're no longer using.
Take the time to go
through your code
and make sure you're
only keeping
around things you
absolutely need.
Now, because the applications
that a user chooses to put
Now, because the applications
that a user chooses to put
on their watch face and
in the dock are kept alive
and suspended in
memory by the system,
they will be resumed much more
often than they're launched.
Because of this, for watchOS 3
the key path we want to focus
on optimizing is resume time.
Now apps won't only be
resumed more often just
because they're kept
alive in memory,
but also because
they're in the dock.
When the user scrolls over to
your application in the dock,
the application will be resumed.
When the user scrolls away, the
application will be suspended.
This behavior of resuming and
suspending often is now typical
for applications in watchOS 3.
So it's important to understand
which lifecycle methods
are a good place to do work
and which lifecycle methods
are not a good place.
So let's talk about the
different lifecycle methods
that the WatchKit
extension delegate will see.
ApplicationDidFinishLaunching
is the first method
that your delegate will see.
This gets called when the
application is first launched
and is a good place to perform
any final initialization
of your application
as well as any tasks
that only need to
be performed once.
The second method that
your delegate will see
is applicationDidBecomeActive.
This gets called whenever your
application becomes the active
application on the
platform, restart any tasks
that were previously paused or
not yet started, and if needed,
refresh the user interface.
Once the application
goes from the active
to the inactive state
you will get the
applicationWillResignActive
call.
This can occur for certain
types of temporary interruptions
such as an incoming phone
call or a notification
when the user presses the side
button to bring up the dock,
or when the user
exits your application
and it starts its transition
to the background state.
When your application is no
longer active and it starts
to go to the background
you'll get the
applicationDidEnterBackground
call.
And when your application
returns
to the foreground you'll get the
applicationWillEnterForeground
call.
These methods are only
called when you're going
from background to foreground or
from foreground to background,
so it won't be called
on first launch.
In addition, there are
lifecycle methods associated
with the interface controller.
AwakeWithContext gets called
when your interface
controller's first instantiated.
when your interface
controller's first instantiated.
This is a good place to do work
that only needs to be done once.
willActivate is called when
the interface is active
and able to be updated.
It can be called before the
interface is actually visible
to the user.
Once the interface is
fully visible to the user,
you'll get the didAppear method.
If you have work
to do on resume,
these methods are the
good place to do it.
If the work is heavy
weight it may make sense
to dispatch the work out
to a background queue,
so that these methods
can complete
and your app can
finish resuming.
Once you're application's
getting suspended,
you'll get the willDisappear
call first
on your interface controller
when the user interface is
about to be no longer
visible to the user.
Once the user interface
is deactivated
and no longer be updated you'll
get the didDeactivate call.
These methods are a good place
to cancel any heavy weight tasks
that you may have started in
willActivate and didAppear.
It's important to understand
this lifecycle and understand
that these methods can get
called repeatedly and often.
I'd like to now walk
through an example
I'd like to now walk
through an example
of how an application might
see these events during
its lifetime.
We'll start with an application
that, for the purposes
of this talk is not
running or backgrounded.
When the user taps
your application,
the first methods will go to
the WatchKit extension delegate,
didFinishLaunching,
and didBecomeActive.
The interface controller will
receive its awakeWithContext
willActivate and didAppear.
Now your application is running
foregrounded, and active.
But what happens when the
user presses the side button
to bring up the dock?
At this moment your application
is no longer the active
application on the platform,
that's the system application.
So your delegate will get the
applicationWillResignActive
call.
While the user's still settled
on your application however,
you'll still foreground
it in running,
you're getting CPU time,
your updating your user
interface, all that.
As soon as the user scrolls
away from your application,
the system will suspend
your application.
So your interface controller
will get the willDisappear
and didDeactivate
and you'll get your
application didEnterBackground.
Now here your application has
just entered a background state
Now here your application has
just entered a background state
so the system might
wake up your application
for a background snapshot task.
To learn more about
these snapshot tasks,
check out the talk
we gave this morning,
Keeping Your Watch
Apps Up to Date.
Your interface controller gets
woken up with willActivate
and didAppear, before your
delegates given the opportunity
to handleBackgroundTasks.
And then your interface
controller gets the
willDisappear and didDeactivate.
Now your application
is fully suspended
and it's handled its
background tasks.
Once the user swipes
back to your application,
you'll get your
applicationWillEnterForeground,
and your willActivate
and didAppear.
Your application is
once again running
and foregrounded in the dock.
It's no until the user taps
into your application though
that you become the active
application on the platform
and get
applicationDidBecomeActive.
Now a lot has happened just
from the user entering the dock,
swiping away from your
application, and swiping back.
That's why it's important to
be cognizant of this lifecycle
and understand that as a
user browses their dock your
application may be seeing these
events repeatedly and often.
application may be seeing these
events repeatedly and often.
So what are some other tips
for reducing resume time?
You should use discretion when
updating WKInterface objects.
Every time you set a property
on WKInterface object the system
creates a message to send,
packs it up, and dispatches
it to the app process
where the UI is updated.
It may be tempting to build some
method that based on the state
of your application updates
your UI and then call
that every time you resume.
But setting each property
comes with a cost.
Even if the property isn't
changing this results
in unnecessary traffic between
the app and the extension.
It's worth the effort to
only set these properties
if they're changing, so
you absolutely need to.
You should also not
that WKInterfaceTable does not
behave the same as UITableView.
The phone has a lot more memory
for storing a lot
more information
and UITableView is just
optimized to quickly scroll
through these larger data set.
The cells are created on demand,
and are reused as you scroll.
With WKInterfaceTable however,
all the cells are created
upfront and there's no reuse.
So the amount of work
that your watch has
So the amount of work
that your watch has
to do scales linearly
with the table size.
Because of that it's important
to keep WKInterfaceTable
size down.
The watch is not the appropriate
form factor to scroll
through hundreds of records and
in fact we found that it's best
to keep WKInterfaceTable
size to maybe just over 20.
You should avoid reloading
a WKInterfaceTable whenever
possible as well.
This is an expensive operation.
If it may be tempting to reload
your entire table on resume
or when your data set
changes, but if you need to add
and remove rows, it's better
to use the insertion
and deletion APIs.
I'd now like to talk
a bit about design.
Thinking about the right
information to display
on the watch form factor
as well as the best way
to display it can
greatly help performance.
In watchOS 3you should
design your applications
to be glanceable.
The dock lets users quickly look
through their favorite
applications.
So your application may
only be seen on screen
for a brief moment in
time as the user swipes
for a brief moment in
time as the user swipes
from one application
to the other.
So focus on showing only the
most essential information
and display it as
clearly as possible.
Part of making your application
more glanceable is designing it
with a focused purpose.
The watch is not the appropriate
form factor for scrolling
through large amounts
of content,
or looking at complex
data hierarchies.
By only showing the most
essential information,
you tend to get better
performance as a byproduct.
Since you're displaying less
data, you save on memory
and processing and need
fewer network calls
to stay up to date.
Lastly, it's important
to consider navigation.
I've talked a lot about how
we've improved navigation
on a system level, but
it's equally important
to consider navigation on an
application level as well.
In order to help with this
we're introducing a new detailed
pageing API.
A standard setup for a WatchKit
application is the hierarchal
data view where you have a
table of cells, and tapping one
of the cells drills into
detail about that item.
The problem with this
setup though is if you want
to see the detail
about a couple items,
to see the detail
about a couple items,
you end up tapping
back and forth a lot.
In order to solve this, our new
detail paging API lets users
quickly scroll from
detail view to detail view,
just panning along the screen
or rotating the digital crown.
To learn more about how to set
up this API in your code as well
as learn about other quick
interaction techniques we've
released, for developers,
check out the Quick
Interaction Techniques
for watchOS talk
we gave yesterday.
But in this talk I'd like
to talk a little bit more
about the lifecycle that
view controllers will go
through as part of this API.
Because it's important from
a performance point of view.
So here we have our table with
3 cells, red, orange and yellow.
The detail paging API works
on segue from inner tables
to interface controllers.
So when you tap one of the cells
we're going to trigger a segue.
When you tap the cell,
your master interface
controller is going
to get the method
contextForSegue
withidentifierinTable row index.
This is where you're going to
build up the context object
that gets passed to your
detail view controller
and it's awakeWithContext
method.
Your master view controller
will not only receive its call
Your master view controller
will not only receive its call
for the cell you
tapped, but each
and every cell in the table.
We prepare the context for every
detail view controller right
away so that when we
prepare the context for them
so that we can instantiate
them upfront.
That way when the user, goes
to their first one
they can quickly scroll
through all of them.
Your first controller
will be the,
first one to get its
awakeWithContext called on it
as well as its willActivate
and didAppear.
However, this is where
behavior is interesting
for the scroll view.
We'll preheat the
controllers close
to the selected detail
view controller,
so that the users can
scroll to the next one.
So the other colors are going
to get their lifecycle
methods called on them as well.
They're going to first get
their awakeWithContext,
and then their willActivate
and didDeactivate.
It's important to be smart
about setting up work
on these off screen
view controllers.
Don't start long CPU intensive
tasks on all of them blindly.
Because this may cause a lot
of work to spin up on the CPU
Because this may cause a lot
of work to spin up on the CPU
if you have a lot
of table cells.
Now as the user scrolls
from one detail view
to the other your previous
interface controller will be the
first to get its willDisappear
call, willActivate,
didDeactivate, and didAppear.
This keeps your interface
controllers
in a consistent state.
Those that are on screen
most recently have got their
didAppear call and those
that are off screen
most recently got their
didDeactivate call.
That way when you tap back to go
to the master interface
controller,
only one interface controller
needs lifecycle methods called
on it, the one that's visible.
It'll get its willDisappear,
and didDeactivate.
Alright, I'd like to
invite up Todd to talk
about how we've applied
these ideas
to our Stocks WatchKit
application.
Thank you.
[ Applause ]
>> Good afternoon.
I'm a watch OS engineer,
and we're presenting Stocks
I'm a watch OS engineer,
and we're presenting Stocks
as a case study to
WatchKit and developers.
So many of you may
not know this,
but Stocks is a watch
app built with WatchKit.
At Apple we wanted to
have firsthand experience
with WatchKit development,
and we felt that Stocks
would be a great use case
for WatchKit development.
I have three topics that I
would like to talk about today
in regard to Stocks and
WatchKit development.
I'm going to identify our
2-Second tasks for Stocks,
then I'm going to discuss some
of the implementation details
behind our background refresh
use cases.
Finally, I will talk a bit about
the optimizations we have made
to help with our resume time and
by extension, our launch time.
So, we'll begin with
our 2-Second tasks.
When we thought of Stocks,
we thought of three
important 2-Second tasks,
the first is you
most likely want
to view how a favorite
stock's current price is doing
to view how a favorite
stock's current price is doing
right now.
This can of course
be accomplished
with a complication.
But with the dock, we're able
to get a little bit more detail
with that 2-Second task.
In particular, we felt
that another important 2-Second
task would be seeing your
favorite stock's current
performance throughout the day
in a chart.
Lastly, we felt that it
would be important for you
to see the current
price for a few stocks.
So we'll start with
the complication.
Now of course the
complication is the fastest way
to see data on your watch.
That data is always present and
it's there every time you go
to look at the time
on your watch.
The important piece in that, in
watchOS 3 is that data is kept
in sync between the
complication and the app.
Now for more information on
that, I would encourage you
to check out the Keeping Your
Watch App Up to Date session
that occurred this morning.
So now we'll go and
talk about how some
of the other 2-Second tasks
were performed in watchOS 2.
So in watchOS 2, you
would launch Stocks
and you could see the
current price of the stock
that you were interested in or
the other stocks right away.
But if you wanted to see how
that stock had been
performing throughout the day,
you would need to
tap on a stock,
and now you're presented
with this view.
It's a little bit
more information,
but it still doesn't really
answer the question on how
that stock price had been
performing through the day.
So if you wanted to see that,
you would have to scroll
down a little bit, and
now you're on the chart.
We had four options, for the
chart, we have the day interval,
the one week, the one
month, and the six month.
So odds are the first
time that you scroll
down there you're probably
not even seeing the interval
that you care about which is
probably the one day interval.
So that would require you
tapping on those very,
very small buttons and
opening that chart.
And then after that, you would
have all this other metadata
And then after that, you would
have all this other metadata
down below that a lot of the
time isn't really necessary
for when you're glancing at
information throughout the day.
And of course if you wanted
to view multiple stocks
and how they're performing
throughout the day,
you would have to navigate
back, tap into the new one,
much like Tyler should you
in this animation earlier.
So let's look at watchOS 3.
Now here's the new watchOS
3 design, as you can see,
first of all, still a list
view that you come into.
But the font is much larger,
much more legible, a little bit
of a simplified interface.
To me it pops and it's easily
readable at small sizes
like you would see in the dock.
So if you wanted to see
how Apple was doing,
today and how the performance
was going you would tap
on Apple, again, but now you
see the chart right there.
And we just assume
that you always want
to see the one day chart.
There are instances of course,
where there isn't a day chart,
much like index funds
won't have a day chart.
But we can fall back to the
one month chart when we come
But we can fall back to the
one month chart when we come
across those, and that's
the more relevant interval
that you would like
to see at a glance.
We also got rid of some of
the more minute detail below.
Now this gives us
two advantages.
One, it eliminates a
network request, which speeds
up our loading performance.
And two, it allows us to adopt
the new vertical detail pageing
API, so then that
way you can scroll
through multiple Stocks either
with a turn of the digital crown
or a swipe of your finger.
And of course, if you
want to view the details
of a stock's performance you
know like more minute details
that we had before such as the
52 week high or the 52 week low.
You can view that using
Handoff, so with Handoff,
you're able to setup a
context activity and then hand
that off to your iPhone.
So we feel that the watch is
the place for glanceable data,
and that the iPhone
is the place for,
you know like a view
that's data rich
or a little bit too convoluted.
So the good thing about the
new design, as I mentioned,
it's very readable in the
dock and with the dock,
we decided to reevaluate what
we would show there for Stocks.
So if you attended some of the
other sessions you're aware
that there is a concept of a
default state, and a snapshot.
So we took this to mean that
it should be a sticky view.
And what I mean by sticky is
that when you leave Stocks,
if you're looking at the
stock list, when you return
to Stocks either in the dock or
by going into the application,
you will see the stock list.
And this is also the
view that we'll keep
up to date throughout the day.
However, if you were to tap
into the details of a stock
and returned to look at
the, either the dock or go
into the app, then you're
going to see the detail view.
Now there's one caveat
with this,
so on Stocks you can set
your complication stock
and that's the stock
that you view,
of course on your complication.
So we took that to mean
that that's most likely
your favorite stock.
So once you set that,
that's the detail view
that we try to return you to.
So if you open up Stocks and
you say navigated from Apple
to the Facebook stock, and you
resumed back to the home screen,
in about an hour, when we get
the return to default state flag
for our snapshot, we will
actually take you back
to the Apple stock.
Because we take that to mean
that you had that selected
as your complication
stock and that
that would be your favorite
stock, and that's the one
that we want to return you to.
So we want to make a
predictable experience
and always return the user to
something that they would expect
to see after a certain
amount of time.
So let's recap what we've done
in our 2-Second tasks
for Stocks.
The first thing, we made sure
that we had consistent data
between our complication
and app.
The next, we simplified
our design,
we made it a lot more
legible at smaller sizes,
and much more usable
whenever you vertically scroll
and much more usable
whenever you vertically scroll
through the detail pageing API.
And that lets you look at
multiple stocks, quickly instead
of having to do the
back and forth shuffle.
So next, we'll talk a little
bit about background refresh,
and I would like to
talk a little bit more
about how we implemented
background refresh in Stocks.
So when we started implementing
background refresh in Stocks,
we came up with two questions.
One, how often do we need
to update our information
in Stocks?
And two, what data
do we need to fetch
to keep our app up to date?
So determining how often
we should refresh our data
in Stocks was a little bit
of a tricky proposition.
At first we felt that updating
our data every 15 minutes was a
pretty good start.
This would leave us updating our
app many times throughout the
day, however.
And many of those updates could
occur when it's unhelpful,
And many of those updates could
occur when it's unhelpful,
like when the stock
market is closed at the end
of the day or over the weekend.
So let's take some
facts that we know
because we felt we
could be a bit smarter
in how we implemented this.
First, markets are
open for a period
of time throughout the day.
So for an example, let's
say we're following a stock
on the New York Stock Exchange,
and we know that the New
York Stock Exchange opens
at 9:30 a.m. Eastern and it
closes at 4 p.m. Eastern.
So if we limit our
background refresh request to,
basically when the market is
open, then we're able to cut
down our number of updates,
and it can sort of budget
for other applications.
And it's also going
to give us the benefit
of not updating our complication
and our application in times
when it would be ineffective.
So that's also nice as well.
So let's look at a little pseudo
code on how we would do that
So let's look at a little pseudo
code on how we would do that
and how would we decide
when the next refresh date
for Stocks should be.
First, we're going to enumerate
through their list of stocks,
then we're going to check and
see if the markets are like,
basically if the
markets are all closed.
Because if we know,
if the markets are all closed we
want the earliest next open time
that we have in our stock list.
Otherwise, that means at
least one market is open,
so we should fall back to our
regular 15 minute cadence.
So we'll look at a
little bit of source here.
The first thing that I'll
call your attention to,
this is just a function
that we would have in Stocks
for scheduling our
background refresh time,
and it takes an optional
preferred date.
We use the
scheduleBackgroundRefresh
instance method in WKExtension,
and we're going to pass
in this preferredDate here.
Now that preferred date is
calculated elsewhere in the app,
but that's at least how
we schedule our background
refresh time.
So I'm kind of working
backwards from the end result.
So I'm kind of working
backwards from the end result.
So let's see what happens in
our next preferred refresh data.
That function has a guard early
on, and so basically we're going
to call our function
earliestNextOpenDateInStocks.
And if it returns nil, then
we're going to go ahead
and bail, because in
earliestNextOpenDateInStocks,
we would return nil
if you didn't have any
stocks in your list.
Because at that point
there's no use
in doing a background refresh
because there's no
data to refresh.
So now we'll go ahead
and we'll calculate the
nextRegularRefreshData,
so that's just our update
cadence, so every 15 minutes.
And then finally, we'll
do this check here.
So we take that
earliestNextOpenDateInStocks,
and we'll do a later
date comparison
against our regular
refresh cadence.
Now our
earliestNextOpenDateInStocks
also has the added benefit
of returning distant past,
if the market is currently
open for any of our stocks.
if the market is currently
open for any of our stocks.
So the later date would
always be the refresh cadence
in that scenario.
So let's look at that
earliestNextOpenDateInStocks
method.
First we're going to
grab our list of stocks
and then we're going to
do this guard check here.
And so if it's 0 again, we're
going to bail out, return nil,
there's no use in doing
background refreshes.
Then we're going to iterate
over our list of stocks.
If any of the markets, are open
then we're going to go ahead
and return the distantPast.
Otherwise we're going
to do this check here.
And we're just going to
basically iterate over the list
and find the
earliestNextOpenDate.
And so I mean, I just wanted
to show some of that code
because we feel that
that's a pretty good way
of limiting the number of times
that you're doing
background refresh,
with not a whole lot of code.
So let's talk about scheduling
multiple background requests.
Because in particularly with
Stocks, we have two end points
that we hit to keep our
application up to date.
So we have endpoint A, which
keeps the application data
up to date, and then
we have endpoint B,
for updating the complication.
So if we're going to schedule
our background refresh time,
we do that.
Once we receive the
handle background task,
we'll submit our
endpoint A request,
submit our endpoint B request
and we'll schedule our future
background refresh time.
So what does that look like?
Well, we have our handle
background tasks method
in our WKExtension delegate.
We're going to iterate over
those background tasks.
We're going to go ahead
and first check to see
if it's an application
refresh task.
And if it is, we're going
to go ahead and schedule
that data update request
and that's just going to be
where we actually
schedule our NSURL request.
The next we'll do,
we'll go ahead
and schedule our next
background refresh time,
using that handy dandy
nextPreferredRefreshDate.
using that handy dandy
nextPreferredRefreshDate.
And then we'll complete
our app refresh task.
The last part of this, I'll
call out to your attention is
that URL session
refresh background task.
Now you will get one of these
when you trigger a background
NSURL session request.
So it's our job here
to store that somewhere
where we can complete
it later whenever
that request is finished.
So now we've talked
about that let's talk
about what it actually looks
like when we schedule
those NS URL requests,
just at a high level.
So we're going to schedule
those requests, we're going to,
and then when those
requests are complete,
we're going to schedule
a snapshot,
reload the complication,
and we're going
to complete our refresh
background task.
So the first thing we'll
setup the app data request
and the complication
data request.
Then we're going to setup
our finish update handler.
Now the finish update handler is
just, for lack of a better term,
a block that I set so that
whenever the NS URL session
delegate method for finishing
the background request is
delegate method for finishing
the background request is
called, I can call that
finish update handler
and that'll call
what's in that block.
So then we have our
submitRequest
which is essentially just
taking the network request
and calling resume on the tasks.
Now once the task is complete,
we'll go ahead and grab
that task from our URL sessions
task which is just a dictionary.
We'll schedule our snapshot,
we'll reload our complication,
and we'll go ahead and
complete that URL session task.
And one last thing
that I'll call
out here is our
urlSessionDidFinishEvents just
to show you that whenever
our requests finish,
we just grab the identifier
from the session configuration,
and we call our
finishUpdateHandler.
And so that kind of
gives you an idea
of how you can run multiple
requests to keep your app
up to date if you have
separate requests for your app
and your complication.
So the first thing,
obviously you want
to optimize how often
you schedule your updates
to optimize how often
you schedule your updates
for your app when you're
doing background refreshes.
That's goal number one.
And if you're updating
with data from a server,
try to use a single
specialized endpoint
if you have control over that.
But if you don't, it is possible
to submit multiple requests
during a background refresh.
So now let's move onto
resume time optimizations.
So when you optimize your resume
time by extension you're going
to be optimizing
your launch time
as well, which is very nice.
So let's talk about
what we can do.
As Tyler I mentioned earlier,
we can minimize the work we're
doing during willActivate
and didAppear.
So you know to do that,
of course we avoid
long running tasks
that are triggered
from willActivate.
We'll do a smart loading
and reloading of our data,
and of course as he
mentioned before, we only want
to set properties on
our interface elements
that have actually changed.
So I'll start this off
with a cautionary tale,
and this involves implementing
the vertical detail paging API.
So as Tyler mentioned before,
neighboring detail pages
will have willActivate called
and you also want to
avoid expensive operations
in willActivate for
detail pages.
But in particular, there's one
very big expensive operation
in this view.
So it started with a
couple of bug reports
but essentially we got
reports of slow loading,
a slow loading chart for a stock
when you first entered
the detail page.
And other detail pages never
finish loading their charts,
or were extremely slow.
So we kind of looked at
the code, and tried to look
and see what was going on, so
this is a slimmed down version
of a stock interface controller.
But if you'll notice,
in willActivate we're calling
this downloadAndGenerateChart
which was, basically an NS
operation that was long running
and doing a lot of work to get
chart data and draw that chart.
So what can we do to
improve upon that?
Well, so we know that in
didAppear it gets called
when that interface
controller is actually visible
to the user and it has settled.
So how about we start
downloading and generating
that chart data there?
And then what happens
if you're scrolling
through those quite frequently?
We don't want to continue
downloading and generating
that chart data for a view
that you already left.
So we'll go ahead and we'll call
cancelDownloadAndGenerateChart,
which is just a method
that takes the operation
that's running and cancels it.
So, let's look at, again to
review some of these caveats,
because I have to learn
from my mistakes here.
We want to avoid triggering long
running tasks in willActivate.
And if possible, it's
great to make use
of cancellable operations,
so NS operation is a nice
template for doing that.
So we'll move onto the
WKInterfaceTable loading.
We know that all rows
are loaded in memory,
and we know that there's
a linear upfront cost
to the number of rows
you have in your table.
And, of course there's no reuse
as there is in UITableView.
So I'm going to show a graph and
this is some of the profiling
that I had done in Stocks.
And this for the initial
launch time, so after a reboot,
not resume time, any of that.
But it's kind of important to
note that when we had 0 stocks
in the list, so an
empty stock listed,
so just under 5 1/2
seconds to load.
If we added one stock it
jumped up a little bit,
to just under 6 1/2
seconds, and if add 5 stocks,
a little over 6 1/2 seconds.
And if we had 10
stocks, now it's starting
to creep up towards 7 seconds.
So if you have a
large number of rows
in your table you're just
basically delaying how quickly
that interface controller
can load.
So what can we do to improve
our loading time here?
Well first we can limit the
number of rows that we load.
And we can also try to do
smart updates of our table
when row deltas occur so
meaning, when the list mutates.
So let's look at our initial
approach of loadTable.
We'll go ahead and we'll grab
the stocks from our manager,
and then we're going to set the
number of rows on the table.
And then after that,
we'll populate each row
controller with a stock.
Now it seems pretty harmless at
first, what's happening there?
Well the number of stocks isn't
capped, so if you had 20 stocks,
it would be 20 rows, if you had
30, 30 and so on and so forth.
And we were always using
a set number of rows.
And if just one row is being
added when use that number
of rows you're essentially
wiping
out what you had there before
and starting over again.
So it's inefficient.
So let's look at
what we could do
to be a little smarter
this time.
So we grab the stocks like
we did before, we'll go ahead
and check the count, and
we're going to go ahead
and cap that at a max size.
So in Stocks' case, 20.
Then we're going to
calculate our row delta
to see what the difference
is, how much has it changed?
And then we're going to call
this insertRemoveTableRows,
which I'll get to in a second.
And then one last
button suspender approach
to make sure we're not doing
more work than we need to.
We'll go ahead and check to
make sure our index falls below
that max Stocks list size.
So let's look at that
insertRemoveTableRows.
So the first thing we're going
to do is calculate the row
change and then we're going
to check the stock row delta.
So if it's greater than 0,
we know we're inserting,
if it's less than 0, then we
know that we're going to remove.
And the important thing
here, I mean you can try
to be a little bit more
clever if you would like
and do smart updating based
on how much the list
has actually changed.
But we found, for
performance reasons,
just doing a simple insert
at index 0 or removing,
starting at index 0, seems
to serve us pretty well.
So let's not do more
work than we have to.
Alright so to recap,
the number of the stocks
in your stock list, or in
my case, the stock list,
in your case, I'm not sure
what you're putting in there,
but keep the number of
rows down and cap it
at something reasonable
for your use case.
Next, when you're inserting
and removing rows, that's going
to be much more efficient than
if just calling the set number
of rows method on
WKInterfaceTable.
So one last thing here,
instead of iterating
over the entire table when
single row updates are coming.
So think about it this way,
like what if we're updating
the Apple stock price
in our table list or
our list of stocks?
Instead of going through and
updating each one of those rows
when we don't have to, we can
make use of the rowController
at index so that way we only
update the rowController
that we care about.
Or, you can even do something
similar to storing a reference
to that rowController
and updating it later.
So now we're going
to talk a little bit
about updating your UIElements.
So as Tyler mentioned before,
these UIObjects and WatchKit,
they're modified in
the extension process,
and updates to these
properties are sent
from the extension
process to the app process.
And the app process handles
layout of the interface.
So let's look at
our UI for Stocks,
and this is just
a rowController.
But we have the platter
here, which is a group
which has just the
tappable area for the row.
which has just the
tappable area for the row.
Then we also have the list name
and so that's just
the ticker symbol
of the company name,
that's a label.
The change in points label,
and that's just the
change that we've had.
And then we have the price
label, current price.
So let's look and see
what we're doing there.
When we would go to
update this rowController,
we had this update method
and it would just take
whatever values we gave it
and it would set those
properties right away.
Now that's bad because
properties
on the interface object
are not cached, right.
And setting a property on
that object sends that value
to the app process every time,
and I'm redundant on this
but I want to emphasize
the importance of that.
On average, in my profiling
in Stocks it would take roughly
200 milliseconds for a value
to move from the extension
process to the app process.
And that doesn't really
seem like a long time, but,
in some profiling, that I
did for the initial launch,
in some profiling, that I
did for the initial launch,
I saw a pretty staggering
number of on average,
a worst case scenario
1.4 seconds for some
of those messages to get sent
over from the extension
process to the app process.
So it's a big difference.
So what can we do to
be a little smarter?
Really just cache those values
that you've already sent over
and then only send them
if they've changed.
So let's do a little recap of
our resume time discussion.
We want to minimize the work
performed in our willActivate
and our didAppear, and
we'll want to make use
of cancelable operations
whenever possible.
It's also important to note,
that overly complicated user
interfaces, they're going
to lead to slower load times.
So the more data that you're
having to pull through
and update on the UI,
the slower it can be.
And of course, we'll only want
to update our user interface
when necessary, so only
when things change.
So to summarize the Stocks
case study and what I would
like for you to take from
this, think small in your apps.
like for you to take from
this, think small in your apps.
Keep your tasks small
and easy to perform.
You'll want to simplify your
user interface and you want
to make use of the new
background refresh APIs.
Focus on resume time in your
apps, we want to pay attention
to the WKInterfaceController
lifecycle methods,
especially willActivate
and didAppear.
And make use of our cancelable
operations when possible,
and optimize when updating
your user interface
by not sending redundant
information.
For more information, you can
view the developer website.
Our session number is 227.
Some of the related sessions,
unfortunately have
already happened,
but some of these I
feel are important
to not only WatchKit development
but, we have concurrent program
on GCD in Swift 3, so that's
also important as well.
So thank you and have a
wonderful rest of the week.
[ Applause ]