ProcedureKit and You
Jon Shier at Swift Summit San Francisco 2016
Thank you. Like Ida said, my name is Jon Shier. I'm an iOS developer with Detroit Labs in Detroit, Michigan, and this is ProcedureKit and You. So, what is ProcedureKit? ProcedureKit is the framework formerly known as Operations that's based on the WWDC 2015 session advanced NSOperations and was created by Dan Thorpe after that. It was forced to rename to ProcedureKit, of course, because of the great renaming that we all saw in Swift 3. You can't really use the name operation anymore without direct collision. It provides operation and operation queue subclasses and related protocols that support enhanced functionality over what's provided by the foundation types. Adopting ProcedureKit can mean greater code re-use and flexibility, easier composition of complex asynchronous tasks and higher reliability for those tasks because all of the testing that's been done to work around issues with Operation.
So, what does it provide? What does it do? At its core, it provides the Procedure and ProcedureQueue sub classes and various protocols. These features, some of which I'll be talking about today, are things like conditions to control whether a procedure is executed at all. ResultInjection, which allows you to pass the results of one procedure to another automatically. Observers, which allow you to tap into the lifetime events of procedures as they're running. Capabilities to insure that your code has permission to access, say the use location, or a Cloud Kit container before you perform your task. It also provides a series of testing classes which make it much easier for you to test your synchronous code.
So, the procedure class itself is like I said, a subclass of Operation that's at the core of the framework and it provides all of the attachment points for the other features like conditions and observers. It needs to be subclassed to be useful, just like operation, but the framework does provide quite a few built-in subclasses to accomplish tasks that you probably already do in your application. For instance, there is a UserLocationProcedure to find the user's location which will also automatically prompt the user for permission so you don't have to manage that yourself. There's the GroupProcedure, which can combine several procedures into what appears to be one allowing you to operate on a whole set of them as if they were a single procedure. There's also the RepeatProcedure which can repeat a procedure, like it says, until a condition is met. This allows you to, say, do polling against an API or something like that without having to write all of the time and repeat code yourself. This is just a subsample of a variety of subclasses that are included with the framework already.
So, we have something of a problem. We have this giant picture of a kitten and we need to make it smaller. This is a very typical user task for an asynchronous operation. We can do this in ProcedureKit, like I said, by subclassing Procedure. This procedure, of course, just takes in what size we want to resize to, a completion handler that will be called when the resize is complete, and then it overrides the execute function. Now, if you've ever implemented operation subclasses you've probably had to override main, or one of the other functions. And, it's not always really clear because if you're writing in synchronous operation versus in asynchronous operation, you might have to override different things. Well, in ProcedureKit, the procedure subclass allows you to just override execute. This is a single thing that can be overridden, wether you're synchronous or asynchronous, and as long as you eventually call finish, the procedure will complete successfully.
And so, as you can see in here really all we're doing is making sure we aren't canceled before we begin our work. We read the image from disk, create a UIImage out of it, and then we resize it using the UI image graphics context. And then, we dispatch the completion block back on to the main queue, so that, of course, we can update the UI with that new image. All very straightforward. And how would we run this? Well, we can create an instance of this procedure. We feed it in size that we want and the completion handler that we want, and then just like an operation we create a procedure queue, and add this procedure to that queue. As soon as it's added to the queue, it starts executing. So, if you want to do work before it's added, you need to do that during your declaration time.
Now, there are some drawbacks to this very simple implementation that I created here. Of course, first, there is really no error handling. If we're missing a file, if we fail to initialize some data, if the resized image didn't work for some bizarre reason, we don't really handle those errors at all. This is also a single purpose procedure, in that, obviously it reads just a single file from disk, and then resizes it. And the file name is fixed, and things like that. There's no abstraction, which means it's not really very reusable. It's also more difficult to test, because you have to create the entire environment up front, and then run the task, and then see what comes out the other side. So, that can work if that's really all that you're doing. But, if you're doing similar things elsewhere, breaking it up into more modular pieces can really help in testing.
So, one of the first things that we can do to make our procedure safer is add a Condition to it. Now, conditions are essentially a procedure subclass themselves that run before your procedure and determine wether or not it gets to run. And so, there are several subclasses already built into the framework, like MutualExclusion Condition, which allows you to mutually grant a resource to your procedure, like reading from a file, or checking some sort of permission, or prompting the dialogue. There's the NoFailedDependenciesCondition, which ensures that your procedure won't run if any of its dependencies have already failed. And then, there's also simple things like the BlockCondition, which just allows you to run a simple boolean closure, to determine yes or no, wether a condition succeeds.
And so, what does this look like for our kitten resize procedure? Well, we can create this KittenExistenceCondition. And, a condition only needs to override this evaluate method, which takes in the procedure itself, as well as a completion handler that's called when you're done evaluating wether the condition is successful or not. As part of a failure, you need to return an error, so I've created a simple error enum here. And so, we make sure that we can read the file from disk. If not, we return an error. Then we make sure we can create a UIImage from that, and then return an error. And then, if both of those things have succeeded, we can call the completion handler with the satisfied state of the enumeration.
And this is a very simple addition to the code that we've already written. It allows us just to declare the condition, in addition to our KittenResizeProcedure. The procedure then has that condition added to it. And, of course, you add it to the queue like normal. And so, before the KittenProcedure itself is run, the condition will run, and evaluate yes or no. And if it fails, then it will cancel that KittenResizeProcedure. And so, you won't even have to deal with wether or not that runs.
So, the advantages here, of course, is that we're now generating proper errors. Failing conditions propagate their errors to their attached procedure. And the procedure is canceled, like I said. It's also extracted some of the logic. So, now that we have this separate thing that's just checking to make sure a file is on disk, it's easier for us to encapsulate by itself, and then test separately. It's reusable, and we could make it more reusable, of course, abstracting out the file name, and just make something like a file exists condition.
But there's still several drawbacks here. We have some redundant logic. We're reading the file from disk, and creating a UIImage in both the condition and the procedure itself. And, the current way we're composing these can't really get the error from one point to the other. So, our completion handler doesn't even handle the error case.
That is the reason for one of the other major features of the framework, which is the automatic result injection. And the ResultInjection protocol injects values from one procedure into another. It works by automatically adding observers to those dependent procedures, so that when it's injecting procedure completes, it automatically starts the next one after setting the result value.
It works in conjunction with other provided classes in the framework like Map-, Reduce- and FilterProcedures. And, many of the built in procedures already have conformance to this protocol. So, you can plug them into your streams of expected results just as if you'd written the code yourself. You can adopt this by having procedure subclasses conform to the protocol directly. Or, if your work is synchronous, you can subclass the ResultProcedure or TransformProcedure, which are generic to the result, or the expected value and then the result, to make sure that the work is done. But, if you're creating asynchronous work, like a network request or something like that, then you will still need to subclass the procedure class directly.
And so, the ResultInjection protocol is actually fairly straightforward. It has two associated types, and then it has this PendingValue enumeration. These associated types are the type of your requirement, which comes in, and your result, which goes out. The PendingValue enumeration is sort of like a result, but it allows for the void situation, which is, if you don't have a requirement, if you're at the start of a queue of procedures, you don't have anything that's going to come in, you can declare it as void, and it knows to ignore it. And then the same thing for the result. If you're bringing in results, but you're not doing anything with it, you can just have that as a void.
And so, we can now refactor the current procedures that we've already created. So, instead of having a condition, I can create this KittenReadingProcedure, which reads it from disk, and has the result of the UIImage that comes out of it. And by subclassing ResultProcedure, I can make it generic to the UIImage already. And this allows us to reuse some of the code, rather than having to build up the boiler plate of the ResultInjection protocol, where you have to declare the requirement and the results separately. That's already taken care of for you, and so all you have to do, when you call super.init, is provide a closure that does the work.
And so, as you can see here, we're doing something very similar to what we did before. We read the file from disk, and we throw an error back if it wasn't there. We convert that to a UIImage, throw an error back if that didn't work, and then we return the image itself. So, this has not only simplified our code, but it has made it much more flexible, as we can now plug this into our sequence of procedures. And the KittenResizeProcedure itself can be transformed, or refactored into a subclass of the TransformProcedure, which as you can see by its declaration, takes any UIImage, and then returns a UIImage. It's very similar to a ResultProcedure, except that it has input and output at the same time.
So, you can see here, when we call the super, it brings in the input image, resizes it, returns an error if that fails, or throws an error if that fails, and then returns the resized image. This allows our use case to become a little bit more elegant, where we have the readingProcedure by itself, we have the kittenProcedure itself, that is just initialized with the size. And then, by calling injectResult there at the end of that line, we can take the result from that readingProcedure, which is the UIImage that we've already created, and allow it to take the image from disk and put it into our transform. And, of course, I've taken the opportunity to pull the completion handler out of our Procedure, and make it its own value because, really you would want to extract that separately. But, I just didn't want to complicate the example, here. The kittenProcedure just adds a BlockObserver, which is something I'll be talking about in a moment, and allows it to call the completion handler. And, of course, you add it to the queue, and everything executes as you would expect.
So, our advantages here is that we now have full error propagation. Failures in an injected procedure are propagated to the dependent procedure. And, we've fully extracted these classes, so they can now be run and tested independently. If we wanted to go further, we could have a file read procedure, an image extraction procedure, and things like that. It could all be tested separately, and could be used in sequences of procedures that have nothing to do with what we're currently doing.
Now, there are really a few drawbacks here. The results injection protocol is a little bit more complicated. There's several layers of generics here, as you can probably see. And so, it's very powerful, but that power comes at a bit of a complexity price. But, once you get the hang of it, it's very easy to build these in a reusable fashion. Also, error handling isn't completely automatic. Prior errors aren't part of the result. It's not like a real result type, that has either a value or an error. Instead, the results comes in automatically, but the errors themselves are used when they cancel the dependent procedure. And we extract it manually when we call the completion handler. So, it's not completely automatic. But, it's still very close to an ideal situation.
One of the other main features of ProcedureKit is our observers. And, the ProcedureObserver protocol allows you to hook into different lifetime events of your procedures. And, of course, there are several built in types already. The TimeoutObserver can cancel a procedure if it's exceeded a certain amount of time, or a certain date has been reached. The BackgroundObserver can automatically convert your procedures into background tasks when the app goes into the background. So you don't have to do that yourself. And the NetworkObserver can automatically trigger the activity indicator in the status bar during any running procedure. GroupProcedure is good to use here to combine many into one, and just observe that one group.
So, we can look briefly at the Observer's Protocol itself. And you can see all the different events that we get here. I won't go through all of them, but you can see that you get most of the important things, including when the observer was added to the procedure. And also, something that I'm not going to really talk about today, but also procedures can produce other procedures. And you can see here that we get it as part of this protocol.
And so, I can make a very simple observer here, where say I want to start and stop animating an activity indicator. And so, I create this GroupProcedure that brings in a bunch of different procedures. I've added a delay procedure here, that delays it by 2 seconds. So you can actually see the activity indicator. And so, these blocks will be executed at the WillExecute point and at the DidFinish point, to start and stop animating this activity observer, or this activity indicator I should say.
And so, you can just add that group to your procedure queue, like normal. And now, we have a very simple app that will resize the kitten image with an activity indicator. And that was all done automatically by using ResultInjection, as well as the observers.
There are a few other things that I wanted to talk about. Capabilities. Capabilities are a namespace in a protocol that can nest system capabilities. It provides abstracted methods for checking for and requesting permissions for some sort of system capability. ProcedureKit provides the Location capability, which I sort of mentioned before, which can check to see if you've got permission to access the user's location. As well as, if you don't, it will automatically prompt the user for that. It makes it very easy to check for those permissions without you having to write your own code. There's also the CloudKitCapability, which lets you ensure that you have access to certain CloudKit containers before you execute any of your code.
ProcedureKit also includes a large number of testing subclasses. These are various XCTest subclasses and various XCTAsserts methods. ProcedureKitTestCase is a base for that, and then the various subclasses for a lot of the major features of the framework. It has built in functionality to run, wait for, and check the result of procedures. So you don't have to write in your code any sort of non-asynchronous method of execution. It provides several classes, like I said, some of which are very important like the StressTestCase and the CurrencyTestCase, which allow you to run batches of sequential or parallel procedures, to ensure that your thread safety is complete. And you can run 50, 100, 1,000,000 of these at the same time, to ensure that your code is 100% reliable. It also provides several assertions already. So, things like AssertProcedureFinishedWith(out)Errors, or AssertProcedureCancelledWith(out)Errors. This gives you the ability to, like any other sort of XCTest code, write these assertions and wait for them to be synchronized.
Now, ProcedureKit is currently ongoing development of its 4.0, which is really just the first version of the framework called ProcedureKit. This is the full Swift 3 compatibility rewrite, and major refactors from version 3 of the Operations Library. It has separated some modules, so that features that are specific to certain OS's, or aren't, say, extension compatible, are separated out. But you can add them in yourself. The documentation still needs to be updated for the new Swift 3 API's, but it very well conveys the idea of the framework. The documenation has been very good, even if the code samples themselves may be out of date. You can open an issue on the ProcedureKit GitHub. Dan has been very good about answering questions about, "So, how would I do this thing," "How would I use what's already in the framework," and stuff like that.
So that's it, thanks a lot. My name is Jon Shier, I work for Detroit Labs. ProcedureKit is on GitHub and the demo code for this presentation is also on my personal GitHub, with each major feature as a commit. Thank you.