• 20 hours
  • Medium

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 3/6/20

Integrate third party API

Log in or subscribe for free to enjoy all this course has to offer!

We’ve prepared for networking by implementing the foundation of networking functionality. Now, we need to get specific and define which server we are going to communicate with, what requests we need to send out, and what responses we should expect.

Utilizing specific API

Where do we place all the code responsible for reaching out to the server and delivering the results?

Remember, we have  MediaService.swift  which is not much of a service at the moment.

We've established a couple of methods already - placeholders that define what we'll need eventually. Now is the time to be specific:

  • getMediaList - to get a list of items to present on our grid view;

  • getMedia - to get details of the details.

Before we implement them, let's create some helper code elements within the MediaService class. We need to define some strings that will help us compose the URLs that we are going to use in our code. Le's use a private structure for that with static constants:

private struct API {
private static let base = "https://itunes.apple.com/"
private static let search = API.base + "search"
private static let lookup = API.base + "lookup"
static let searchURL = URL(string: API.search)!
static let lookupURL = URL(string: API.lookup)!
}

Let's review this new structure:

  • We've defined some strings that help us store the base URL string for the iTunes API, and two suffixes to append to the base for the search and lookup respectively.

  • We've declared two variables that generate URL objects for each of the two endpoints that we'll use: search and lookup.

Now we'll implement a few helper methods that will create a request.

A general request method:

private static func createRequest(url: URL, params: [String: Any]) -> URLRequest {
var request = URLRequest(url: url)
request.httpMethod = "POST"
let body = params.map{ "\($0)=\($1)" }
.joined(separator: "&")
request.httpBody = body.data(using: .utf8)
return request
}

And two more to generate specific search and lookup requests:

private static func createSearchRequest(term: String) -> URLRequest {
let params = ["term": term, "media": "music", "entity": "song"]
return createRequest(url: API.searchURL, params: params)
}
private static func createLookupRequest(id: Int) -> URLRequest {
let params = ["id": id]
return createRequest(url: API.lookupURL, params: params)
}

Here we're using some hard-coded values to define the media type we are requesting and the entity type we are expecting as a result.

Now we're ready to implement the key methods.

Getting the list of media items

First, we need to alter the declaration of the original placeholder method:  getMediaList .

We'd expect this method to return an array of objects (or an empty array). However, since the networking process is not instant, we can't manage to get a response from the server while this method is being executed. We can't wait either, as the app may appear "frozen," especially if it's taking time to communicate.

We also have to do something with the response, and that processing code cannot just follow getMediaList(...) execution.

To resolve this, let's alter our declaration to take two parameters:

  • a term - that we'll base our search on;

  • and a completion block - the code that will be executed after we hear back from the server, (good or bad) or after a requested timeout.

static func getMediaList(term: String, completion: @escaping (Bool, [MediaBrief]?) -> Void) {
}

The completion block will provide two parameters:

  • boolean status indicating success;

  • and an array of data objects (empty or not) in case of success, and nil otherwise.

Let's proceed with the implementation. We'll perform it in a few segments:

static func getMediaList(term: String, completion: @escaping (Bool, [MediaBrief]?) -> Void) {
let session = URLSession(configuration: .default)
let request = createSearchRequest(term: term)
let task = session.dataTask(with: request) { (data, response, error) in
if let data = data, error == nil {
// response needs processing
} else {
completion(false, nil)
}
}
task.resume()
}

Let's review the above:

  • We are staring with defining a session constant using default configuration. 

  • Then, we've got a constant for search request generated by a helper method we created earlier. We supply it a term that we've got as a parameter to the getMediaList method.

  • Next, we've created a task object and supplied a closure to it to handle the completion.

  • Finally, we've resumed the task.

Let's now direct our attention to the completion handling block of a task.

Here we're checking if the data object is NOT nil and the error object IS nil. If so - not much is implemented here. And if either data object is nil or if error is not nil, we are exiting the block and calling the completion handler for the getMediaList method setting success to  false  and data array to  nil .

Let's continue with the scenario where we get a positive response:

if let response = response as? HTTPURLResponse, response.statusCode == 200,
let responseJSON = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
let results = responseJSON!["results"] as? [AnyObject] {
// process the results
}
else {
completion(false, nil)
}

In this addition, we are verifying the response further:

  • Parsing JSON objects

  • Accessing results field and making sure it's an array of objects

If any of the criteria are not met, we exit the block and call a completion block.

Otherwise, here is the final data processing code:

var list = [MediaBrief]()
for i in 0 ..< results.count {
guard let song = results[i] as? [String: Any] else {
continue
}
if let id = song["trackId"] as? Int,
let title = song["trackName"] as? String,
let artistName = song["artistName"] as? String,
let artworkUrl = song["artworkUrl100"] as? String {
let mediaBrief = MediaBrief(id: id, title: title, artistName: artistName, artworkUrl: artworkUrl)
list.append(mediaBrief)
}
}
completion(true, list)

