Bootcamp, iOS Development, Lambda School, Projects, Swift

Building an iOS app using an API

Here’s my plan to build an app that uses the cocktail DB API which can be found at https://www.thecocktaildb.com/ . I choose this API because it’s free, I don’t plan on posting this app on the Apple Store so they allow free use for educational purposes. It also has different endpoints and this is great practice.

Before we start building we want to have a general idea of what we are making. The API allows us to get a random drink. So I think this will give us a good idea of how to set it all up and make sure we are calling the API correctly.

Build the Project

To start we need to build a new project in Xcode. We will call it cocktailMaker.

Since the app is going to be very simple at first I want to keep the layout simple so, for now, it just looks like this.

Screen Shot 2020-03-24 at 8.46.44 PM

With the layout complete we can start working with the API and getting the code ready.

The API results that we are interested in are the

  • Drink Name
  • Image
  • Ingredients
  • Instructions

Model

We are going to create a file named “CocktailResults.swift”. We are going to have a variable for each item we want to import from the API, the API itself has so much more information than what we need, but luckily Swift has the Codable struct that allows us to just import the elements that we need. I don’t like the variable names that they used, luckily we can create our own. We do this with an enum and have it conform to the CodingKeys protocol.

One more thing to keep in mind if we look at the API resultsScreen Shot 2020-03-25 at 6.53.14 PM

we notice that we get an Object and the results are in a dictionary with drinks as the key and the actual results are in an array. To make sure we decode the data right we will add another struct to represent the highest level of our data. You can see in the code below that it’s inside an array. The reason for this is because although we are only getting one result later when we add more features we will be getting more than one result, so we may as well get ahead for now.

My final file looks like this:


//
// CocktailResults.swift
// cocktailMaker
//
// Created by Claudia Contreras on 3/25/20.
// Copyright © 2020 thecoderpilot. All rights reserved.
//
import Foundation
struct CocktailResults: Codable {
let drinkName: String
let imageString: String
let ingredient1: String?
let ingredient2: String?
let ingredient3: String?
let ingredient4: String?
let ingredient5: String?
let ingredient6: String?
let ingredient7: String?
let ingredient8: String?
let ingredient9: String?
let ingredient10: String?
let measurement1: String?
let measurement2: String?
let measurement3: String?
let measurement4: String?
let measurement5: String?
let measurement6: String?
let measurement7: String?
let measurement8: String?
let measurement9: String?
let measurement10: String?
let instructions: String
enum CodingKeys: String, CodingKey {
case drinkName = "strDrink"
case imageString = "strDrinkThumb"
case ingredient1 = "strIngredient1"
case ingredient2 = "strIngredient2"
case ingredient3 = "strIngredient3"
case ingredient4 = "strIngredient4"
case ingredient5 = "strIngredient5"
case ingredient6 = "strIngredient6"
case ingredient7 = "strIngredient7"
case ingredient8 = "strIngredient8"
case ingredient9 = "strIngredient9"
case ingredient10 = "strIngredient10"
case measurement1 = "strMeasure1"
case measurement2 = "strMeasure2"
case measurement3 = "strMeasure3"
case measurement4 = "strMeasure4"
case measurement5 = "strMeasure5"
case measurement6 = "strMeasure6"
case measurement7 = "strMeasure7"
case measurement8 = "strMeasure8"
case measurement9 = "strMeasure9"
case measurement10 = "strMeasure10"
case instructions = "strInstructions"
}
}
struct DrinksResults: Codable {
let drinks: [CocktailResults]
}

Notice that our ingredients and measurement are saved as optional Strings. That’s because if you look at the JSON file not all of the ingredients might be used.

MY ERROR: I made an error in the first file I created and I misspelled instructions. My project kept crashing and I spent too much time trying to figure out why. It turned out to be a misspelling. So check the spelling and make sure it matches the JSON file. 

 

Controller

Now that we have a place to store our results we want to create a file where we will be doing our network requests. Let’s create a new file called “CocktailResultController.swift”.

This file will have all the code we need to make the network request.

The first thing we need is a place to store our drinks so let’s go ahead and create a variable for that

var cocktailResults: [CocktailResults] = []

