A well performing Watch app begins with great app architecture. Join us to discover strategies you can apply to keep your app responsive, lower your app's resume time, and stay memory efficient. Learn how we used these same principles in the Stocks app to enhance the user experience for watchOS 3.
[ Music ]
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.
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.
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
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.
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
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
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
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
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
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
And when your application returns
to the foreground you'll get the applicationWillEnterForeground
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.
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
of how an application might see these events during
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
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
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
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.
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
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
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
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,
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
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
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
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.
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
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
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
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
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
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
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
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
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
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.
So the later date would always be the refresh cadence
in that scenario.
So let's look at that earliestNextOpenDateInStocks
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.
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
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
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
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.
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,
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.
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.
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.