Here we're doing the following:

  • Declaring a list array where we'll collect all the data objects to pass on.

  • Iterating through the results array we received in the response.

  • Checking if an element of the array is a dictionary with expected structure, and skipping the loop otherwise using  guard . Guard is another conditional structure that acts somewhat opposite to else/if. This line of code reads as if the condition is met - continue after the guard block. If the condition is not met - execute the else clause. In our example, that is to skip continue to the next iteration in the loop - using continue  command.

  • Finally, we are accessing all of the expected elements using the field names we found in the API description, and downcasting them to the expected types. If all goes well, we're creating a MediaBrief object and appending it to the list array.

  • After we've examined the whole array of results, we call a completion block indicating success and supply an array of data objects. Or, an empty array if we've received no data in response. 

Here's the complete code of the method:

static func getMediaList(term: String, completion: @escaping (Bool, [MediaBrief]?) -> Void) {
let session = URLSession(configuration: .default)
let request = createSearchRequest(term: term)
let task = session.dataTask(with: request) { (data, response, error) in
if let data = data, error == nil {
if let response = response as? HTTPURLResponse, response.statusCode == 200,
let responseJSON = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
let results = responseJSON!["results"] as? [AnyObject] {
var list = [MediaBrief]()
for i in 0 ..< results.count {
guard let song = results[i] as? [String: Any] else {
continue
}
if let id = song["trackId"] as? Int,
let title = song["trackName"] as? String,
let artistName = song["artistName"] as? String,
let artworkUrl = song["artworkUrl100"] as? String {
let mediaBrief = MediaBrief(id: id, title: title, artistName: artistName, artworkUrl: artworkUrl)
list.append(mediaBrief)
}
}
completion(true, list)
}
else {
completion(false, nil)
}
} else {
completion(false, nil)
}
}
task.resume()
}

Phew, one down, one to go! 😅

Getting a specific media item

For this method, we also need to alter its declaration adding two parameters: a media object ID we wish to receive the details for, and the completion block:

static func getMedia(id: Int, completion: @escaping (Bool, Media?) -> Void) {
...
}

Implementing this method will be very similar to the one we've just completed. Instead of the search API end point, we'll use the lookup and provide an object ID we are looking for. The results, however, will arrive in the array as well, so we'll take the first one.

Here's the code:

static func getMedia(id: Int, completion: @escaping (Bool, Media?) -> Void) {
let session = URLSession(configuration: .default)
let request = createLookupRequest(id: id)
let task = session.dataTask(with: request) { (data, response, error) in
if let data = data, error == nil {
if let response = response as? HTTPURLResponse, response.statusCode == 200,
let responseJSON = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
let results = responseJSON!["results"] as? [AnyObject] {
if results.count > 0,
let song = results[0] as? [String: Any],
let id = song["trackId"] as? Int,
let title = song["trackName"] as? String,
let artistName = song["artistName"] as? String,
let artworkUrl = song["artworkUrl100"] as? String,
let sourceUrl = song["trackViewUrl"] as? String {
let media = Media(id: id, title: title, artistName: artistName, artworkUrl: artworkUrl, sourceUrl: sourceUrl)
media.collection = song["collectionName"] as? String
completion(true, media)
}
else {
completion(false, nil)
}
}
else {
completion(false, nil)
}
} else {
completion(false, nil)
}
}
task.resume()
}

As you can see, this implementation is very similar to the previous one. Let me highlight the differences:

  • We've created a lookup request supplying a media item id as a parameter.

  • We've verified if the results array contains any items and, if so, taken the first one.

  • Then, performing all the essential verifications like we did in the previous method, and after creating a media object, we've checked if optional values are present (single collection name in our example) and added them to the media object.

Great work! 🤩

Wait a minute, what about images?

Serving images

So far, we only have the URLs for all of the artwork in our data components. How do we get the actual images? They have to be requested separately. In fact, we may not even need some of them. We'll cover this in the next chapter. For the moment, let's create another method in the MediaService class that will be responsible for delivering the actual image data:

static func getImage(imageUrl: URL, completion: @escaping (Bool, Data?) -> Void) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: imageUrl) { (data, response, error) in
if let data = data, error == nil,
let response = response as? HTTPURLResponse, response.statusCode == 200 {
completion(true, data)
}
else {
completion(false, nil)
}
}
task.resume()
}

As you may observe, the structure of the code is similar to the implementation of the two functions we've completed. The difference is that the WHOLE data object of the response IS the image data. We don't need to parse any details and can simply pass it along to the completion block.

We can then use this data to convert it to a UIImage object to be used in UIImageView elements.

Good to know...

Delivering response

Using closures (completion blocks) is one of the options for handling networking responses. Other possible implementations are:

  • using Delegation;

  • using Notification Center.

Using completion blocks is the most common practice - and typically most effective.

Networking

There are third-party libraries that you can use to assist you with this networking. If you are curious, check out Alamofire

We're done with the essential implementation! To see this in action, we need to connect this to our view controllers which we'll do in the following chapter!

Let's recap!

  • When implementing networking services, it's a good practice to abstract literals of an API using constants, which can be organized using structures.

  • Service methods that are expected to return data in response can use the following techniques:

    • Closures (the most common approach)

    • Delegation

    • Notification center

  •  Images and other stand-alone data pieces are typically requested separately.

  • guard/else construction is used like reverse  if/else - to continue with the code that follows the construction if the condition is met, or exit after executing the else clause. 

Example of certificate of achievement
Example of certificate of achievement