Making an API Request in Swift

Making an API Request in Swift

·

10 min read

In today's post, we are going to discuss how you can make requests to an API to retrieve data. For today, we'll use the PokeAPI to get data for different Pokemon, based on the name of the Pokemon you enter in your request. The PokeAPI is free and well-documented, so it's a great introduction to using API's if you've never interacted with one. Before we start writing any code, let's take a look at the data we expect to get back from our request.

Reading API Documentation

For this example, we are going to focus on the PokeAPI's Pokemon section. If you've never seen API documentation before, it can be pretty intimidating. Let's walk through it together. The first thing you'll notice in the Pokemon section is the type of request we need to make. Notice the blue GET and the url:

image.png

The documentation is telling us that we need to make a GET request and use the given url endpoint in order to get a valid response. Important note, if the given endpoint does not work as you go through the tutorial, change pokeapi.co to poke-api.vapor.cloud. If you read through my Introduction to Enums in Swift post, you should recognize GET. This was one of the cases we made in our HTTPMethod enum, along with put, post, and delete. We'll be using that enum later on in this post, so if you haven't already, read through the enums post so you have a better understanding of how enums work once we start using them here.

Now, if you look right below the request line, you can see all the data that is going to be returned from this endpoint for the pokemon, Butterfree:

image.png

If this kind of structure isn't familiar to you that's okay. This kind of data formatting is what's known as JSON or JavaScript Object Notation. This is a standard file format used by many API's to organize large amounts of data that is easy for people to read and understand. Notice, there's nothing here that you haven't seen before. We see a bunch of Int values, some String values, and a whole lot of [] and {}, which represent arrays and dictionaries, respectively. If you keep reading in the Pokemon section, you can see the descriptions for all the fields in the request. For this post, we are going to get the id, name, height, and weight when we make the API request.

Setting up our Request

Open up Xcode and create a new playground file. Add the HTTPMethod enum to your file:

enum HTTPMethod: String {
    case get = "GET"
    case put = "PUT"
    case post = "POST"
    case delete = "DELETE"
}

We'll use this enum later when we create our request. Next, we need a way to model the data that we are going to get back from the API. We can do this with a struct. Add the following struct below the enum you just added:

struct Pokemon: Decodable {
    let id: Int
    let name: String
    let height: Int
    let weight: Int
}

You've seen structs before from Part 2 of the Beginner Swift Series, so this shouldn't be anything new to you. What you probably won't recognize is Decodable. What is it? If we look at the documentation from Xcode, we can read that Decodable is "a type that can decode itself from an external representation." In basic terms, decodable is going to let our Pokemon struct decode data from an external source (the PokeAPI) and let us use it in our application.

Now that we have our Pokemon data model in place, it's time to make our request. To make our code more modular and reusable, we are going to create a NetworkManager class that will handle the network communication logic. This will come in handy for future posts when we'll build out a full Pokedex application. Go ahead and add the following class beneath your Pokemon struct:

class NetworkManager {
    static let shared = NetworkManager()

    let baseURL = URL(string: "http://poke-api.vapor.cloud/api/v2/pokemon")!
    var pokemon: Pokemon?
}

Don't worry too much about the first line in our class, as this will be more useful in our full app. Right now, all you need to know is that our shared property will let us use our NetworkManager throughout our application. Next, we can see the baseURL property. This is the alternative URL from the documentation that I mentioned before, minus the {id or name} section, which we will add in a moment. Finally, we use an optional pokemon property that will store the Pokemon that we fetch from the API.

Now, let's go ahead and add our fetchPokemon method to our NetworkManager. Add the following below your pokemon property:

func getPokemon(with searchText: String) {
    let requestURL = baseURL.appendingPathComponent(searchText)

    var request = URLRequest(url: requestURL)
    request.httpMethod = HTTPMethod.get.rawValue

    URLSession.shared.dataTask(with: request) { (data, _, error) in
        if let error = error {
            print("Error fetching pokemon: \(error)")
            return
        }

        guard let data = data else { return }

        do {
            let pokemon = try JSONDecoder().decode(Pokemon.self, from: data)
            self.pokemon = pokemon
            print(pokemon.name)
        } catch {
            print("Error decoding Pokemon: \(error)")
            return
        }
    }.resume()
}

