Writing Cleaner Asynchronous Code in Swift using PromiseKit
Sam Agnew at Swift Summit 2017
You might have heard of the infamous pyramid of doom, or pyramid code, which I like to call Hadouken code because a reused signature attack can just fire right through it, as you can see there. And there are several different ways that the community generally talks about making this stuff a little more clean. One of my favorites is Promises. Some language communities refer to them as Futures. But they're like the same thing. So, a Promise, I like to think of, is just something that wraps a task. Whenever that task finishes executing, it promises to pass the result onto the next step in the chain, for better or worse. You can see in the PromiseKit documentation, that you can see this little code example here.
It kind of reads a lot more nicely than just nesting a bunch of completion handlers inside of each other. It also has this syntactic sugar like, firstly you do this and then you do this. And then there's the always block, which will always run. And this catch block where, if they're any errors you don't have to check an error every step of the way. If you throw anything, they just all get caught in here, for any rejected promises. So you just do that all at once.
So, it's pretty nice to work with. To demonstrate how Promises work in Swift, I'm gonna be writing some code. Well, we're gonna be writing some code, actually. I say "we" because I'm gonna be live-coding and if I make any typos or anything, feel free to point them out. This will just be like a giant pair-programming session. And if you want access to this code after my talk, I actually wrote a blog post that runs through a lot of this stuff that you can read. It's a tutorial on what I'm gonna be speaking about with some of the code that you can copy and paste. If you want to get that, you can take your phones out and send a text message to this number on my screen. If you want my contact information or this tutorial from my talk, send a text message to 74310, the short code 74310, it's just a quick Twilio number I made to help me out with this. And you'll have my Twitter handle, which is @SagnewShreds, and the tutorial for this talk that you can just look at afterwards if you end up not paying attention or whatever.
Cool. So, I am going to hop over to Xcode and I have this project that kinda already exists. It's just super-basic, all it has is an image view that is going to contain gifs of Super Nintendo games, because I love Super Nintendo. I still have my childhood Super Nintendo hooked up in my apartment in New York and I play it on a regular basis. Why not? We're going to be using the Giphy API to grab gifs of Super Nintendo games and put them in this image view, and that's all this app is gonna do.
We're gonna start off by writing it the naïve way, without using Promises. I'm gonna go over this Giphy class that I made, it's just a wrapper around the Giphy REST API. I'm gonna make a function that grabs a random URL to a gif given a search query. I'm gonna call it "fetchRandomGifUrl" and that is gonna take, for a search query that will take a query, that query will be a string, and it will also have a completion handler. That completion handler will be a function, obviously. It will have ... Once I have this gif URL, that'll be in the form of a string. So I'm gonna have an image URL that is gonna be a string optional. And it'll also take an error object, just in case there are any errors, and that'll be an optional, as well. And it'll be a void function, not gonna return anything, just passing it on to the completion handler.
First thing I'm gonna do is I'm going to create a dictionary of parameters that I'm gonna be passing along in this http request. These parameters are just going to be basic things like the api_key, which you might notice is displaying right here. You might be thinking, "Wow, Sam, that's super-insecure! I could just take your stuff!" But that's just the default public API key, so steal it if you want, it's free anyway. I'm gonna be passing that API key and also the query that was passed to this function and a limit on the number of images that we want to search for. I'm just going to take the image limit that I set as the default, it's just 50.
Cool. So these are my parameters I'm gonna make the http request with. Then I'm gonna use Alamofire to make this request, if I can spell it correctly. So now I have a request and that is gonna take our URL, which I'm gonna use this giphyBaseURL and give it some parameters, which is my params dictionary. I'm gonna expect a JSON response in return. This JSON response is gonna have a response and I'm gonna start off by unwrapping that. I'm gonna throw this in an if let and if that goes well, fine. If it doesn't, I'm gonna pass my error object to my completion handler. This is kinda like ... this pattern is kinda like what node developers do a lot. Instead of throwing stuff, I'm just writing it this way to prove a point.
I'm gonna pass my ... The URL will be nil 'cause this means something went wrong and I'm just gonna pass the error from the response object. So if it unwraps correctly, then that's where I'm gonna do the fun stuff. I'm gonna call that completion handler. But first I want to, I'm gonna parse the JSON. So I'm gonna parse that. Then I'm gonna generate a random number, 'cause this JSON is gonna be just a big dictionary and I wanna grab ... It's gonna be an array of dictionaries, actually. So I'm gonna grab a random element from that array, which is just gonna be one gif image at random. I wrote a helper function already, as you can see at the bottom of my screen, to generate a random imager. I'm gonna call that and I'm just gonna give my image limit as the number that it's gonna go up to.
Next I'm going to try to grab an image URL from all this data. In my Chrome, I have an example request already up here so you can see the response JSON that we get back. The first key is data, so I'm gonna go in there. That's gonna be JSON of data. Then I'm gonna give it my random number because that's the array. After that, I wanna go to images and that's gonna be all the different types of images that it has. And I think I wanna grab the small one. So, where is that? Downsized. And then the URL from that. And that's gonna be the URL to our gif image that we are going to pass on to the next whoever, whatever function was passed into here.
This completion handler is gonna take this image URL, no error, and that's all we need to do for that. Now I'm gonna write a function to download that image from the URL that we got from the Giphy API. So I'm gonna make a function called, downloadGifImage, and that is going to take the image URL. I'm gonna call that, imageUrl. That is gonna be a string. It's also gonna take a completion handler, which is gonna be very similar to our other one, except instead of taking a string URL, it will take a data object. It will just take the data of the image. I'm gonna make that an optional as well, and an error object. Cool. And it's gonna be void again.
So in this one, I'm just gonna be sending another Alamofire request. I'm going to make a request to this image URL to try to download the gif data from it and I'm going to expect DataResponse this time. This will have my response in it. Now I'm gonna try to unwrap the data from that response, so that will just be ... I'm gonna throw in an if let, gonna make a variable and call it, imageData, and that is just gonna be the result from that response. If all goes well, I should just be able to pass that to my completion handler and it'll just do whatever with the data from there. I don't have to worry about it. If it doesn't, again I'm just gonna do this error checking stuff. I'm gonna pass the error to my completion handler. The error from the response ... So I think that's all I need to do to just download a gif from Giphy. I might've messed up, but I'll find out when I build everything.
Now I have those functions and I can call them in my view controller. I'm just gonna call out of my viewDidLoad. Here, I'm going to ... This is where I'm gonna write the completion handlers that I'm passing to the functions and everything. I'm gonna instantiate a Giphy object. I'm just gonna use all the default values that I set, 'cause those are fine for me for now.
The first thing I'm gonna do is ... Oh, thank you for telling me that I didn't use that variable ... So, first thing I'm gonna do is I'm gonna fetch a random gif URL from the string SNES, for "Super Nintendo Entertainment System." 'Cause there are lots of cool 16-bit games that have gifs on the internet. I'm gonna have this completion handler that is gonna take in a, I believe it was the URL string. The URL string and an error object. In here, I'm going to see if this URL string is valid. I'm gonna take that and if it's good, I'm going to call the next function in my chain. But let me not forget to do some error checking. If the error is there, I want to just print the localized description of that error.
So from here, I can do stuff with this URL to the image. I have the random image and now I want to download that. So I'm gonna download that with this image URL. Oh, cool, I have water. This conference rules! So I'm gonna download that with this image URL and then I'm gonna give it another completion handler. I think this completion handler will take the data. I'm gonna call that, image data. From here I'm gonna do basically the same thing. I'm gonna make sure this image data isn't nil. Also gonna do this error checking stuff. I think you can see where this is going. It's these nested completion handlers. Actually, I'm just gonna copy and paste this right here. It's gonna be the same error-checking code, actually, so that's cool.
Now that I have this image data, I can attach it to my snesGifImageView and that's all our app is gonna do. It's a pretty sweet app. I'm gonna call my attachImage function, that's just a helper function I wrote down here, to attach that imageData to this imageView. And as long as I didn't make any crazy typos, this should be all I need to do to get some Super Nintendo images. The build might fail, I'll fix a typo or something. There we go. Cool. Always happens. "Did I mean parameters?" Did I? Yeah, I guess so. I guess I should've capitalized that. Nice. Oh, the colon! There you go. You said, "colon?"
Oh! Oh, man! Yeah, I'm used to writing Python. So, this initializer, what does this say? Oh, this needs to be a string. I need to grab the string from that, not the object. Cool. It looks good. Yes! Build succeeded!
So just a couple of typos. So if all goes well, you should see a sweet Nintendo image once that loads. Wow, that is not a sweet image, but it did work! That is a ... I'm gonna open that app up again, hopefully we get a better one. There we go! Super Mario World, that's a classic. And every time we close the app and open it again, we get a new image and it's just ... Oh, look, Super Punch-Out. Cool.
So that's cool. That's the coolest app ever, right? But look at this, this is ugly. Look at this code. This is gross. I don't want this code. I want the app to do the exact same thing 'cause I love looking at Super Nintendo images, but I don't want it to look like this. So I'm gonna comment all that out and just start over using Promises, hopefully make it more readable. But first I need to refactor some of my code in this Giphy class because we're not going to be taking completion handlers anymore. So I can just get rid of that.
Instead of going void, we're gonna return a Promise. This Promise is gonna wrap around a string value, and I'm not going to have to change my code too much to refactor it. I'm gonna keep this line where there are parameters, but I'm gonna wrap this Alamofire code in a Promise. This Promise is just gonna have two arguments in it. It's gonna take a function called, fulfill, and a function called, reject. Fulfill is what happens when you wanna fulfill the Promise. If everything goes well, you wanna pass the value to that function and it will just pass that to the next function in the chain. And reject is when there's an error you just pass your error to that reject function, and it'll go straight to your catch block.
So instead of having this completion handler, I'm gonna replace that with a fulfill, to fulfill our promise. And over here I'm gonna reject and just give it that error. I don't believe that takes an optional, so I'm gonna force unwrap it and seize the day right here. 'Cause probably won't get an error in this talk anyway, not that kind of error, at least. I'm gonna do the same thing down here. Gonna replace this completion handler and I'm gonna have this return a Promise instead. This one will be of type "data" because it's the image data. Gonna do the same thing, just return a promise down here with fulfill and reject. Gonna cut and paste this Alamofire code and just have it inside this Promise. And here I'm gonna fulfill the Promise with the image data and I'm gonna reject it with this error that I'm gonna force unwrap again. The error stuff is mostly there for you to see how error-handling looks with promises in comparison.
Now I'm going to go to my view controller and it's like, "Cool."I can actually use those Promises now and not have to write code like this." So I am going to ... I'm gonna start off with some syntactic sugar, I'm gonna use that firstly keyword just 'cause it looks nice. It's not necessary, but I feel like it reads nicer. First, I'm gonna call the Giphy.fetchRandomGifUrl for my Super Nintendo search query. Then I'm going to ... The next block of code in this chain, after that Promise is fulfilled, will take this imageUrl, and I don't need to do error checking in this 'cause I'm gonna have that all in one catch block later. So now that I have my imageUrl, I'm going to call my downloadGifImage function with that imageUrl and then ... I love saying that, it's just reads nicely. "And then."
Then I'm going to have the image data in this next block and I'm going to call my attachImage function, and I'm gonna attach that imageData. Then I'm gonna catch any errors that might come up. So, I'm gonna have an error there and gonna do the same thing as before. I'm just gonna print the localized description, if that's a thing.
Oh, the "in." Thank you! See? We're pair-programming! I love pair-programming! Yeah, I'm looking at this, I'm like, "Man, why is this autocomplete not working?" So this is actually all we need to do to do all that same stuff. This should be an equivalent code block. So I'm gonna run this app and hopefully I didn't mess anything up. If all goes well, the app should stay exactly the same and nothing should change. So the build succeeded, that's good. Let's see if we get any Super Nintendo gifs. Oh, nice! I think that's from Final Fight. That's a fun game. I wanna see what else I get. Let's open it one more time. Is that? Oh, that's ClayFighter. Nice. That was a mediocre fighting game. Cool.
So you see that's this block of code is all you need to do the same stuff as down here. I think it looks a lot nicer. Now I know you might be thinking, "Hey, Sam, didn't you just write that code very messily to prove a point?" And maybe you're right, but I do think that this looks pretty nice. The "firstly," "and then," and the catch. Promises are pretty cool and they read nicer, I think.
If you want any of this code afterwards, again I have this ... Don't wanna do that ... I have this phone number you can text: 74310. Just gonna put that on the screen again in case anyone missed it before. If you want a blog post that just explains basically what all I went through. You'll also have my contact information and all that. My name is Sam Agnew, I'm a developer evangelist for Twilio, and if you have any questions you can find me outside after this. I'll also be at the Twilio booth for the rest of the day handing out these T-shirts, which I still have three boxes of, so please come grab one if you didn't get one already. Thank you.