The documentation for the API doesn’t come out and say it specifically but if we look at the request URLs we can see that they all have https://www.thecocktaildb.com/api/json/v1/1/ in common so this is our base URL and so that we don’t have to type it in all the time we are going to store this in its own variable. 

let baseURL = "https://www.thecocktaildb.com/api/json/v1/1/"

I know that I want to implement a few of the features of the API so I am going to create an enum where I can store all my different endpoints. This way it will be easy to find and change in the future It will look like this.

enum Endpoints {
        static let baseURL = "https://www.thecocktaildb.com/api/json/v1/1/"

        //This is where we store the different endpoint cases. They are named based on their functionality
        case getRandomCocktail

        var stringValue: String {
            switch self {
            case .getRandomCocktail:
                return Endpoints.baseURL + "/random.php"
            }
        }

        var url: URL {
            return URL(string: stringValue)!
        }
    }

The enum might look at bit weird at first but it’s pretty easy to understand when we call for an URL we will do

Endpoints.getRandomCocktail.url

This will combine the base URL with the endpoint needed for the random cocktail and convert it to a URL for us. The more we use it the more comfortable we will feel with it.

We will need to create a function to get the request. We don’t have to pass in any information, we do need a completion handler to handle the data once it comes back.

func getRandomCocktail(completion: @escaping (Result<DrinksResults, NetworkError>) -> Void) {

        //Build up the URL with necessary information
        var request = URLRequest(url: Endpoints.getRandomCocktail.url)
        request.httpMethod = HTTPMethod.get.rawValue
        
        //Request the data
        URLSession.shared.dataTask(with: request) { (data, response, error) in
            guard error == nil else {
                completion(.failure(.otherError(error!)))
                return
            }

            guard let data = data else {
                completion(.failure(.noData))
                return
            }

            //Decode the data
            let decoder = JSONDecoder()
            do {
                self.cocktailResults = try decoder.decode(DrinksResults.self, from: data)
                completion(.success(self.cocktailResults!))
            } catch {
                completion(.failure(.decodeFailed))
                return
            }
        }.resume()
    }

Inside the function we are doing a couple of things. We are building up the URL, we need to have an HTTP Method assigned to it. The most common ones are GET, POST, PULL, DELETE. To avoid any mistakes we can put these inside an enum.

    enum HTTPMethod: String {
        case get = "GET"
        case post = "POST"
        case pull = "PULL"
        case delete = "DELETE"
    }

now when we want to access a method we do

HTTPMethod.get.rawValue

this way with autofill we can avoid spelling mistakes.

smart

We then use the URL we built to do our request. We can get an error or we can not get any data back so we want to check and make sure that we don’t. And if we do we will pass back the type of error that happens. We do this with another custom enum. As we build our app we can add more errors to our enum but for now, we have the following.

enum NetworkError: Error {
        case otherError(Error)
        case noData
        case decodeFailed
    }

We are hoping that the data comes back with information and so we have to decode the data to convert it from JSON to swift. To do this we use the JSON Decoder method. If it all works out we want to build an array of our data. Our data is stored in the variable we created.

Part of the data we are getting back is a URL for an image. We also have to write a request to get the image. Getting an image requires some slight modifications to the previous get request.

Our endpoint will be different because it’s a string that comes in after we get the data. So our endpoint will have a string associated value.

case getImage(String)

The URL itself is going to be passed into the imageString constant.

Our switch case will access the string via a constant that we create and it will return that variable.

case.getImage(let imagePath):
     return imagePath

We are going to create a method to download the image with the image path. Just like before we are going to build the URL, request the image and make sure we don’t have errors and that we do in fact have data to pass along.

func downloadCocktailImage(path: String, completion: @escaping (Result<Data, NetworkError>) -> Void) {
        //Build up the URL with necessary information
        var request = URLRequest(url: Endpoints.getImage(path).url)
        request.httpMethod = HTTPMethod.get.rawValue

        //Request the image
        URLSession.shared.dataTask(with: request) { (data, response, error) in
            guard error == nil else {
                DispatchQueue.main.async {
                    completion(.failure(.otherError(error!)))
                }
                return
            }

            guard let data = data else {
                DispatchQueue.main.async {
                    completion(.failure(.noData))
                }
                return
            }
            DispatchQueue.main.async {
                completion(.success(data))
            }
        }.resume()
    }