There is a lot going on here, so let's break it down piece-by-piece. Let's look at the function declaration:

func getPokemon(with seachText: String) {

In this line, we are creating our function that will accept one argument, searchText of type String. When we call this function, we'll pass in the name of a pokemon that we want to get back from the API.

let requestURL = baseURL.appendingPathComponent(searchText)

var request = URLRequest(url: requestURL)
request.httpMethod = HTTPMethod.get.rawValue

In these three lines, we are defining the request that we are going to pass to the API. In the first line, we create a requestURL that consists of the baseURL we created earlier, plus an additional section for the searchText. We use the .appendingPathComponent method to add our searchText as an additional piece of the URL. If you were to print out the full URL with butterfree as the text, you would see http://poke-api.vapor.cloud/api/v2/pokemon/butterfree.

We construct our request using the URLRequest method, which accepts the url we just created as its input. Next, define that the httpMethod we want to use in our request is a GET, which we defined using our HTTPMethod enum earlier in this post.

URLSession.shared.dataTask(with: request) { (data, _, error) in

We call the dataTask method and pass in our request as the input. We then use the completion handler of the dataTask method to define what happens when our request is made. Notice, we have three values data, _, error. The first value is the data that we get back in our response from the API. The second value is one we don't care about in this example, so we leave it out by using a _. The third value is the error, which would tell us any error we get while making our request.

Now for some good news, the following sections will be very similar for almost all networking requests you make using Swift. So this code is highly reusable and is worth remembering.

if let error = error {
    print("Error fetching pokemon: \(error)")
    return
}

Here we are using if-let syntax to determine whether or not we received an error from our request. If an error does occur, we want to print the error message and then return out of the function

guard let data = data else { return }

Here we use guard-let syntax to determine whether or not we received any data from our request. If we don't receive any data, there is no point in continuing with the rest of the request, so we'll just return out of the function.

do {
    let pokemon = try JSONDecoder().decode(Pokemon.self, from: data)
    self.pokemon = pokemon
    print(pokemon.name)
} catch {
    print("Error decoding Pokemon: \(error)")
    return
}

After we've checked that there are no errors and we did receive data back from our request, now we can start to use that data. Inside our do-catch framework, we create a pokemon property that is going to accept the data from the request. Notice we use the JSONDecoder() object and the decode method to decode the data we got from our request. Remember how we made our Pokemon struct conform to the Decodable protocol? This is why. We told Xcode that we want our Pokemon struct to be able to receive decoded data from a network request. Next, we are assigning the pokemon to the class's pokemon property so it can be used at a later time. Then, we'll print out the name of the pokemon we just retrieved to make sure our request worked. Finally, if we encountered any errors during the decoding process, we'll print them to the console and return out of the function.

Lastly, don't forget to call the .resume() method on the closing } of the dataTask. This is what makes the function actually run, so if you don't include it your method will not execute and you won't see anything get returned.

Testing the Request

Whew! You've gone through a lot to set this up. Now it's time to test it. Go ahead and add this line to the bottom of your playground file, outside your NetworkManager class:

NetworkManager.shared.getPokemon(with: "butterfree")

You should see that when you run the playground, butterfree gets printed to the console. If you want to see the full request and data that was returned, click on the little box to the right of the line that says NetworkManager:

image.png

You should see the following pop up:

image.png

In this box, we see the request's URL and a dictionary of the data that we got back from the request: id, name, height, and weight.

Wrap Up

Congratulations! You've just made your first networking request using Swift. While this may seem like a lot, just remember, the majority of this code is reusable. In reality, you can use this code from app to app by simply changing the URL and the model that you want to decode. Making network requests follows this pattern:

  1. Set up your data model
  2. Specify your URL
  3. Construct the request
  4. Execute the request in a data task

Now it's time for you to practice what you've learned. Change which attributes you want returned from the request. Can you figure out how to return an ability from the abilities array? What are some of the other endpoints you can call from the PokeAPI? Remember, the best way to learn is to experiment.

Happy coding!