Transcript for:
CS50 iOS Track 3: Dynamic Data in the Pokedex App

TOMMY MACWILLIAM: So now that we've built out the first version of our Pokédex app with that static list, let's take a look at how we can bring in dynamic data from the internet to use inside of our app. So to do this, we're going to use something called an API, or an application programming interface. And you can think about an API as basically some code that someone else has written that's designed in a way for you to use it, as well. So in this case, we're going to be going to a website and making requests to that website and getting back data about Pokemon. So the format that we're going to get our data back in is a format called JSON. And JSON stands for JavaScript Object Notation, basically a format that started off as part of JavaScript, which is another programming language you might use to write a website. But it's really become this common format to exchange data between a client and a server. And so this happens to be what the API that we're going to use returns. So here's what JSON looks like. You'll notice it looks a little bit actually like a Swift dictionary, and that's no accident. So JSON will start off with this curly brace and end with this closing curly brace, and everything inside of that is basically a dictionary. So we have keys over here on the left. In this case, we have three string keys, one called "course", one called "tracks", one called "year", then a colon to separate the key and the value. And then after the colon is the value. So you'll notice here that we can have a string value like "cs50". We could have an array of strings, like "mobile", "web", and "games", or we could have numbers, like 2019. In the case of our API, we're mostly going to be dealing with strings and lists of strings, but JSON has a few other data types that you might run into. So before we get into that, let's cover a couple more Swift syntactic constructs, just to make sure we have them. The first is a construct called Try, Catch. So there's this notion in Swift called exceptions. And an exception is something that happens when, as you've guessed, something goes wrong under the hood. So here's the syntax of what that might look like. First, you'll have a block that starts with do. And when you have this sort of do catch block, this basically tells the compiler I'm going to call a function or a method that might trigger an exception. And when that exception happens, I want to be able to catch that and break out of this code and handle that error. So my next line, this left half looks like something we've seen before, just let result for creating a variable. Then on the right side here is where we're doing something that might throw an exception. So you'll see this try line. And this tells the compiler the function I'm calling right here might throw an exception. And if it does throw an exception, we're going to break into this second block here, this catch block. And here you can see this catch let error. This says I'm going to catch this error. And in this case, I'm just going to print it. But if nothing goes wrong, then I'll just never enter this catch block. So the next piece of Swift syntax that we're going to take a look at is called a closure. You'll often also hear these called anonymous functions or in line functions. But what a closure is is basically a way to define a function right in the middle of your code and then use it. So let's look at an example. So suppose that we're creating a new variable and we're calling this variable reversed. And our goal is to take a list of strings. This time it's called names. And we want to sort them in reverse order. So there is a method on our list of strings called sorted. And it's going to take one parameter, which is a closure. And so to walk you through this syntax, we have this open brace and then we have this open parens. Inside of the parentheses is where we're going to define the parameters to our closure. So this closure or this function just takes two arguments, one called s1, one called s2. And they're both strings, which you can see from the types. After that, it's that same right arrow that we saw before. And that just specifies the return type of our closure. And so from this declaration, what we're basically saying is I'm going to create a function. That function has two arguments. They're both strings, and this function returns a Boolean. After that Boolean, we have this keyword in, and then we specify the body of the function. So this function is really simple. We can just say if s1 is greater than s2, that's going to return true or false. And this is the comparison function that's going to be used to sort our list. So as you can see here, we could have done something like create some comparison function somewhere else in the file, and creating that function would be the same syntax you've already seen, func and call that compare, have it take two arguments, return a Bool. But there's this nice shortcut where we know if we're only going to use this function once and we're using it right here, a closure just allows you to define it right in line, which makes your code a little bit easier to read and more organized. So that's all this function is doing, and we're going to see one of these in our API in a bit. Another concept that we'll see shortly is this design pattern called delegates. This is really common throughout the iOS API. You'll see the word delegate a lot. And a delegate is basically a way of attaching event handlers to some other object. So suppose you have some object and it needs to respond to some data that comes in and you want that object to notify somebody else, that hey, I just got some data and I need you to respond to those changes. So that object is called a delegate, because what you're doing is you're delegating the logic for handling some change to a different object. And so you'll see this in a bit. So let's walk through an example. And what we want to do is use this API. It's called pokeapi.co. And this page is kind of nice, because it shows you what the API is going to do. So we have this little sandbox here, where every API URL starts with https://pokeapi.co/api/v2, so version 2. There's probably a version 1 somewhere. It doesn't matter. And then if I enter in the rest of my URL, they suggested Pokémon and Ditto, who is just a Pokémon, then you can see what the API is going to give us back. So you can see that this is a JSON object. There's things like abilities and forms, and basically a whole bunch of information about this Pokémon. If I come down here, there's also this up option to view the raw JSON. So if I click on that, then this is literally what the API is giving me back. And this looks just like the JSON object that we saw before. It starts with this brace. All of our keys are strings. They have quotes. After the quotes is a colon, and then could be anything. In this case, it's a list of objects. Here we have a number, and so on. And so this is the API that we can use to start grabbing some Pokémon data. So let's take a look at how we would do that in iOS. So we're back in Xcode. So let's jump back to our first view controller, where we defined that static list of Pokémon. So the first thing we're going to do is remove the right hand side of this expression, because we're no longer hard coding this data. Instead, we just want to have this be an array. But now after we fetch the data from the API, we're probably going to change the value of this so it's not an empty array forever. So let's make this a var. And then let's also tell the compiler what type this is going to be. And ultimately, we're going to have that same array of Pokemon that we had before. So now to actually fetch that data, we want to fetch the data as soon as somebody opens our app, so as soon as this view is loaded. And we saw this last time. We have this method, viewDidLoad-- and I can use autocomplete to get it for me. And remember, this method is called whenever the view is loaded for the first time. You don't call this method. iOS going to call this method for you. You just fill in the definition of what it should do. So remember last time that in this case, we want to make sure we call the super classes method. So we want in case that the UI table view controller defines viewDidLoad and does some important stuff, we want to make sure that we do that stuff because remember, we're overriding that definition with our own. So the first thing we want to do is decide what URL that we want to use. So let's come back to our API, and let's go to the documentation. This time, let's just go to the most recent version. And here we can see a lot of information about what types of information this API can give us. So if I start over here at Resource Lists you will notice here that it says that calling any API endpoint without a resource ID will return a list of available resources, which is great because we're looking for a list of Pokémon, so that's perfect. And it also lets me know that I can specify this parameter called Limit to tell me how many results I want to limit my search to. So that's exactly what we want. We just want to take the first 151 Pokémon from the list of all Pokémon. So now that we know that, let's come back to this little sandbox they provide. And rather than requesting to pokemon/ditto, let's just request a Pokémon, since they said that's what you need to do. And then let's add that Limit equals 151 and hit Submit. So now if I expand this results, this looks really good. This looks like exactly what I want. I have a list where everything in this list has a name, and it looks like it also has a URL where I can go get more information about that resource or about that Pokémon. And so let's also save that to make sure we have it later. So the first thing we want to do is write out a model that corresponds to this API. So let's jump back to Xcode and open up our model file, this Pokémon struct. So here we see that every Pokemon has a name and a number, and that's fine. And notice that this API also gives you back URL. So let's add that to our data, as well. So let's say that every Pokémon has a URL. And this URL looks like it's a string, so we'll just use a string to represent that. So now our model has everything we need to use the API. So we're back in our viewDidLoad function. So now let's start loading this data. The first thing we want to do is create a URL. So let's say let URL equals-- and there's a URL class that's given to us in iOS, and there's a whole bunch of different ways to construct a URL. We're going to use a simple one, which is to just take a string. And so back over here, if you remember, we wanted to take this Pokémon at api/v2, so we'll just copy/paste that. And we wanted to call Pokémon, and then we also wanted to specify a limit of 151. So now that we have our URL, let's do something with it. The first thing we want to do is make a request to this URL to get that data. So to do that, we're going to use a built in class in Swift call URL session. So URL session is a class that's provided for us that has a bunch of methods in it targeted at getting data from URLs. So the way to do this is you'll say URLSession.shared, which is basically just an instance of URL session that you can use. You don't have to create your own. And then we'll say dataTask. And so a data task is basically just this method that you can call that's going to go fetch the data from that URL and give it back to you in a string that you can use. So it looks like it takes a URL, and we know that we can just pass this URL. And then we have a completion handler here. And this is going to be our closure. So this is going to be defining a function that's going to be called when the task finishes. So from the looks of it, there are a few different parameters we can use here there's a parameter with data. There's a parameter for response, and then there's an error. And you can see from the autocomplete that this function doesn't return anything, just something that gets called once the request is done. So let's call this data, response, and error. And what we've just done here is we've defined our closure. Here are three parameters, and then we have this keyword in to separate the parameter list from the body. But you'll notice here that the compiler is giving us an error. So if I hit Build here and I check this out, so it looks like when I created this URL, this is returning an optional. And when we call data tasks, we need to specify a URL, not a URL optional. So let's just make sure that this works. So we can say something like guard let u is URL, and if not, we'll return from our method. As before, we could just use an exclamation point here as well, as long as we just somehow get from optional to URL. So now we've done that. So now we have a request to this URL, and we have-- looks like we're going to get back some data. So now let's write our closure. From that autocomplete, it looks like this data is also an optional, so let's just make sure that we got back some data and unwrap that optional. So we'll say we'll just let guard let data is data, else will return. You noticed you can actually use the same variable name on the left and right hand side of this? It's kind of nice, so you don't have to keep renaming variables. So now that we've made sure that our data isn't nil, let's convert that JSON data into a Swift object. So first, let's go back to that API and remind ourselves what the data that's coming back down to us from the API looks like. So here's this API response, and it looks like I'm getting back a JSON object with a few different keys, one called count, one called next, previous, but the one I really care about is called results. And it looks like the type of this results key is an array of Pokémon objects. So let's first create a struct to represent this API response. We'll come back to Xcode into our Pokémon model. So we have a struct representing each element in that list, but we don't have a struct representing the list itself. So let's create a new struct. We'll call it PokemonList. And this struct is only going to have one field, we'll say results. And that's going to be a list of Pokémon. So now let's use this new struct to decode that JSON into an object. If we come back to our view controller, let's create a variable called pokemonList. And now we're going to say, use this new object called a JSON decoder. We're going to create a new instance of that, and then we're going to call this method called decode. And it looks like this method takes two arguments. One is a type to decode into, and the second is some data to decode. So let's use that type we just created, that pokemonList, and there's this little weird syntax here where to reference the type of that struct, I'm just going to say .self. And now the second parameter, that's just going to be the data that came down from the API. That makes sense, because that's what we want to decode into an object. So now you'll see here that Xcode is letting me know that this instance method requires that pokemonList conform to decodable. So decodable all is just another protocol that's defined somewhere in a UI kit. And any class or struct that conforms to this protocol can be serialized or de-serialized using JSON. And all that means is that we can convert back and forth between a Swift object and JSON data, which is just a string representation of that object. So all we have to do is take those two structs that we wrote before and make them conform. So I just have to say colon Codable on both of these things, and now I can go back and forth between JSON. So OK, we've got one more error. Let's see what it is. It says call can throw, but it's not marked with try. So here is where that try, catch block is going to come in. Let's say that some JSON came down from an API, but it was malformed. Maybe there is a typo or something else that made it not valid JSON. So if we try to decode some JSON that's not valid, we're going to get an invalid object as a result of that. And to keep going with our program with an object that doesn't make any sense, that's not something that we really want to do. So let's use a try, catch block to recover from this error. So. just like we saw, we're going to put this inside of a block that says do. We're going to mark this call that can throw an exception with try. And then we're going to have a block to catch the error. So let's say catch let error, and let's just print it out. If this were a real app, you'd want to display some sort of error message to the user, but for simplicity, let's just use string interpolation here to print out that error. So now let's build. Now it looks like we've succeeded. So let's run this app and let's just make sure that we don't run into any errors. So let's come up here and click Run. And OK, it looks like we did run into an error. We have this key not found. OK, so it sounds like there's some key that's not being found somewhere with a value of number. So that makes sense, because if you remember our original Pokémon struct had that number field that we were using before. But if we look at our API response, there is no field called number. And so when we're trying to decode this JSON and create a struct, we're looking for there to be a number field. But there just isn't one, and so that error is thrown. So this is a good example of an error we need to recover from, because we declared in our struct there has to be a field called number. And the JSON doesn't have one, so it doesn't make sense for our program to continue at that point. So for now, let's just remove that field number from our struct. Let's come back to our model, remove that field. And now we also have to make sure we remove any usages of that field. So let's jump back to that Pokémon view controller which was using it, and let's just comment out this line for now. Let's rebuild. Looks like we succeeded. Let's try running the app again. OK, this time there was no error, so it looks like our API request went through successfully. But we haven't displayed anything yet, because we haven't done anything to actually use that response yet. So let's do that now. Let's jump back to our view controller. So recall that we've defined a field in our view controller called Pokémon, and that's an array of Pokémon. And that's where our table view is getting its data from. So let's use this PokemonList variable and say that our Pokémon field is equal to our pokemonList.results. And you can see here from the autocomplete that our types match, as expected. The type of the Pokémon field is a list of Pokémon, and that type of the results field is also a list of Pokémon. So great, now we've assigned the variable we're using for our data model to be equal to the variable we got back from our API. So already, we're getting this helpful error message from the compiler. And you'll see here that the message says that we need an explicit self. So as you've noticed, every time I create a field in a class I'm not typing self.something. I'm just referencing the field. Most of the time, you can do that in Swift. And it's a little nicer, because it's less typing. But we're inside of a closure, remember. We're inside of this URL session shared data task, and then we're writing this function that's going to be called when the task finishes. And when you're inside of a closure, Swift just requires that you prefix any field with self, just to be a little clearer about what you mean. So all I have to do is say self.pokemon here, and we're good. So let's try rerunning the app, now that we've assigned this value to the results of the API call. OK, we're still empty, so what's happening here.? Well, we've assigned the variable, but we haven't notified our table view that it has new data available. So to do that, all we have to do is call a method on our table view called Reload Data. And what that's going to do is say I'm going to look back at that data source and just reload everything. So every time you change the data that's backing a table view, you just have to remember to call and reload data. So let's do that. I'm going to need self again, because I'm still inside of this closure, table view, and we'll call reload data. All right let's run this and see what happens. All right, a new error this time. And it looks like by the looks of it our app actually crashed here. But this nice purple line showed us exactly where the problem happened, so let's just expand this. So this says UITableView. reloadData must be used from the main thread only. OK, I'm not really sure what that means, so let's just click on this question mark to see a little bit more about this. So OK, so this just opened up the documentation right inside of Xcode. This is great. And hey, it looks like this code example is exactly what we were doing. We're using a URL session, a data task, and we're trying to update something on the UI. So what's happening here is when I call this data task method, what I'm actually doing is launching a new background task. But in iOS, tasks that are launched in the background are not allowed to update the UI, because the UI is going to be running in the foreground of the app. So all I need to do is say, OK, I've got some code running in the background. I have this line that I really want to run in the foreground. And to do that, I'm just going to follow what this documentation says and call DispatchQueue.main.async. And that's just going to jump me right back to the foreground where I'm allowed to make UI updates. So I'm just going to copy this right from the documentation, and I'm going to surround just this reload data call with this block, because this is the only line that's actually updating the UI. I don't need to put this around everything here. So now that I have that, let's restart our app. All right, so now we have the result of our API call in our app. We see here we have a really long list of Pokémon. It looks like it has all of the original 151. But one thing is a little different. And that's it. All the Pokémon names are no longer capitalized. And I kind of like the way that looked, so let's take that data from the API and capitalize it before we display it inside of the cell. To do that, let's create a new method called capitalize. So we're going to say func capitalize. Our method is going to take one argument, just some text to capitalize. The type of that argument is a string. And we're going to return the capitalized text. So our return type is going to be a string. So let's think about what we want this method to do. Given some string, we basically want to take just the first letter, capitalize that, and then append on the rest of the string, since that will result in just the first letter being capitalized. So there's some handy methods in Swift already defined for us that we can use for our capitalize method. So the first one is called prefix. And prefix is going to take a string and give us a substring depending on how many characters we need. So if I say prefix 1, that's going to give me back a string just with the first character. And that's exactly what we wanted. And then there's another method on string called uppercased. And that's going to do just what you think. It will take a string, and uppercase every letter. So that's the first letter of our string. So now let's add the rest of the string. So now I just need everything except the first letter, and I don't need to do anything to it. To do that., I can say text. dropfkirst. and this is exactly what it sounds, this string just without the first letter. And that's really handy, because that's exactly what I wanted. Lastly, the compiler is reminding us that we just have to return this value. And now if we build, we don't have any errors. So let's just easily use this new capitalize method. Let's come down here to our cell for row at index path, and rather than just displaying name, let's display capitalize of name. So now let's try rerunning the app again. All right, now this looks what we want it to look like. All of our Pokémon names are now capitalized, just like they were before. So now that we've used the API in our first view controller, that table view, let's also utilize the API in our second view controller that's displaying some information about Pokémon. Right now when we hardcoded that list, we just hardcoded the name and the number. But there's a whole bunch of information this API gives us that we can display. So let's jump back to our API to remind ourselves what that looks like. Let's just try giving an example URL here. Looks like it's this first API response gave me back this URL, so let's just see what happens if I were to load that. OK, so here is our resource for just this one Pokémon. There's a whole bunch of fields here. We've got strings. Some are arrays. Others are objects. And it has this ID. That's the number that we're using. It has the name. That's just the name. And just for simplicity, let's say that we just want to display the types of each Pokémon in our Pokedex, kind of a classic Pokédex thing to do. So we wanted to use this types field. And it looks like this types field is an array, and each element of that array is an object. And inside of that object, we have an integer, called Slot, and then we have another object, called Type, with two more keys that are strings. So in order to use this API call, let's model this data with structs, just like we did before, so that we have a Swift model that reflects what we're getting back from the API. So let's jump back into our model file and create some more structs. So the first struct that we can create is, let's call this a Pokémon data since it has a bunch of data about Pokémon. We know that we're going to be decoding JSON here, so let's just have it conform to codeable right off the bat. And now there's a few fields that we care about. We care about this ID field because that's the number. And we also care about this Types field. But it looks like this type is going to be an object of its own, so let's sort of work backwards from the bottom up. Let's first define a struct to represent a single type. So we'll say struct Pokemon type needs to be codeable. And that has two things. It has a name that's a string and it has a URL, also a string. OK, so that's our type object. Now let's look at this other containing object. It has a key called Slot, that's an integer, and it has that Type Key, so let's just call this a type entry. So we'll create another struct called Pokémon Type Entry. It's gotta be codeable. And it has a Slot, that's an integer, and then it has a list of types. And so there, we can use our new Pokémon Type. So lastly, we have this types field and we're back up to the root of that API response. So we can say let types be a list of Pokémon type entry. So now we've created these three Swift structs that map to the API response. We want to extract this ID field and this Types field, and then inside of each types-- we have a typo, actually. This should be type instead of types. We have a Slot and a Type. And then each Type has a name and a URL. OK, so now that we have our data model, let's make another API call, this time from our second view controller. So let's jump back to that first view controller and let's just copy what we had before inside of viewDidLoad. So I'm just going to copy all of this, and we're going to paste this into our second view controller. We were doing some stuff before here, so let's just remove that since we're going to redo it using the API. OK, so the first thing we want to look at is the URL we want to request. This time, we don't want to request this list URL, we want to make requests based on the Pokémon object that was passed in. So to do that, we can just say pokemon.url. And so now, this is going to be dynamic, depending on what the user tapped on, we're going to get a different URL pass to this second view controller. OK, so this URL session data task, that's all the same. We still want to make sure our data is not nil. That's the same. But how we're parsing it is going to be different. So this time, rather than having this Pokemon list, I'm now going to have some Pokemon data. So we're going to change this to be Pokemon Data, and we're going to use that new class that we just wrote. Now that we've decoded this Pokemon data object, let's use some of these fields. First, we can set the name of the Pokemon based on the object that's passed in. We actually don't need to change that. The API also has the name, but we already have it from that first view, so no need. And remember, we're inside of a closure, so we need to say self. Next, let's set the number for the Pokemon. That came from our API, remember. We now have numberLabel.txt, and we can say pokemonData.id. And just like before, this is an integer, so let's use that string format again. Let's say format:"#%03d" and pass in the ID. And here we have another helpful warning, we forgot self here. Let's just fix that. OK, so now we've set those two labels we already have. So to display that type information we just got from the API, let's just create a couple more labels inside of our storyboard. Let's jump back here, just like we did before. Let's come to the top right, add in a few labels, and let's give some place holder information. We can say Poison. Let's copy and paste. So one type is poison, one type is grass, and those are some place holder data. Let's just expand these to make sure that text is going to fit. On the right hand side, we're going to center that. Same over here, let's center this. OK, so now we have our two new UI labels. Just as we did before, let's create outlets for those labels. We'll have one IV outlet called Type 1 Label, that's a UI label. And we'll have one more IV outlet called Type 2 Label. And you'll notice here that the circles over to the left, they're not filled in. And that's indicating to us that we haven't actually connected these to anything on the storyboard yet. So that's kind of a handy reminder. So let's do that. Just as we did before, we're going to open up our scene, we're going to hold down Control, drag over to the labels on the right, select Type 1 Label, Control drag all the way here to the label on the right, select Type 2 Label , and now our outlets are hooked up. So now let's use them. We know that Pokemon data has a field called Types. And this is an array, so let's iterate through them. Let's say for typeEntry in pokemonData.types. Now we can use that Slot and Type object. So we can look at this value of that Slot key and use that to determine which label we want to set. So for example, we can say if typeEntry.slot is one, then we can self.type1Label.text is typeEntry.type. And it looks like here I just happen to have a typo. I made this a list instead of an object. So let's just jump back to our model, and this should just be a single pokemon type. That makes sense because each type entry has a key called Type and that type is an object, not a list. So now that we've fixed that typo, we can say, typeEntry.type.name. Similarly, we can say, else if typeEntry.slot is 2. Then we have type2Label.text is typeEntry.type.name. OK, so it looks like we're now using all of the fields that we defined, and we're setting some things in the UI based on those fields. So let's try running our application. All right. There's our table view. Let's try tapping on a Pokémon. And OK. It looks like we got that same issue we had before. Not only does reload data update the UI, but we're trying to manipulate some labels here, and that's also going to need to happen on the foreground. So to do that, after we parse this JSON, let's just put everything in here back on the main thread by saying, DispatchQueue.main, where I can make UI updates, call this async method, and let's just indent everything here and close our brace. So now let's rerun the app one more time. Here's our list. Let's tap on my favorite Pokémon. And it looks like we're now loading this data from the API. Every Pokémon I tap is going to have different data coming in from the API. So one last thing-- some Pokémon have two types and others just have one. And it looks like this is a little messed up because I left in some of that placeholder data. I happen to know that Charmander isn't a grass type. That wouldn't make any sense. But in my storyboard, I had this placeholder data just to kind of remind myself what the view looked like. So if you're going to do that, make sure you clear out any placeholder data when you're loading in data from somewhere dynamically. So to do that, all I'm going to do is just clear out these placeholders by saying, type1Label.text is empty, type2Label.text is empty. Since depending on what the API sends back to me, we might not have values for those. So now if I run the app again, now the Pokémon that only have one type show one type, and the Pokémon that have two show two types. So that's it for our Pokédex app. We've replaced that hard-coded data with dynamic data that we're loading in from an API. And that API has lots more information that we can leverage to create a really cool app. Now, when you're making your own apps for your final projects, you can look at all of the different APIs out there, and bring in data from different sources and display it inside of your app. Now, in the next video, we'll take a look at another type of app, this time using images.