The file will look like this.


//
// CocktailResultController.swift
// cocktailMaker
//
// Created by Claudia Contreras on 3/25/20.
// Copyright © 2020 thecoderpilot. All rights reserved.
//
import Foundation
import UIKit
class CocktailResultController {
// MARK: – Properties
//To store our cocktails
var cocktailResults: [CocktailResults] = []
//We want to keep track of our possible errors
enum NetworkError: Error {
case otherError(Error)
case noData
case decodeFailed
}
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case pull = "PULL"
case delete = "DELETE"
}
enum Endpoints {
static let baseURL = "https://www.thecocktaildb.com/api/json/v1/1/"
//This is where we store the different endpoint cases. They are named based on their functionality
case getRandomCocktail
case getImage(String)
var stringValue: String {
switch self {
case .getRandomCocktail:
return Endpoints.baseURL + "/random.php"
case.getImage(let imagePath):
return imagePath
}
}
var url: URL {
return URL(string: stringValue)!
}
}
// MARK: – Functions
// Function to get a random Drink
func getRandomCocktail(completion: @escaping (Result<[CocktailResults], NetworkError>) -> Void) {
//Build up the URL with necessary information
var request = URLRequest(url: Endpoints.getRandomCocktail.url)
request.httpMethod = HTTPMethod.get.rawValue
//Request the data
URLSession.shared.dataTask(with: request) { (data, response, error) in
guard error == nil else {
DispatchQueue.main.async {
completion(.failure(.otherError(error!)))
}
return
}
guard let data = data else {
DispatchQueue.main.async {
completion(.failure(.noData))
}
return
}
//Decode the data
let decoder = JSONDecoder()
do {
self.cocktailResults = try decoder.decode([CocktailResults].self, from: data)
DispatchQueue.main.async {
completion(.success(self.cocktailResults))
}
} catch {
DispatchQueue.main.async {
completion(.failure(.decodeFailed))
}
return
}
}.resume()
}
func downloadCocktailImage(path: String, completion: @escaping (Result<Data, NetworkError>) -> Void) {
//Build up the URL with necessary information
var request = URLRequest(url: Endpoints.getImage(path).url)
request.httpMethod = HTTPMethod.get.rawValue
//Request the image
URLSession.shared.dataTask(with: request) { (data, response, error) in
guard error == nil else {
DispatchQueue.main.async {
completion(.failure(.otherError(error!)))
}
return
}
guard let data = data else {
DispatchQueue.main.async {
completion(.failure(.noData))
}
return
}
DispatchQueue.main.async {
completion(.success(data))
}
}.resume()
}
}

Next we need to get that data to our view controller to display it. Let’s start off by creating a new file let’s call it “DetailCocktailViewController.swift”.

View

The first thing we are going to do is create IBOutlets for the 4 different components in the app.

//MARK: - IBOutlets
    @IBOutlet var drinkNameLabel: UILabel!
    @IBOutlet var imageView: UIImageView!
    @IBOutlet var IngredientsTextView: UITextView!
    @IBOutlet var instructionsTextView: UITextView!

We need to create some properties to hold our data.

The first will be an instance of the CocktailResultController

var cocktailResultController = CocktailResultController()

We also need a variable to store the cocktail that we are going to be displaying.

var cocktailResult: CocktailResults?

Let’s take a look at the data we want to put on our View Controller.

The title is a string so this is easy enough.

drinkNameLabel.text = cocktail.drinkName

The image has to be requested. This should have its own function.

Let’s create a function that will do the network request and update the image view. This function will get the imageString and perform a network request. Once we get our data back we will add the image to the image view.

func getImage(with cocktail: CocktailResults) {
        let imagePath = cocktail.imageString
        cocktailResultController.downloadCocktailImage(path: imagePath ) { (result) in
            guard let imageString = try? result.get() else { return }
            let image = UIImage(data: imageString)
            DispatchQueue.main.async {
                self.imageView.image = image
            }
        }
    }

The next View field that we are going to update is the ingredients. This one is going to take some data manipulation because we have 10 variables for the ingredients and 10 for the measurements and we want to put them all into a TextView.

Let’s do this in a separate function as well.

