Moya Tutorial for iOS: Getting Started

Moya is a networking library inspired by the concept of encapsulating network requests in type-safe way, typically using enumerations, that provides confidence when working with your network layer. Become a networking superhero with Moya! By Shai Mishali.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Authorizing Requests in Marvel’s API

The Marvel API uses a custom authorization scheme where you create a “hash” from a unique identifier (such as a timestamp), the private key and the public key, all concatenated together and hashed using MD5. You can read the full specification in the API reference under Authentication for Server-Side Applications.

In Marvel.swift, replace task with the following:

public var task: Task {
  let ts = "\(Date().timeIntervalSince1970)"
  // 1
  let hash = (ts + Marvel.privateKey + Marvel.publicKey).md5
  
  // 2
  let authParams = ["apikey": Marvel.publicKey, "ts": ts, "hash": hash]
  
  switch self {
  case .comics:
    // 3
    return .requestParameters(
      parameters: [
        "format": "comic",
        "formatType": "comic",
        "orderBy": "-onsaleDate",
        "dateDescriptor": "lastWeek",
        "limit": 50] + authParams,
      encoding: URLEncoding.default)
  }
}

Your task is ready! Here’s what that does:

  1. You create the required hash, as mentioned earlier, by concatenating your random timestamp, the private key and the public key, then hashing the entire string as MD5. You’re using an md5 helper property found in Helpers/String+MD5.swift.
  2. The authParams dictionary contains the required authorization parameters: apikey, ts and hash, which contain the public key, timestamp and hash, respectively.
  3. Instead of the .requestPlain task you had earlier, you switch to using a .requestParameters task type, which handles HTTP requests with parameters. You provide the task with several parameters indicating that you want up to 50 comics from a given week sorted by latest onsaleDate. You add the authParams you created earlier to the parameters dictionary so that they’re sent along with the rest of the request parameters.

At this point, your new Marvel target is ready to go! Next, you’re going to update ComicsViewController to use it.

Using Your Target

Go to ComicsViewController.swift and add the following at the beginning of your view controller class:

let provider = MoyaProvider<Marvel>()

As mentioned earlier, the main class you’ll use to interact with your Moya targets is MoyaProvider, so you start by creating an instance of MoyaProvider that uses your new Marvel target.

Next, inside your viewDidLoad(), replace:

state = .error

With:

// 1
state = .loading

// 2
provider.request(.comics) { [weak self] result in
  guard let self = self else { return }

  // 3
  switch result {
  case .success(let response):
    do {
      // 4
      print(try response.mapJSON())
    } catch {
      self.state = .error
    }
  case .failure:
    // 5
    self.state = .error
  }
}

The new code does the following:

  1. First, you set the view’s state to .loading.
  2. Use the provider to perform a request on the .comics endpoint. Notice that this is entirely type-safe, since .comics is an enum case. So, there’s no worry of mis-typing the wrong option; along with the added value of getting auto-completed cases for every endpoint of your target.
  3. The closure provides a result which can be either .success(Moya.Response) or .failure(Error).
  4. If the request succeeds, you use Moya’s mapJSON method to map the successful response to a JSON object and then print it to the console. If the conversion throws an exception, you change the view’s state to .error.
  5. If the returned result is a .failure, you set the view’s state to .error as well.

Build and run the app. The Xcode debug console should show something similar to the following:

{
    attributionHTML = "<a href=\"http://marvel.com\">Data provided by Marvel. \U00a9 2018 MARVEL</a>";
    attributionText = "Data provided by Marvel. \U00a9 2018 MARVEL";
    code = 200;
    copyright = "\U00a9 2018 MARVEL";
    data =     {
        count = 19;
        limit = 50;
        offset = 0;
        results =         (
            {comic object},
            {comic object},
            {comic object},
            ...
        )
}

Awesome work, you’ve got a valid JSON response from the backend using Moya and your new Marvel target!

Note: It may take several seconds for result to appear in the debug console.

The last step to complete this view controller is actually mapping the JSON response into proper Data Models — in your case, a pre-configured Comic struct.

This is the perfect time to use a different Moya response mapper that maps a response on to a Decodable instead of raw JSON.

You might’ve noticed the JSON response’s structure looks something like:

data ->
  results -> 
      [ Array of Comics ]

Meaning two levels of nesting (data, results) before getting to the objects themselves. The starter project already includes the proper Decodable object that takes care of decoding this.

Replace the following:

print(try response.mapJSON())

With:

self.state = .ready(try response.map(MarvelResponse<Comic>.self).data.results)

Instead of mapping the object to a raw JSON response, you use a mapper that takes the MarvelResponse generic Decodable with a Comic struct. This will take care of parsing the two levels of nesting as well, which lets you access the array of comics by accessing data.results.

You set the view’s state to .ready with its associated value being the array of Comic objects returned from the Decodable mapping.

Build and run the project. You should see your first screen fully functional!

On to the detail view then!

When you tap on a comic, the starter project already has the code for showing a CardViewController and passing it the selected Comic to it. But, you might notice that tapping a comics only shows an empty card without any comic details. Let’s take care of that!

Switch to CardViewController.swift and find the layoutCard(comic:) method. Inside the method, add:

// 1
lblTitle.text = comic.title
lblDesc.text = comic.description ?? "Not available"

// 2
if comic.characters.items.isEmpty {
  lblChars.text = "No characters"
} else {
  lblChars.text = comic.characters.items
                       .map { $0.name }
                       .joined(separator: ", ")
}

// 3
lblDate.text = dateFormatter.string(from: comic.onsaleDate)

// 4
image.kf.setImage(with: comic.thumbnail.url)

This code updates the screen with information from the provided Comic struct by:

  1. Setting the comic’s title and the comic’s description.
  2. Setting the list of characters for the comic, or, “No characters” if there are no characters.
  3. Setting the “on sale” date of the comic, using a pre-configured DateFormatter.
  4. Loading the comic’s image using Kingfisher — a great third-party library for loading web images.

Build and run your app, and tap one of the comics in the list — you should see a beautiful information card:

You have two more features to add: uploading your card to Imgur and letting the user delete the card.