Operations are a flexible way to model your app's business logic, but they can do so much more. See how NSOperation forms the heart of the WWDC app, and how using features like dependencies, readiness, and composition allow you to quickly and easily build dynamic and complex apps.
(Applause) PHILIPPE HAUSLER: Good morning. My name is Philippe Hausler. I work in the Frameworks Group, on Foundation. And today we are going to talk about NSOperation and NSOperationQueue. These are two extremely powerful classes that can transform your application from running tasks linearly to a hybrid scenario of both the object-orientated and functionally asynchronous concepts. Now, I am pretty sure that all of you have seen this application.
The WWDC app uses NSOperation and NSOperationQueue extensively.
To be able to accomplish numerous different tasks all the way from downloading content from the Internet all the way to synchronizing the database as well as even concepts like presenting alerts or displaying videos. And here to take you more in depth of NSOperation and NSOperationQueue and how it actually was used to be able to implement the WWDC app is Dave DeLong. Dave? DAVE DeLONG: Thanks, Philippe. So, my name is Dave DeLong, and I am a Frameworks Evangelist at Apple, and I am also the primary engineer on the WWDC app, which hopefully you are all familiar with.
Today we are going to be covering three main areas of the WWDC app and NSOperation. First, we are going to go over the core concepts of NSOperation and how you can understand its API and take advantage of its powerful state machine. Next, we are going to go beyond those basics and look at the challenges of the WWDC app across and how we solved them. And finally, we are going to talk about some sample code that we have provided for you.
So first, let's look at some core concepts.
Any time you use an NSOperation, you will always be using an NSOperationQueue. And the way to think about an NSOperationQueue is that it's a high-level dispatch queue. Hopefully you are all familiar with dispatch queues from using Grand Central Dispatch. Now, by providing a wrapper around an NSOperationQueue, we can gain some additional functionality.
For example, NSOperationQueue makes it very easy to cancel operations that have not yet begun executing. While you can perform cancellation of dispatch blocks, it is somewhat tricky to do so, but NSOperationQueue makes this quite easy.
Another thing that you get with NSOperationQueue is a property called the max concurrent operation count. And to understand what this is, let's take a look at a little animation.
If we set the max concurrent operation count of an NSOperationQueue to be 1, then we essentially make our NSOperationQueue into a serial operation queue.
So let's load up a bunch of operations onto this queue.
With the max concurrent operation count of 1, the queue will pull off these operations one by one and execute them in order. The next operation will not begin executing until the previous one has finished.
That's a serial queue.
However, by default, the value of this property is a default value, which means as many as the system allows. So this means that our operation queue can perform multiple operations simultaneously as system resources allow.
So in this case, our operation queue might be performing two operations at once. The ability to change the behavior of an operation queue like this can be very powerful. We don't have to decide this at creation time of our operation queue.
So that's NSOperationQueue. Now let's take a look at NSOperation. Where the queue is a high-level wrapper around a dispatch queue, you can think of an NSOperation as a high-level wrapper around a dispatch block.
Now, in general, NSOperations run for a little bit longer than you would expect a block to run, so blocks usually take a few nanoseconds, maybe at most a millisecond, to execute. NSOperations, on the other hand, can be much longer, for anywhere from a couple of milliseconds to even several minutes, as we will talk about later.
The other thing that's really nice about an NSOperation is that since it's a class, you can subclass it and provide your own custom logic on how it executes. So in order to subclass NSOperation, let's take a look at its lifecycle.
When you create an NSOperation, it always starts off in a state that we call the pending state. So this is the operation when it's initialized and as it's being put onto its operation queue.
Now, at some point, the operation is going to become ready to execute, and it enters the ready state. And after it becomes ready, the operation queue will pull it off of the queue and begin executing it. And like I said, this execution can be anywhere from a couple of milliseconds to several minutes to even longer.
After execution finishes, the operation enters the finish state, its final state. So that's pretty simple. The other thing that an operation can do is at any point, it can enter a canceled state.
So let's take a look at cancellation.
Cancellation on an NSOperation is defined as a simple Boolean property, is canceled. And the important thing to understand about this property is that it only changes the state of the property. When you cancel an operation, all that's happening is that a Boolean value is getting flipped.
So as you subclass NSOperation, it is up to you to decide what it means for your NSOperation to be canceled. So, for example, if your operation is performing a network task, then maybe canceling your operation is akin to canceling your network communication. Or perhaps if you are performing some sort of database transaction in your operation, then perhaps canceling your operation would be like discarding that transaction. So as you subclass NSOperation, be sure to observe this value changing and react appropriately if there's any reaction you need to do.
The other thing to be aware of with cancellation is it is susceptible to race conditions.
What do I mean by this? Well, let's consider an operation that's executing in the background, and maybe in your UI you have a cancel button that would cancel this operation. If the user taps the cancel button, it's going to take a small amount of time for that message, to cancel, to move from the main queue to the operation in the background.
And if in that small window of time your operation finishes executing, then your operation will actually never be canceled because an operation cannot go from the finished state to the canceled state.
So it is important to understand that just because you try to cancel an operation, there are some cases where it won't actually cancel.
However, if you do need to cancel an operation, it is very easy to do so. All you need to do is call the cancel method.
So that's cancellation. Now let's take a look at this other interesting state called Ready.
The readiness of an NSOperation, like cancellation, is defined as a simple Boolean property, is ready.
And what this property means is that the operation is ready to execute. So let's take a look at how this interacts with operations on an operation queue.
So again, we've got our serial operation queue, and we are going to load up a bunch of operations, and they are all in the initial blue pending state.
Now, the first operation to enter the ready state is the first operation that will be executed, even, for example, in this case, if it's the fourth operation that was put onto the queue. So once the operation is ready, it begins executing.
Then, as other operations become ready, they are pulled off the queue and executed.
And in this case, since we have a serial queue, only executing one at a time, if two operations become ready at the same time, then the first one that has a higher priority will be pulled off first, and then the second one will be executed. And then, as the other operations become ready, they are also pulled off the queue and executed.
So that's brief look at readiness.
Now, what can we do with this? Well, we can make dependencies.
Dependencies are a way for us to express a strict ordering between our operations, that first we want to execute this thing, and then we want to execute that thing.
And the neat thing about dependencies is they provide the base definition for what it means for an operation to be ready.
By default, an operation will become ready if all of its dependencies have finished executing. This is behavior that you get for free.
The other neat thing about dependencies is that they are not limited by operation queues. Now, what do I mean by this? If you have two operation queues in your application, operations in the first queue can be dependent on the operations in the second queue. And we are going to see later how this can enable some really powerful patterns.
Now, setting up dependencies amongst your operations, again, is extremely simple. All we need to do is use the add dependency method. So in this case, operation B will become dependent on the successful exectution of operation A. And so operation B will not execute until after operation A. This is guaranteed. Now, with dependencies, we can run into a couple of problems, like operation deadlock.
So if I have an operation A and another operation B that is dependent upon the execution of A, this is fine. However, if I inadvertently make A also dependent on B, then these two operations will never execute because they will both be waiting on each other to finish, and since they are both waiting, they will both never start. So as you are setting up dependencies in your application, don't do this.
Now, the WWDC app uses dependencies all over the place. And a really simple example is what happens when you tap the "add to favorites" button on a session in the app, which hopefully you all did for this session.
When you tap that button, we're going to first create an operation called the login operation. This is an operation that guarantees that you have logged into the app with your developer name and password. Next, we are going to create another operation called the User Info operation. This is an operation that guarantees that the user name and password are actually a developer username and password and not, for example, your iTunes username and password. So your Apple ID is an appropriate developer Apple ID.
Now, favorites in the WWDC app are stored in CloudKit, so we also need another operation to make sure that we have access to your iCloud account. So this happens silently, because we are not requesting permission to see your first name and last name in the app, so we need to make sure that you have an iCloud account. And finally, we can set up the save favorite operation, and this is dependent on the successful completion of verifying that you are a developer and the successful completion of verifying that you have an iCloud account.
So that's a simple example. Let's take a look at a bit more complex one.
When the WWDC app starts up, there's a bunch of setup that we need to do. First, we are going to download a small configuration file, and this file will tell us small things like what's the most recently supported version of the application, what features we have enabled, and so on. So after we download this file, we are going to perform a version check to make sure that you are running the latest version of the WWDC app.
And then after we check the version of the app, we can start downloading useful pieces of information, such as the news that we show in the News tab and the schedule for the conference. After we've downloaded the schedule, then we can start importing any favorites that you've saved to iCloud, any feedback that you've submitted so you can see it in the app, and we are also going to start downloading the list of videos. All of these things require the schedule to first be in place.
And then finally, we can save our NSManaged object context, where we are saving all of this information. So let's see how dependencies and operation lifecycle affect the execution of these operations. And we are going to move them all into the Pending Operations state. Now, the first operation to download the app settings has no dependencies, so it immediately becomes ready to execute. And so our operation queue is going to pull it off, execute it, and then it's going to finish. Now, when it finishes, the version check operation also immediately becomes ready to execute, and so it's going to get pulled off the queue and executed.
When it finishes, the next three operations simultaneously become ready to execute. So they're going to start executing.
And as they finish executing, more and more operations will become ready to execute. They will be pulled off the queue and executed.
Now, the important thing to realize here and to notice is that this final operation to save our context does not become ready to execute until everything else has already finished. By using dependencies, we can guarantee that things happen in the correct order and that nothing will get out of order. So now that this one is ready, it can be executed, and it can finish. And App Start-up can continue.
So that's a look at dependencies.
Overall, NSOperations are a fantastic way to abstract logic in your code.
By putting our logic inside of operations, it makes it easier to simplify these logic changes because we are dealing with isolated pieces of work, much like we do when we are dealing with a block. As an example of this, the WWDC app this year moved from being -- from saving your favorites and feedback on a custom backend, to being on CloudKit.
Now, at this point I want you all to think about, what would it take to move your application from a custom, from whatever service you are using now to CloudKit? And if you are suddenly panicking, and all of the places in your code where you've got network communication, and all these dependencies on, you know, the intricacies of your server provider, then this is a good sign that you should be using operations.
In the WWDC app, all of our network communication is hidden behind operations, which means that in order to change the backend from using a custom service to using CloudKit, all we had to do was rewrite four small classes. It took us less than a day and then a couple more days to successfully test our changes. It was a simple, trivial change.
Now, in all of this, you might be wondering, well, what about Grand Central Dispatch? Grand Central Dispatch absolutely has its place. In fact, when you download the sample code for this presentation and look through it, you will see places where we are using Grand Central Dispatch in that sample code for things that are not really appropriate to NSOperations. So for example, anytime you simply need to bounce a method call from one queue to another queue, you don't need to wrap that in an operation. That's something you want to keep fast and very lightweight. Or if you are doing anything with semaphores or dispatch group, these are all perfect use cases for Grand Central Dispatch.
So that's a look at the basics.
Let's go beyond them.
Now, one of the things that we realized in the WWDC app is that there are places where we want to have UI interaction but still have it participate in the operation chain. So for example, authentication. We talked earlier about saving a favorite. We need to make sure that you are logged in. But what if you are not? Well, we realized that we can put UI elements, UI functionality within our operations. So for example, the authentication dialogue that slides up in the WWDC app is actually an NSOperation.
Or anytime that you are watching a video in the WWDC app, we encapsulated this inside a "watch video" operation. So all we need to do is create one of these operations with the appropriate video asset and put it on our operation queue, and everything else will just fall into place.
Even more broadly, any time you see an alert in the WWDC app, this is something that we thought was also a really good use case for putting UI inside an NSOperation.
And we discovered that the underlying principle we found here was that when we're dealing with any sort of modal UI, so a UI that takes over generally the entire real estate of your application, this is an excellent thing to encapsulate inside of an NSOperation. So to reiterate, the first time you launch the WWDC app, you saw this dialogue asking if we could collect some simple usage data on how you are using the app. This dialogue that appears, this UI alert controller, is actually being run from inside an NSOperation.
Or the login sheet. If you try to add something to Favorites or leave feedback on a session, this is also an NSOperation.
The next thing we encountered is that there are some times we want to perform simple pieces of logic as a block, but we also wanted to participate inside the operation mechanism. So we turned to block operations, NSBlock operation and other custom operations that we created.
So this is really just an NSOperation to execute a block. And you may be asking, well, if NSOperation is just an abstraction around a block, why would I then return to using blocks inside an NSOperation? And that's because by putting a block inside an NSOperation, you gain all of the great features of NSOperation for that block that you do for NSOperation, such as dependencies.
Let's take a look at what we can do with this and see what happens when you tap the Leave Feedback button in the WWDC app.
Well, the Leave Feedback button wants to perform a segue. It wants to present the view controller where you can leave some five-star ratings or maybe four if they were really good but not truly excellent.
We want to perform this segue. So we are going to put this segue inside of a block, and then we are going to put this block inside of a block operation. Now, we only want to allow you to leave feedback if you've signed into the app. So we need to verify that you've signed in with your developer account, just like we do when you save a favorite. And in order to verify that you have a developer account, we need to make sure that you are logged in at all. So by putting the perform segue call inside of a block operation, we can guarantee that we will never present the login sheet until after you have logged in. This is really powerful. We have described a really complex behavior, a sequence of things that must occur simply by using operations and dependencies.
Now, as we were writing the WWDC app, we noticed that there were some cases where we were doing a lot of the same operations over and over again. So for example, we've already seen this login and user info operation a couple of times.
So we thought wouldn't it be great if there were a way where we could just automatically have those operations created? So we came up with a way for an operation to generate its own dependencies. In other words, we're expressing the idea that we never want to execute this thing without always executing this other thing.
So again, let's take a look at saving a favorite to CloudKit, or perhaps downloading a pass, or really, anything in the WWDC app that requires you to be logged in.
So when you tap the "add to favorites" button, all we are really doing is creating a single operation to save the favorite. And this is going to encapsulate some small pieces of information, the session identifier and whether or not you want it added to favorites or removed from favorites, a little Boolean flag.
Now, this "save favorite" operation knows that it requires permission to run, so it is automatically going to generate two dependencies, the one to check that you are a developer and the other to guarantee that we have access to your iCloud account. Now, the operation to guarantee that you are a developer itself needs to guarantee that you are logged in, so it generates its own dependency to make sure you are logged in. And so we are able to keep our app code quite simple. We only need to create a single operation, and then it automatically generates its own dependencies. And perhaps later, if we decide to remove the requirement that you need to be logged in to save a favorite, then we simply remove the small line of code that instructs this favorite operation to generate that particular dependency, and we have now removed that requirement across the entire application. We don't have to go through every single place where we have an "add to favorite" button and modify code there.
Now, we also wanted to make sure that other kinds of conditions were met.
We wanted to be able to expand upon this idea of readiness, expand upon the idea of when we're allowed to execute an operation. Some examples that we came up with include, we only want to execute this operation if you are actually connected to the network. If you try to add the favorite in while your phone is in airplane mode, for example, we, of course, don't want to try executing our CloudKit operation.
We also want to guarantee, perhaps, that maybe we only want to execute an operation if we have access to your location. So we need a way to express this as well.
Or for example, we only want to execute certain kinds of operations if you are actually logged into the app.
So by extending the concept of what it means for an operation to be ready, we can make our operations even more powerful.
So hopefully you won't ever see this error, but if you do, this is an example of an operation failing because it was never able to become fully ready to execute. In this case because it was unable to connect to the network. So extending the readiness concept can also be really powerful.
Next, there were a couple of operations where we found that they were always being done together. So we thought, wouldn't it be neat if instead of having to create the same sequence of operations over and over again, if we could just create one operation and then under the hood it would create the same sequence of operations for us? A common example of this is the idea of downloading something and then parsing it to save into a local storage.
I am sure this is a concept that almost all of you are familiar with. So let's take a look at how we can compose operations to make them simpler. So let's say we have a generic import data operation, and then it's dependent on something and other things are dependent on it. We have this import idea. Well, we want this to actually do two things, so it's going to wrap another NSOperation, and this operation is simply going to perform the download. It's a single, isolated piece of work.
And then it's going to create a second operation to parse whatever was downloaded and make it dependent on the download operation so that parsing will always occur after downloading. Now, by encapsulating those two operations inside of a larger operation, we can now easily change perhaps, maybe where our data is coming from, what format it's in, and even how we handle errors. And we only have to do this in one place, inside our import operation, because that's the only thing that the rest of our app knows about. Now, you don't always know ahead of time, perhaps, the exact operations that you need to perform.
In the WWDC app, we cannot know at compile time how many favorites you've saved to CloudKit, so we needed a way to be able to dynamically compose operations.
So we created this wrapper called a Fetch Favorites operation, and since we are using CloudKit, under the hood we are going to perform a CK query operation, because CloudKit is also built on NSOperation.
So we are going to perform our first query operation. And maybe you have every single session favorited at WWDC, so this is going to indicate that there are still more favorites to fetch. So we are going to keep on executing query operations until we have received a response that that's all of them and we've got them all.
So by using this composition model, we can still simply express our operation chain as a single -- with a single "fetch favorites" operation, but under the hood, actually be performing many operations, potentially, in sequence.
Now, in the code, it looks something like this. Our operations have an execute method, and this is where they all start doing their work. So the first time the fetch favorite operation starts executing, it's going to set up the initial query. We are going to look for session favorite records created by you. So we are going to construct our query operation and pass it to this method called execute query operation. And this is the execute query operation. As this query operation completes, we are going to first check, was there an error, and if there was, let's abort the process and handle the error. If there wasn't an error, but instead, there was a cursor, this is how CloudKit tells us that there are still more things for us to fetch. So we're going to create the next CK query operation in the sequence using this cursor and semi-recursively call this execute query operation. And this is how we can be executing many query operations.
And then if we get neither the cursor nor the error, this is how CloudKit indicates that we have fetched everything, and so we can begin to import the records that we have downloaded.
Next, during development, there were some times when we came up with some visual glitches, the things that we thought were visual glitches. Now, perhaps you've all had the experience of using an app and an alert pops up, and then as you are about to tap the button, another alert pops up. And you think, oh, great, man, what is even going on now? And as you are about to tap that button, maybe another alert pops up, and with all of the animations of coming and going, you are no longer even sure if you are back on the first alert or if you are now on the third.
We really wanted to avoid this confusing scenario.
Another thing we wanted to do is we wanted to guarantee that you could never, ever try to watch more than one video at once. This is something that the WWDC app does not know how to handle correctly, so we wanted to guarantee that no matter what you did, we would never allow you to do that. Another thing we wanted to guarantee is that we would never try to load our underlying database more than once. So we came up with a way of describing mutual exclusivity, the idea that only one of these particular kind of operations can be running at a time. Now, you are probably thinking, wow, this is a really complex idea. How would we even do this? And it is really simple. So let's go back to the alert example.
Let's say we create an operation to display alert and alert to the user, and we put it onto an operation queue. And maybe it's there waiting for something else to finish, maybe it's already in the middle of executing. Who knows? But then something happens, and we decide to create another alert operation. Well, all we need to do is make the second alert operation dependent on the first one. And this is where cross-queue dependencies are really powerful. Because it does not matter which queue this alert operation is executing on, as long as the second operation is dependent on the first, then the second operation will never execute until after the first operation completes. And so for some -- if for some insane reason, we decide to create more alert operations, even more alert operations, as long as we set up these dependencies of the next operation being dependent on the previous, like a singly linked list back in time, we are guaranteeing that our operations will be mutually exclusive.
This is really powerful. By using dependencies, we can guarantee correct behavior in our application. We can guarantee that you will never see more than one alert at once. We can guarantee that you will never be able to watch more than one video at a time. We can guarantee that we will never try to load two copies of our data store simultaneously.
So those are a taste of some of the challenges that we came up with when writing the WWDC app. There are more.
But we think these ones are really cool. And we came up with what we thought was a pretty neat way to solve them. So let's talk about the sample code. On the WWDC website, under the sample code section, you can find a piece of sample code called Advanced NSOperations. And this is a simple app to show recent earthquakes. But under the hood, it's built entirely on NSOperations, and the operations that it's using in the app is code that we have extracted from the WWDC app and put into the sample. And this is code that's been in the app, actually, for a couple of years. It is stable.
Now, the primary class that this sample code uses is operation. And this is a basic subclass of NSOperation. And in the sample code, this operation adds two key features.
The first is the idea of a condition, which we will talk about in a second. And the second is a concept that we call "observers." Now, we've got a bunch of different kinds of operations in the sample code. We have group operations, so it's very easy to make operations internally perform more operations. We also have an operation subclass in the sample code that allows you to take an NSURLSession task and wrap it up inside of an NSOperation so that you can make it, perhaps, dependent on something else or make other things dependent on this, or perhaps add conditions or observers to it. There's a simple operation to request your current location. There's one -- because it's sometimes useful -- to just simply wait a little bit of time. There's even an operation to show an alert to the user with buttons and block handlers. So lots of great kinds of NSOperation subclasses in the sample code. Now, this operation has a concept of a condition. And a condition is a protocol that we have defined, and it's a way for an operation to express how it generates dependencies, how it defines mutual exclusivity, and also how it extends the concept of readiness. So some kind of conditions that we have provided in the sample code. One is the mutually exclusive generic condition, and this is a way of describing that an operation is mutually exclusive with other kinds of operations with the same generic type.
We have a reachability condition in there, so you can simply, with one line of code, express that an operation can only execute if the network is reachable at a very high level. And we've got a plethora of permission conditions, such as only execute this operation if we have access to a certain CloudKit container, or only execute this operation if we have access to your calendar or to your photo library or to your contacts or whatever else you'd like. So that's conditions. And the final piece is operation observers.
An operation observer is again a protocol type, and it's a way for this value to be notified about significant events in operation lifecycle, such as start or the beginning of execution, the end of execution, and also if the operation decides to produce another operation that should be executed later, such as if an operation decides that it failed and it wants to show an alert, it can produce or generate an alert operation.
And we have a couple of examples of observers, such as timeouts. By simply adding a timeout observer to an operation, that observer is going to watch to make sure that the operation completes within whatever time interval you specified, and if it takes too long, it's going to automatically cancel it. One that I think is really neat is a background observer, so this is an observer that when you attach it to one of these operations is going to watch the state of your UI application, and if your application enters the background, it's going to automatically begin a background task, and then automatically end it when the operation finishes executing. So if you have some sort of critical operation, perhaps you are uploading data to a server and you don't want that to be interrupted or suspended, one way you can accomplish this is by adding one of these background observers to the operation, and it will guarantee that you have some time in the background during which you can complete this operation. And another one that's really cool is the network activity indicator observer. This is a simple observer you can attach to an operation, and when it begins, it's going to increment a sort of retain count on the activity indicator spinner in the status bar, and then when the operation ends, it's going to decrement that retain count. So you can have multiple networking operations in flight at the same time, and by simply attaching one of these network indicator -- or, observers -- it will automatically show and hide the network activity indicator as appropriate. It is no longer this crazy -- crazy state that you have to manage yourself. It all kind of happens automatically. It's really cool. And then there are other observers that we have in the sample code for you, such as being able to attach arbitrary blocks to one of those three events and have them react appropriately. So that's a quick look at the sample code. On the surface, it looks like a really simple application, but under the hood, there is lots of really meaty goodness, and I really encourage you to download it and check it out. So, in summary, use operations to abstract the logic in your app. By putting your logic inside of operations, it becomes very easy to change it later, such as how we converted the WWDC app to use CloudKit. It was a simple change for us.
Use dependencies to express the relationships between your operations. It makes it very simple to guarantee certain kinds of behaviors, that B must always follow A.
Next, operations allow you to describe complex behaviors, such as mutual exclusivity or composition. These are all simple with operations. And overall NSOperation allows you to perform some very powerful things with very minimal effort.
So we have a couple of related sessions for you. Immediately after this session is "Building Responsive and Efficient Apps with GCD." We don't want you to leave GCD behind. It is still a perfectly appropriate technology to use. So I encourage you to go to this session or watch the video and see when you should be using GCD in your apps. And then if you want to see more about how our frameworks use NSOperation, you can watch the "CloudKit Tips and Tricks" session from this year or the "Advanced CloudKit" session from last year.
Like I said, we have sample code available on the WWDC website. I encourage you to check it out. I also want you to read the 'Threading Programming Guide' in the Developer Library. This has a lot of really great information on other ways that you can use NSOperation. And if you need any technical support, we encourage you to post your questions in the Developer Forums or contact DTS. Thank you very much, and have a great rest of the conference. [ Applause ]
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.