Swift has a feature called Mirror. Mirror allows us to iterate over, and read the values of, all the stored properties that a type has — whether that’s a struct, a class, or any other type. John Sundell has a really good article that goes into more detail on how it works.

Once we implement Mirror we can then grab all the items that have the words “ingredient” and “measurement” and put them into an array. Once it’s in an array we can then go ahead and put the data in the TextView.

func getIngredients(with cocktail: CocktailResults) {
        let mirrorCocktail = Mirror(reflecting: cocktail)

        var ingredientsArray: [String] = []
        for child in mirrorCocktail.children {
            guard let ingredientKey = child.label else { return }
            let ingredientValue = child.value as? String
            if ingredientKey.contains("ingredient") && ingredientValue != nil {
                ingredientsArray.append(ingredientValue!)
            }
        }
        let ingredients = ingredientsArray.compactMap{ $0 }

        var measurementArray: [String] = []
        for child in mirrorCocktail.children {
            guard let measurementKey = child.label else { return }
            let measurementValue = child.value as? String
            if measurementKey.contains("measurement") && measurementValue != nil {
                measurementArray.append(measurementValue!)
            }
        }

        let measurement = measurementArray.compactMap{ $0 }
        IngredientsTextView.text = "Ingredients: \n"
        for i in 0..<ingredients.count {
            IngredientsTextView.text += "- \(measurement[i]) \(ingredients[i])\n"
        }
    }

The last thing to change in our View is the instructions. That’s already in a string so that is also an easy line.

instructionsTextView.text = "Instructions:\n \(cocktail.instructions)"

We can put the different calls in a updateViews function to keep things neat. This is what my final file looks like:


import UIKit
class DetailCocktailViewController: UIViewController {
// MARK: – IBOutlets
@IBOutlet var drinkNameLabel: UILabel!
@IBOutlet var imageView: UIImageView!
@IBOutlet var IngredientsTextView: UITextView!
@IBOutlet var instructionsTextView: UITextView!
// MARK: – Properties
var cocktailResultController = CocktailResultController()
var cocktailResult: CocktailResults?
override func viewDidLoad() {
super.viewDidLoad()
getCocktail()
}
// MARK: – Functions
func updateViews() {
guard let cocktail = cocktailResult else { return }
drinkNameLabel.text = cocktail.drinkName
getImage(with: cocktail)
getIngredients(with: cocktail)
instructionsTextView.text = "Instructions:\n \(cocktail.instructions)"
}
func getCocktail() {
cocktailResultController.getRandomCocktail { (result) in
do {
let cocktail = try result.get()
DispatchQueue.main.async {
self.cocktailResult = cocktail.drinks[0]
self.updateViews()
}
} catch {
print(result)
}
}
}
func getImage(with cocktail: CocktailResults) {
let imagePath = cocktail.imageString
cocktailResultController.downloadCocktailImage(path: imagePath ) { (result) in
guard let imageString = try? result.get() else { return }
let image = UIImage(data: imageString)
DispatchQueue.main.async {
self.imageView.image = image
}
}
}
func getIngredients(with cocktail: CocktailResults) {
let mirrorCocktail = Mirror(reflecting: cocktail)
var ingredientsArray: [String] = []
for child in mirrorCocktail.children {
guard let ingredientKey = child.label else { return }
let ingredientValue = child.value as? String
if ingredientKey.contains("ingredient") && ingredientValue != nil {
ingredientsArray.append(ingredientValue!)
}
}
let ingredients = ingredientsArray.compactMap{ $0 }
var measurementArray: [String] = []
for child in mirrorCocktail.children {
guard let measurementKey = child.label else { return }
let measurementValue = child.value as? String
if measurementKey.contains("measurement") && measurementValue != nil {
measurementArray.append(measurementValue!)
}
}
let measurement = measurementArray.compactMap{ $0 }
IngredientsTextView.text = "Ingredients: \n"
for i in 0..<ingredients.count {
IngredientsTextView.text += "\(measurement[i]) \(ingredients[i])\n"
}
}
}

That was a lot of work but we are done so now when we run our app we get a random drink.

The UI needs some work and we can also add some more endpoints to make it more useful and that will be next but for now this is it.

1 thought on “Building an iOS app using an API”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s