[ 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.
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 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.
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 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 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.
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 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 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.
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 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, 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 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.
[ Applause ]
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 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 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 day, however.
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 refresh time.
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.
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.
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 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.
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.
[ Applause ]