Chapters

Hide chapters

UIKit Apprentice

Second Edition · iOS 15 · Swift 5.5 · Xcode 13

My Locations

Section 3: 11 chapters
Show chapters Hide chapters

Store Search

Section 4: 13 chapters
Show chapters Hide chapters

34. Networking
Written by Fahim Farook

Now that the preliminaries are out of the way, you can finally get to the good stuff: adding networking to the app so that you can download actual data from the iTunes Store!

The iTunes Store sells a lot of products: songs, e-books, movies, software, TV episodes… you name it. You can sign up as an affiliate and earn a commission on each sale that happens because you recommended a product — it can be even your own apps!

To make it easier for affiliates to find products, Apple made available a web service that queries the iTunes store. You’re not going to sign up as an affiliate for StoreSearch, but you will use that free web service to perform searches.

In this chapter you will learn the following:

  • Query the iTunes web service: An introduction to web services and the specifics about querying Apple’s iTunes Store web service.
  • Send an HTTP request: How to create a proper URL for querying a web service and how to send a request to the server.
  • Parse JSON: How to make sense of the JSON information sent back from the server and convert that to objects with properties that can be used in your app.
  • Sort the search results: Explore different ways to sort the search results alphabetically so as to write the most concise and compact code.

Query the iTunes web service

So what is a web service? Your app — also known as the “client” — will send a message over the network to the iTunes store — the “server” — using the HTTP protocol.

Because the iPhone can be connected to different types of networks — Wi-Fi or a cellular network such as LTE, 3G, or GPRS — the app has to “speak” a variety of networking protocols to communicate with other computers on the Internet.

The HTTP requests fly over the network
The HTTP requests fly over the network

Fortunately you don’t have to worry about any of that as the iPhone firmware will take care of this complicated process. All you need to know is that you’re using HTTP.

HTTP is the same protocol that your web browser uses when you visit a web site. In fact, you can play with the iTunes web service using a web browser. That’s a great way to figure out how this web service works.

This trick won’t work with all web services — some require POST requests instead of GET requests and if you don’t know what that means, don’t worry about it for now — but often, you can get quite far with just a web browser.

Open your favorite web browser — I’m using Safari — and go to the following URL:

http://itunes.apple.com/search?term=metallica

The browser will download a file. If you open the file in a text editor, it should contain something like this:

{
 "resultCount":50,
 "results": [
{"wrapperType":"track", "kind":"song", "artistId":3996865, "collectionId":579372950, "trackId":579373079, "artistName":"Metallica", "collectionName":"Metallica", "trackName":"Enter Sandman", "collectionCensoredName":"Metallica", "trackCensoredName":"Enter Sandman", "artistViewUrl":"https://itunes.apple.com/us/artist/metallica/id3996865?uo=4", "collectionViewUrl":"https://itunes.apple.com/us/album/enter-sandman/id579372950?i=579373079&uo=4", "trackViewUrl":"https://itunes.apple.com/us/album/enter-sandman/id579372950?i=579373079&uo=4", "previewUrl":"http://a38.phobos.apple.com/us/r30/Music7/v4/bd/fd/e4/bdfde4e4-5407-9bb0-e632-edbf079bed21/mzaf_907706799096684396.plus.aac.p.m4a", "artworkUrl30":"http://is1.mzstatic.com/image/thumb/Music/v4/0b/9c/d2/0b9cd2e7-6e76-8912-0357-14780cc2616a/source/30x30bb.jpg", "artworkUrl60":"http://is1.mzstatic.com/image/thumb/Music/v4/0b/9c/d2/0b9cd2e7-6e76-8912-0357-14780cc2616a/source/60x60bb.jpg", "artworkUrl100":"http://is1.mzstatic.com/image/thumb/Music/v4/0b/9c/d2/0b9cd2e7-6e76-8912-0357-14780cc2616a/source/100x100bb.jpg", "collectionPrice":9.99, "trackPrice":1.29, "releaseDate":"1991-07-29T07:00:00Z", "collectionExplicitness":"notExplicit", "trackExplicitness":"notExplicit", "discCount":1, "discNumber":1, "trackCount":12, "trackNumber":1, "trackTimeMillis":331560, "country":"USA", "currency":"USD", "primaryGenreName":"Metal", "isStreamable":true}, 
. . .

Those are the search results that the iTunes web service gives you. The data is in a format named JSON, which stands for JavaScript Object Notation.

JSON

JSON is commonly used to send structured data back-and-forth between servers and clients, i.e. apps. Another data format that you may have heard of is XML, but that’s being fast replaced by JSON.

There are a variety of tools that you can use to make the JSON output more readable for mere humans. I have a Quick Look plug-in installed that renders JSON files in a colorful view (www.sagtau.com/quicklookjson.html).

You do need to save the output from the server to a file with a .json extension first, and then open it from Finder by selecting the file and then pressing the space bar:

A more readable version of the output from the web service
A more readable version of the output from the web service

That makes a lot more sense.

Note: You can find extensions for Safari (and most other browsers) that can prettify JSON directly inside the browser. github.com/rfletcher/safari-json-formatter is a good one.

There are also dedicated tools on the Mac App Store, for example Visual JSON, that let you directly perform the request on the server and show the output in a structured and readable format.

A great online tool is codebeautify.org/jsonviewer.

Browse through the JSON text for a bit. You’ll see that the server gave back a list of items, some of which are songs; others are audiobooks, or music videos.

Each item has a bunch of data associated with it, such as an artist name — “Metallica”, which is what you searched for —, a track name, a genre, a price, a release date, and so on.

You’ll store some of these fields in the SearchResult class so you can display them on the screen.

The results you get from the iTunes store might be different from mine. By default, the search returns at most 50 items and since the store has quite a bit more than fifty entries that match “Metallica”, each time you do the search you may get back a different set of 50 results.

Also notice that some of these fields, such as artistViewUrl and artworkUrl100 and previewUrl are links/URLs. Go ahead and copy-paste these URLs in your browser and see what happens.

The artistViewUrl will open an iTunes Preview page for the artist, the artworkUrl100 loads a thumbnail image, and the previewUrl opens a 30-second audio preview.

This is how the server tells you about additional resources. The images and so on are not embedded directly into the search results, but you’re given a URL that allows you to download each item separately. Try some of the other URLs from the JSON data and see what they do!

The HTTP request

Back to the original HTTP request. You made the web browser go to the following URL:

http://itunes.apple.com/search?term=the search term

You can add other parameters as well to make the search more specific. For example:

http://itunes.apple.com/search?term=metallica&entity=song

Now the results won’t contain any music videos or podcasts, only songs.

If the search term has a space in it you should replace it with a + sign, as in:

http://itunes.apple.com/search?term=pokemon+go&entity=software

This searches for all apps that have something to do with Pokemon Go — you may have heard of some of them.

The fields in the JSON results for this particular query are slightly different than before. There is no previewUrl but there are several screenshot URLs per entry. Different kinds of products — songs, movies, software — return different types of data.

That’s all there is to it. You construct a URL to itunes.apple.com with the search parameters and then use that URL to make an HTTP request. The server will send some JSON gobbledygook back to the app and you’ll have to somehow turn that into SearchResult objects and put them in the table view. Let’s get on it!

Synchronous networking = bad

Before we begin though, there is a good way to do networking in your apps and a bad way.

The bad way is to perform the HTTP requests on your app’s main thread — it is simple to program, but it will block the user interface and make your app unresponsive while the networking is taking place. Because it blocks the rest of the app, this is called synchronous networking.

Unfortunately, many programmers insist on doing networking the wrong way in their apps, which makes for apps that are slow and prone to crashing.

I will begin by demonstrating the easy-but-bad way, just to show you how not to do this. It’s important that you realize the consequences of synchronous networking, so you will avoid it like the plague (or COVID-19) in your own apps.

After I have convinced you of the evilness of this approach, I will show you how to do it the right way — it only requires a small modification to the code, but may require a big change in how you think about these problems.

Asynchronous networking — the right kind, with an “a” — makes your apps much more responsive, but also brings with it additional complexity that you need to deal with.

Send an HTTP(S) request

In order to query the iTunes Store web service, the very first thing you must do is send an HTTP request to the iTunes server. This involves several steps such as creating a URL with the correct search parameters, sending the request to the server, getting a response back etc.

You’ll take these step-by-step.

Create the URL for the request

➤ Add a new method to SearchViewController.swift:

// MARK: - Helper Methods
func iTunesURL(searchText: String) -> URL {
  let urlString = String(
    format: "https://itunes.apple.com/search?term=%@", 
    searchText)
  let url = URL(string: urlString)
  return url!
}

This first builds a URL string by placing the search text behind the “term=” parameter, and then turns this string into a URL object.

Because URL(string:) is a failable initializer, it returns an optional. You force unwrap that using url! to return an actual URL object.

HTTP vs. HTTPS

Previously you used http:// but here you’re using https://. The difference is that HTTPS is the secure, encrypted version of HTTP. It protects your users from eavesdropping. The underlying protocol is the same, but any bytes that you’re sending or receiving are encrypted before they go out on the network.

As of iOS 9, Apple recommends that apps should always use HTTPS. In fact, even if you specify an unprotected http:// URL, iOS will still try to connect using HTTPS. If the server isn’t configured to use HTTPS, then the network connection will fail.

You can ask to be exempted from this behavior in your Info.plist file, but that is generally not recommended.

➤ Change searchBarSearchButtonClicked(_:) to:

func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
  if !searchBar.text!.isEmpty {
    searchBar.resignFirstResponder()

    hasSearched = true
    searchResults = []

    let url = iTunesURL(searchText: searchBar.text!)
    print("URL: '\(url)'")

    tableView.reloadData()
  }
}

You’ve removed the code that created fake SearchResult items, and instead, call the new iTunesURL(searchText:) method. For testing purposes, you log the URL object that this method returns.

This logic sits inside an if statement so that none of this happens unless the user actually typed text into the search bar — it doesn’t make much sense to search the iTunes store for “nothing”.

Note: Don’t get confused by all the exclamation points in the line,

if !searchBar.text!.isEmpty

The first one is the “logical not” operator because you want to go inside the if statement only if the text is not empty. The second exclamation point is for force unwrapping the value of searchBar.text, which is an optional — it will never actually be nil, so it being an optional is a bit silly, but whaddya gonna do?

➤ Run the app and type in some search text that is a single word, for example “metallica”, or one of your other favorite metal bands, and press the Search button.

Xcode should now show this in its Debug pane:

URL: 'https://itunes.apple.com/search?term=metallica'

That looks good.

➤ Now type in a search term with one or more spaces, like “pokemon go”, into the search box.

Whoops, the app crashes!

The crash after searching for 'pokemon.go'
The crash after searching for 'pokemon.go'

Look at the left-hand pane, the Variables view, of the Xcode debugger and you’ll see that the url constant has no value.

The app apparently did not create a valid URL object. But why?

A space is not a valid character in a URL. Many other characters aren’t valid either — such as the < or > signs — and therefore must be escaped. Another term for this is URL encoding.

A space, for example, can be encoded as the + sign — you did that earlier when you typed the URL into the web browser — or as the character sequence %20.

➤ Fortunately, String can do this encoding already. So, you only have to add one extra statement to the app to make this work:

func iTunesURL(searchText: String) -> URL {
  let encodedText = searchText.addingPercentEncoding(
      withAllowedCharacters: CharacterSet.urlQueryAllowed)!  // Add this
  let urlString = String(
    format: "https://itunes.apple.com/search?term=%@", 
    encodedText)                                             // Change this
  let url = URL(string: urlString)
  return url!
}

This calls the addingPercentEncoding(withAllowedCharacters:) method to create a new string where all the special characters are escaped, and you use that string for the search term.

UTF-8 string encoding

This new string treats the special characters as being “UTF-8 encoded”. It’s important to know what that means because you’ll run into this UTF-8 thing every once in a while when dealing with text.

There are many different ways to encode text. You’ve probably heard of ASCII and Unicode, the two most common encodings.

UTF-8 is a version of Unicode that is very efficient for storing regular text, but less so for special symbols or non-Western alphabets. Still, it’s the most popular way to deal with Unicode text today.

Normally, you don’t have to worry about how your strings are encoded. But when sending requests to a web service you need to transmit the text with the proper encoding.

Tip: When in doubt, use UTF-8, it will almost always work.

➤ Run the app and search for “pokemon go” again. This time a valid URL object can be created, and it looks like this:

URL: 'https://itunes.apple.com/search?term=pokemon%20go'

The space has been turned into the character sequence %20. The % indicates an escaped character and 20 is the UTF-8 value for a space. Also try searching for terms with other special characters, such as # and * or even Emoji, and see what happens.

Perform the search request

Now that you have a valid URL object, you can do some actual networking!

➤ Add a new method to SearchViewController.swift:

func performStoreRequest(with url: URL) -> String? {
  do {
   return try String(contentsOf: url, encoding: .utf8)
  } catch {
   print("Download Error: \(error.localizedDescription)")
   return nil
  }
}

The meat of this method is the call to String(contentsOf:encoding:) which returns a new string object with the data it receives from the server pointed to by the URL.

Note that you’re telling the app to interpret the data as UTF-8 text. Should the server send back the text in a different encoding, then it will look like a garbled mess to your app. It’s important that the sending and receiving sides agree on the encoding they are using!

Because things can go wrong — for example, the network may be down and the server cannot be reached — you enclose this in a do-try-catch block. If there is a problem, the code jumps to the catch branch and the error variable will contain more details about the error. If this happens, you print out a user-understandable form of the error and return nil to signal that the request failed.

➤ Add the following lines to searchBarSearchButtonClicked(_:), after the print() line:

if let jsonString = performStoreRequest(with: url) {
  print("Received JSON string '\(jsonString)'")
}

This invokes performStoreRequest(with:) with the URL object as a parameter and returns the JSON data that is received from the server. If everything goes according to plan, this method returns a new string containing the JSON data that you’re after. Let’s try it out!

➤ Run the app and search for your favorite band. After a second or so, a whole bunch of data will be dumped to the Xcode Console:

URL: 'http://itunes.apple.com/search?term=metallica'

Received JSON string '


{
 "resultCount":50,
 "results": [
{"wrapperType":"track", "kind":"song", "artistId":3996865, "collectionId":579372950, "trackId":579373079, "artistName":"Metallica", "collectionName":"Metallica", "trackName":"Enter Sandman", "collectionCensoredName":"Metallica", "trackCensoredName":"Enter Sandman", 
. . . and so on . . .

Congratulations, your app has successfully talked to a web service!

This prints the same stuff that you saw in the web browser earlier. Right now it’s all contained in a single String object, which isn’t really useful for our purposes, but you’ll convert it to a more useful format in a minute.

Of course, it’s possible that you received an error. In that case, the output should be something like this:

URL: 'https://itunes.apple.com/search?term=Metallica'
2020-08-20 11:44:52.963727-0400 StoreSearch[5676:12463647] Connection 2: received failure notification
2020-08-20 11:44:52.963818-0400 StoreSearch[5676:12463647] Connection 2: failed to connect 1:50, reason -1
2020-08-20 11:44:52.963896-0400 StoreSearch[5676:12463647] Connection 2: encountered error(1:50)
2020-08-20 11:44:52.965094-0400 StoreSearch[5676:12462585] Task <1E233D0F-44B8-4F3D-BE8C-CFCB8AA07615>.<0> HTTP load failed, 0/0 bytes (error code: -1009 [1:50])
2020-08-20 11:44:52.965515-0400 StoreSearch[5676:12463648] NSURLConnection finished with error - code -1009
Download Error: The file “search” couldn’t be opened.

You’ll add better error handling to the app later, but if you get such an error at this point, then make sure your computer — or your iPhone in case you’re running the app on a device and not in the Simulator — is connected to the Internet. Also try the URL directly in your web browser and see if that works.

Parse JSON

Now that you have managed to download a chunk of JSON data from the server, what do you do with it?

JSON is a structured data format. It typically consists of arrays and dictionaries that contain other arrays and dictionaries, as well as regular data such as strings and numbers.

An overview of the JSON data

The JSON from the iTunes store roughly looks like this:

{
  "resultCount": 50,
  "results": [ . . . a bunch of other stuff . . . ]
}

The { } brackets surround a dictionary. This particular dictionary has two keys: resultCount and results. The first one, resultCount, has a numeric value. This is the number of items that matched your search query. By default the limit is a maximum of 50 items, but as you will see later, you can increase this upper limit.

The results key contains an array, which is indicated by the [ ] brackets. Inside that array are more dictionaries, each of which describes a single product from the store. You can tell these things are dictionaries because they have the { } brackets again.

Here are two of these items from the array:

{
  "wrapperType": "track",
  "kind": "song",
  "artistId": 3996865,
  "artistName": "Metallica",
  "trackName": "Enter Sandman",
  . . . and so on . . .
},
{
  "wrapperType": "track",
  "kind": "song",
  "artistId": 3996865,
  "artistName": "Metallica",
  "trackName": "Nothing Else Matters",
  . . . and so on . . .
},

Each product is represented by a dictionary made up of several keys. The values of the kind and wrapperType keys determine what sort of product this is: a song, a music video, an audiobook, and so on. The other keys describe the artist and the song itself.

The structure of the JSON data
The structure of the JSON data

To summarize, the JSON data represents a dictionary and inside that dictionary is an array of more dictionaries. Each of the dictionaries from the array represents one search result. Currently, all of this sits in a String, which isn’t very handy, but using a JSON parser you can turn this data into Swift Dictionary and Array objects.

JSON or XML?

JSON is not the only structured data format out there. XML, which stands for EXtensible Markup Language, is a slightly more formal standard. Both formats serve the same purpose, but they look a bit different. If the iTunes store returned its results as XML, the output would look more like this:

<?xml version="1.0" encoding="utf-8"?>
<iTunesSearch>
  <resultCount>5</resultCount>
  <results>
    <song>
      <artistName>Metallica</artistName>
      <trackName>Enter Sandman</trackName>
    </song>
    <song>
      <artistName>Metallica</artistName>
      <trackName>Nothing Else Matters</trackName>
    </song>
    . . . and so on . . .
  </results>
</iTunesSearch>

These days, most developers prefer JSON because it’s simpler than XML and easier to parse. But it’s certainly possible that if you want your app to talk to a particular web service, you might be expected to deal with XML data.

Prepare to parse JSON data

In the past, if you wanted to parse JSON, it used to be necessary to include a third-party framework into your apps, or to manually walk through the data structure using the built-in iOS JSON parser. But as of Swift 4, there’s a new way to do things — your old pal Codable.

Remember how you used a PropertyListDecoder to decode plist data that supported the Codable protocol for reading — and saving — data in Checklists? Well, property lists aren’t the only format supported by Codable out of the box — JSON is supported too!

All you need to do in order to allow your app to read JSON data directly into the relevant data structures is to set them up to conform to Codable!

“Now hold on there”, I hear you saying. “How does Codable know how an arbitrary data structure from the Internet is set up in order to correctly extract the right bits of data?” Ah, it’s all in how you set your data structures up. You’ll understand as you proceed to parse the data you received from the iTunes server.

The trick to using Codable to parse JSON data is to set up your classes — or structs — to reflect the structure of the data that you’ll parse. As you noticed above, there are two parts to the JSON response received from the iTunes server:

  1. The response wrapper which contains the number of results and an array of results.
  2. The array itself which is made up of individual search result items.

We need to model both of the above in order to parse the JSON data correctly. We’ve already made some headway in terms of modeling the search results by way of the SearchResult object, but we need to do some modifications in order to get the object ready for JSON parsing.

But first, let’s add a new data model for the results wrapper.

➤ Open SearchResult.swift and replace its contents with the following:

class ResultArray: Codable {
	var resultCount = 0
	var results = [SearchResult]()
}

class SearchResult: Codable {
  var artistName: String? = ""
  var trackName: String? = ""
  
  var name: String {
    return trackName ?? ""
  }
}

There are a few changes here:

  1. The ResultArray class models the response wrapper by containing a results count and an array of SearchResult objects. Note that this class supports the Codable protocol.

    If you are wondering why this class is within the same file as SearchResult, it is simply for the sake of expediency. This class is not used anywhere else except as a temporary holder during the JSON parsing process. So I put it in the same file as SearchResult, which is the actual class you’ll be using. But if you prefer, you can put this class in a separate Swift file by itself — it doesn’t make any difference to the app functionality.

  2. The SearchResult class now supports the Codable protocol too.

  3. It also has a new optional property named trackName and the artistName property has been changed to an optional one — the optional properties are to make Codable’s work easier since Codable expects non-optional values to be always present in the JSON data. Unfortunately, the response from the iTunes server might not always have these properties, and you have to allow for that.

  4. The existing property for name has been converted to a computed property which returns the value of the trackName property, or an empty string if trackName is nil.

The reason for changes #3 and #4 might not be obvious immediately. Take a look at the response data you received from the server. Did you notice the “kind” key?

The search results from iTunes can be for multiple types of items — songs, videos, movies, tv shows, books etc. That key indicates the type of item the search result is for. And depending on the item type, you might want to vary how you display an item name. For example, you might not always want to use the “trackName” key as the item name — in fact, as we mention above, “trackName” might not even be there in the returned data. The computed name property is simply preparation for the future in case you want to display different names depending on the result type.

Also, notice that now all the property names in the class match actual keys in the JSON data — you can parse JSON even without the property names matching the key names, but that’s a bit more complicated. So let’s take the easy route here. Remember, baby steps …

And that’s all you need in order to prepare for JSON parsing. Onwards!

Parse the JSON data

You will be using the JSONDecoder class, appropriately enough, to parse JSON data. Only trouble is, JSONDecoder needs its input to be a Data object. You currently have the JSON response from the server as a String.

You can convert the String to Data pretty easily, but it would be better to get the response from the server as Data in the first place — you got the response from the server as String initially only to ensure that the response was correct.

➤ Switch to SearchViewController.swift and modify performStoreRequest(with:) as follows:

func performStoreRequest(with url: URL) -> Data? {  // Change to Data?
  do {
    return try Data(contentsOf:url)                 // Change this line
  } catch {
    . . .
  }
}

You simply change the request method to fetch the response from the server as Data instead of a String — the method now returns the value as an optional Data value instead of an optional String value.

➤ Add the following method to SearchViewController.swift:

func parse(data: Data) -> [SearchResult] {
  do {
    let decoder = JSONDecoder()
    let result = try decoder.decode(
      ResultArray.self, from: data)
    return result.results
  } catch {
    print("JSON Error: \(error)")
    return []
  }
}

You use a JSONDecoder object to convert the response data from the server to a temporary ResultArray object from which you extract the results property. Or at least, you hope you can convert the data without any issues…

Assumptions cause trouble

When you write apps that talk to other computers on the Internet, one thing to keep in mind is that your conversational partners may not always say the things you expect them to say.

There could be an error on the server and instead of valid JSON data, it may send back some error message. In that case, JSONDecoder will not be able to parse the data and the app will return an empty array from parse(data:).

Another thing that could happen is that the owner of the server changes the format of the data they send back. Usually, this is done in a new version of the web service that is accessible via a different URL. Or, they might require you to send along a “version” parameter. But not everyone is careful like that, and by changing what the server does, they may break apps that depend on the data coming back in a specific format.

In the case of the iTunes store web service, the top-level object should be a dictionary with two keys — one for the count, the other for the array of results — but you can’t control what happens on the server. If for some reason the server programmers decide to put [ ] brackets around the JSON data, then the top-level object will no longer be a Dictionary but an Array. This in turn will cause JSONDecoder to fail parsing the data since it is no longer in the expected format.

Being paranoid about these kinds of things and showing an error message in the unlikely event this happens is a lot better than your application suddenly crashing when something changes on a server that is outside of your control.

Just to be sure, you’re using the do-try-catch block to check that the JSON parsing goes through fine. Should the conversion fail, then the app doesn’t burst into flames but simply returns an empty results array.

It’s good to add checks like these to the app to make sure you get back what you expect. If you don’t own the servers you’re talking to, it’s best to program defensively.

➤ Modify searchBarSearchButtonClicked(_:) as follows:

func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
  if !searchBar.text!.isEmpty {
    . . .
    print("URL: '\(url)'")
    if let data = performStoreRequest(with: url) {  // Modified
      let results = parse(data: data)               // New line
      print("Got results: \(results)")              // New line
    }
    tableView.reloadData()
  }
}

You simply change the constant for the result from the call to performStoreRequest(with:) from jsonString to data, call the new parse(data:) method, and print the return value.

➤ Run the app and search for something. The Xcode Console now prints the following:

URL: 'https://itunes.apple.com/search?term=Metallica'
Got results: [StoreSearch.SearchResult, StoreSearch.SearchResult, StoreSearch.SearchResult, StoreSearch.SearchResult, StoreSearch.SearchResult, StoreSearch.SearchResult, StoreSearch.SearchResult, StoreSearch.SearchResult, StoreSearch.SearchResult, StoreSearch.SearchResult, StoreSearch.SearchResult, StoreSearch.SearchResult, StoreSearch.SearchResult, StoreSearch.SearchResult, 
. . . ]

Hmm … that certainly looks like an array of 50 items, but it doesn’t really tell you anything much about the actual data — just that the array consists of SearchResult objects. That’s not much good to you, is it?

Print object contents

➤ Modify the SearchResult class in SearchResult.swift to conform to the CustomStringConvertible protocol:

class SearchResult: Codable, CustomStringConvertible {

The CustomStringConvertible protocol allows an object to have a custom string representation. Or, to put it another way, the protocol allows objects to have a custom string describing the object, or its contents. So, how does the protocol provide this string description? That is done via the protocol’s description property.

➤ Add the following code to the SearchResult class:

var description: String {
  return "\nResult - Name: \(name), Artist Name: \(artistName ?? "None")"
}

The above is your implementation of the description property to conform to the CustomStringConvertible. For your SearchResult class, the description consists of a line break (\n) at the beginning, a string identifying the type of object, and the values of the name and artistName properties. But since artistName is an optional value, you have to account for when it might be nil and output “None” when that happens.

Notice the ?? operator in the above code — it’s called the nil-coalescing operator and you probably remember it from previous chapters. The nil-coalescing operator unwraps the variable to the left of the operator if it has a value, if not, it returns the value to the right of the operator as the default value.

➤ Run the app again and search for something. The Xcode Console should now print something like the following:

URL: 'https://itunes.apple.com/search?term=Metallica'
Got results: [
Result - Name: Enter Sandman, Artist Name: Metallica, 
Result - Name: Nothing Else Matters, Artist Name: Metallica, 
Result - Name: The Unforgiven, Artist Name: Metallica, 
. . .

Yep, that looks more like it!

Do you now see why you put a line break at the beginning of the object description? That way, when you have multiple items in an array, each item gets displayed in a separate line instead of everything being jumbled together. Try removing the line break and see how the output looks.

You have converted a bunch of JSON that didn’t make a lot of sense into actual objects that you can use.

Error handling

Let’s add an alert to handle potential errors. It’s inevitable that something goes wrong somewhere and it’s best to be prepared.

➤ Add the following method to SearchViewController.swift:

func showNetworkError() {
  let alert = UIAlertController(
    title: "Whoops...",
    message: "There was an error accessing the iTunes Store." + 
    " Please try again.", 
    preferredStyle: .alert)
  
  let action = UIAlertAction(
    title: "OK", style: .default, handler: nil)
  alert.addAction(action)
  present(alert, animated: true, completion: nil)
}

Nothing you haven’t seen before; it simply presents an alert controller with an error message.

Note: The message variable is split into two separate strings and concatenated, or added together, using the plus (+) operator just so that the string would display nicely for this book. You can feel free to type out the whole string as a single string instead.

➤ Add the following line to performStoreRequest(with:) just before the return nil:

showNetworkError()

Simply put, if something goes wrong with the request to the iTunes store, you call showNetworkError() to show an alert box.

If you did everything correctly up to this point, then the web service should always have worked. Still it’s a good idea to test a few error situations, just to make sure the error handling is working for those unlucky users with bad network connections.

➤ Try this: In iTunesURL(searchText:) method, temporarily change the “itunes.apple.com” part of the URL to “NOMOREitunes.apple.com”.

You should now get an error alert when you try a search because no such server exists at that address. This simulates the iTunes server being down. Don’t forget to change the URL back when you’re done testing.

Tip: To simulate no network connection you can pull the network cable and/or disable Wi-Fi on your Mac, or run the app on your device in Airplane Mode.

The app shows an alert when there is a network error
The app shows an alert when there is a network error

It should be obvious that when you’re doing networking, things can — and will! — go wrong, often in unexpected ways. So, it’s always good to be prepared for surprises.

Work with the JSON results

So far you’ve managed to send a request to the iTunes web service and you parsed the JSON data into an array of SearchResult objects. However, we are not quite done.

The iTunes Store sells different kinds of products — songs, e-books, software, movies, and so on — and each of these has its own structure in the JSON data. A software product will have screenshots but a movie will have a video preview. The app will have to handle these different kinds of data.

You’re not going to support everything the iTunes store has to offer, only these items:

  • Songs, music videos, movies, TV shows, podcasts
  • Audio books
  • Software (apps)
  • E-books

The reason I have split them up like this is because that’s how the iTunes store does it. Songs and music videos, for example, share the same set of fields, but audiobooks and software have different data structures. The JSON data makes this distinction using the kind field.

Let’s modify our data model to load the value for the above key.

➤ Add the following property to SearchResult (SearchResult.swift):

var kind: String? = ""

You might think that the “kind” property would always be there in the iTunes data and so it need not be an optional. I thought so too, but unfortunately, iTunes proved me wrong :] So we go with an optional value there…

➤ Also modify the return line for description to:

return "\nResult - Kind: \(kind ?? "None"), Name: \(name), Artist Name: \(artistName ?? "None")"

That makes sense given that kind is optional, right?

➤ Run the app and do a search. Look at the Xcode output.

When I did this, Xcode showed three different types of products, with the majority of the results being songs. What you see may vary, depending on what you search for.

URL: 'https://itunes.apple.com/search?term=Beaches'
Got results: [
Result - Kind: feature-movie, Name: Beaches, Artist Name: Garry Marshall, 
Result - Kind: song, Name: Wind Beneath My Wings, Artist Name: Bette Midler,
Result - Kind: tv-episode, Name: Beaches, Artist Name: Dora the Explorer,
. . .

Now, let’s add some new properties to the SearchResult object.

Always check the documentation

But first, if you were wondering how I knew how to interpret the data from the iTunes web service, or even how to set up the URLs to use the service in the first place, then you should realize there is no way you can be expected to use a web service if there is no documentation.

Fortunately, for the iTunes store web service, there is some good documentation here:

affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api

Just reading the docs is often not enough though. You have to play with the web service a bit to know what you can and cannot do.

There are some things that the StoreSearch app needs to do with the search results that were not clear from reading the documentation. So, first read the docs and then play with it. That goes for any API, really, whether it’s something from the iOS SDK or a web service.

Load more properties

The current SearchResult class only has a few properties. As you’ve seen, the iTunes store returns a lot more information than that, so you’ll need to add a few new properties.

➤ Add the following properties to SearchResult.swift:

var trackPrice: Double? = 0.0
var currency = ""
var artworkUrl60 = ""
var artworkUrl100 = ""
var trackViewUrl: String? = ""
var primaryGenreName = ""

You’re not including everything that the iTunes store returns, only the fields that are relevant to this app. Also, note that you’ve named the properties to match the keys in the JSON data exactly and that only some have been marked as optional.

Note: The optionality of the properties was based on my own results. It is possible that with the above code, you still find that the app barfs all over the place with an error like this:

URL: 'https://itunes.apple.com/search?term=Macky'
JSON Error: keyNotFound(CodingKeys(stringValue: "trackViewUrl", intValue: nil), Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "results", intValue: nil), _JSONKey(stringValue: "Index 1", intValue: 1)], debugDescription: "No value associated with key CodingKeys(stringValue: \"trackViewUrl\", intValue: nil) (\"trackViewUrl\").", underlyingError: nil))

If this happens to you, look at the error message to figure out the property in SearchResult which is missing and then mark it as optional — problem solved!

SearchResult stores the item’s price and the currency — US dollar, Euro, British Pounds, etc. It also stores two artwork URLs, one for a 60×60 pixel image and the other for a 100×100 pixel image, a link to the product’s page on the iTunes store, and the genre of the item.

Provided the class supports Codable, with just the simple addition of new properties — as long at they are named the same as the JSON keys and have the right optionality — you are now able to load these new values into your class.

But what if you don’t want to use the not-quite-user-friendly names from the JSON data such as artworkUrl60 or artworkUrl100 but instead want to use more descriptive names such as artworkSmall and artworkLarge?

Never fear, Codable has support for that too :]

But before we get to that, you should run your app once to make sure that the above code changes didn’t break anything. So, run your app, make a search, and verify that you still get output in the Xcode Console indicating that the search was successful.

All working fine? Great! Let’s move on to naming the SearchResults properties to be as you want them and not as the JSON data sets them …

Support better property names

➤ Replace the following lines of code in SearchResult.swift:

var artworkUrl60 = ""
var artworkUrl100 = ""
var trackViewUrl: String? = ""
var primaryGenreName = ""

With this:

var imageSmall = ""
var imageLarge = ""
var storeURL: String? = ""
var genre = ""

enum CodingKeys: String, CodingKey {
  case imageSmall = "artworkUrl60"
  case imageLarge = "artworkUrl100"
  case storeURL = "trackViewUrl"
  case genre = "primaryGenreName"
  case kind, artistName, trackName 
  case trackPrice, currency
}

As you’ll notice, you’ve changed the property names to be more descriptive, but what does the enum do?

As you’ve seen previously, an enum (or enumeration), is a way to have a list of values and names for those values. Here, you use the CodingKeys enumeration to let the Codable protocol know how you want the SearchResult properties matched to the JSON data.

Do note that if you do use the CodingKeys enumeration, it has to provide a case for all your properties in the class — the ones which map to a JSON key with the same name are the last two cases in the enum, you’ll notice that they don’t have a value specified.

That’s all there is to it :] Run your app again (and maybe change the description property to return one of the new values to test they display correctly) and verify that the code still works with the new properties.

Use the results

With these latest changes, searchBarSearchButtonClicked(_:) retrieves an array of SearchResult objects populated with useful information, but you’re not doing anything with that array yet.

➤ Switch to SearchViewController.swift and in searchBarSearchButtonClicked(_:), replace the following lines:

let results = parse(data: data)
print("Got results: \(results)"))

With:

searchResults = parse(data: data)

Instead of placing the results in a local variable and printing them out, you now place the returned array into the searchResults instance variable so that the table view can show the actual search result objects.

➤ Run the app and search for your favorite musician. After a second or so, you should see a whole bunch of results appear in the table. Cool!

The results from the search now show up in the table
The results from the search now show up in the table

Differing data structures

Remember how I said that some items, such as audiobooks have different data structures? Let’s talk about that a bit more in detail…

The biggest differences currently between the other item types and audiobooks is that audiobooks do not have certain JSON keys that are present for other items. Here’s a breakdown:

  1. kind: This value is not present at all.
  2. trackName: Instead of “trackName”, you get “collectionName”.
  3. trackviewUrl: Instead of this value, you have “collectionViewUrl” — which provides the iTunes link to the item.
  4. trackPrice: Instead of “trackPrice”, you get “collectionPrice”.

Interestingly enough, you’ll notice that in SearchResult these are all properties that we had marked as optional. You grok now why we had to mark them optional, right? If your search results included an audiobook item, those properties would not have been there and so Codable would have had a fit :]

Additionally, there are a few other JSON differences for a couple of item types:

  1. Software and e-book items do not have “trackPrice” key, instead they have a “price” key.
  2. E-books don’t have a “primaryGenreName” key — they have an array of genres.

So how can you fix things so that the JSONDecoder can correctly decode the JSON data from the iTunes Store server no matter the type of item? How do you handle the situations where the same property — for example, “trackPrice” — can be present as a different property — like “collectionPrice” or “price” — depending on the type of item?

Remember how you added a computed variable called name which returns the trackName? This is where that comes into play … If you add another variable to store collectionName — the name of the item when it is an audiobook — then you can return the correct value from name depending on the case. You can do something similar for the store URL and price as well.

Let’s make the necessary changes.

➤ Remove the storeURL property from SearchResult — you’ll add two separate optional properties for the audiobook and non-audiobook types. Also remove the storeURL case from CodingKeys.

➤ Remove the genre property from SearchResult — you’ll add two separate optional properties for the e-book and non-e-book types. Also remove the genre case from CodingKeys.

➤ Add new optional properties for the variant keys present in the special items mentioned above:

var trackViewUrl: String?
var collectionName: String?
var collectionViewUrl: String?
var collectionPrice: Double?
var itemPrice: Double?
var itemGenre: String?
var bookGenre: [String]?

➤ Replace the name computed property with the following:

var name: String {
  return trackName ?? collectionName ?? ""
}

The change is simple enough, except for the chaining of the nil-coalescing operator. You check to see if trackName is nil — if not, you return the unwrapped value of trackName. If trackName is nil, you move on to collectionName and do the same check. If both values are nil, you return an empty string.

➤ Add the following three new computed properties:

var storeURL: String {
  return trackViewUrl ?? collectionViewUrl ?? ""
}

var price: Double {
  return trackPrice ?? collectionPrice ?? itemPrice ?? 0.0
}

var genre: String {
  if let genre = itemGenre {
    return genre
  } else if let genres = bookGenre {
    return genres.joined(separator: ", ")
  }
  return ""
}

The first two computed properties work similar to how the name computed property works. So nothing new there. The genre property simply returns the genre for items which are not e-books. For e-books, the method combines all the genre values in the array separated by commas and then returns the combined string.

All that remains is to add all the new properties to the CodingKeys enumeration — if you don’t, some of the values might not be populated correctly during JSON decoding. Once you’re done, CodingKeys should look like this:

enum CodingKeys: String, CodingKey {
  case imageSmall = "artworkUrl60"
  case imageLarge = "artworkUrl100"
  case itemGenre = "primaryGenreName"
  case bookGenre = "genres"
  case itemPrice = "price"
  case kind, artistName, currency
  case trackName, trackPrice, trackViewUrl
  case collectionName, collectionViewUrl, collectionPrice
}

➤ Run the app again, and search for something like “Stephen King” to be sure to get some results which include audiobooks for the master of horror! In case you wonder why that specific search term, we are looking for audiobooks specifically because that is one of the item types with variations in the data structures…

Show the product type

The search results may include podcasts, songs, or other related products. It would be useful to make the table view display what type of product it is showing.

➤ Still in SearchResult.swift, add the following computed properties:

var type: String {
  return kind ?? "audiobook"
}

var artist: String {
    return artistName ?? ""
} 

Remember that kind could be nil if the item type is an audiobook and that we’ve marked artistName as an optional. You hedge against that with these new computed properties.

➤ Open SearchViewController.swift and in tableView(_:cellForRowAt:), change the line that sets cell.artistNameLabel to the following:

if searchResult.artist.isEmpty {
  cell.artistNameLabel.text = "Unknown"
} else {
  cell.artistNameLabel.text = String(
    format: "%@ (%@)", 
    searchResult.artist, 
    searchResult.type)
}

The first change is that you now check that the SearchResult’s artist is not empty. When testing the app I noticed that sometimes a search result did not include an artist name. In that case you make the cell say “Unknown”.

You also add the value of the new type property to the artist name label, which should tell the user what kind of product they’re looking at:

They’re not books…
They’re not books…

There is one problem with this. The value of kind comes straight from the server and it is more of an internal name than something you’d want to show directly to the user.

What if you want it to say “Movie” instead, or maybe you want to translate the app to another language — something you’ll do later for StoreSearch. It’s better to convert this internal identifier, “feature-movie”, into the text that you want to show to the user, “Movie”.

➤ Replace the type computed property in SearchResult.swift with this one:

var type: String {
  let kind = self.kind ?? "audiobook"
  switch kind {
  case "album": return "Album"
  case "audiobook": return "Audio Book"
  case "book": return "Book"
  case "ebook": return "E-Book"
  case "feature-movie": return "Movie"
  case "music-video": return "Music Video"
  case "podcast": return "Podcast"
  case "software": return "App"
  case "song": return "Song"
  case "tv-episode": return "TV Episode"
  default: break
  }
  return "Unknown"
}

These are the types of products that this app understands.

It’s possible that I missed one or that the iTunes Store adds a new product type at some point. If that happens, the switch jumps to the default: case and you’ll simply return a string saying “Unknown” — and hopefully help identify and fix the unknown type in an update of the app.

Default and break

Switch statements often have a default: case at the end that just says break.

In Swift, a switch must be exhaustive, meaning that it must have a case for all possible values of the thing that you’re looking at.

Here you’re looking at kind. Swift needs to know what to do when kind is not any of the known values. That’s why you’re required to include the default: case, as a catchall for any other possible values of kind.

By the way: unlike in other languages, the case statements in Swift do not need to say break at the end. They do not automatically “fall through” from one case to the other as they do in Objective-C.

Now the item type should display not as a value from the web service, but instead, as the value you set for each item type:

The product type is a bit more human-friendly
The product type is a bit more human-friendly

➤ Run the app and search for software, audio books or e-books to see that the parsing code works. It can take a few tries before you find some because of the enormous quantity of products on the store.

Later on, you’ll add a control that lets you pick the type of products that you want to search for, which makes it a bit easier to find just e-books or audiobooks.

Sort the search results

It’d be nice to sort the search results alphabetically. That’s actually quite easy. A Swift Array already has a method to sort itself. All you have to do is tell it what to sort on.

➤ In SearchViewController.swift, in searchBarSearchButtonClicked(_:), right after the call to parse(data:) add the following:

searchResults.sort { result1, result2 in
  return result1.name.localizedStandardCompare(result2.name) == .orderedAscending
}

After the results array is fetched, you call sort on the searchResults array with a trailing closure that determines the sorting rules. This is identical to what you did in Checklists to sort the to-do lists.

In order to sort the contents of the searchResults array, the closure will compare the SearchResult objects with each other and return true if result1 comes before result2. The closure is called repeatedly on different pairs of SearchResult objects until the array is completely sorted.

The comparison of the two objects uses localizedStandardCompare() to compare the names of the SearchResult objects. Because you used .orderedAscending, the closure returns true only if result1.name comes before result2.name — in other words, the array gets sorted from A to Z.

➤ Run the app and verify that the search results are sorted alphabetically.

The search results are sorted by name
The search results are sorted by name

Sorting was pretty easy to add, but there is an even easier way to write this.

Improve the sorting code

➤ Change the sorting code you just added to:

searchResults.sort { $0.name.localizedStandardCompare($1.name) == .orderedAscending }

Now, inside the closure you no longer refer to the two SearchResult objects by name but as the special $0 and $1 variables. Using this shorthand instead of full parameter names is common in Swift closures. There is also no longer a return statement.

➤ Verify that this works.

Believe it or not, you can do even better. Swift has a very cool feature called operator overloading. It allows you to take the standard operators such as + or * and apply them to your own objects. You can even create completely new operator symbols.

It’s not a good idea to go overboard with this feature and make operators do something completely unexpected — don’t overload / to do multiplications, eh? — but it comes in very handy for sorting.

➤ Open SearchResult.swift and add the following code, outside of the class:

func < (lhs: SearchResult, rhs: SearchResult) -> Bool {
  return lhs.name.localizedStandardCompare(rhs.name) == .orderedAscending
}

This should look familiar! You’re creating a function named < that contains the same code as the closure from earlier. This time, the two SearchResult objects are called lhs and rhs, for left-hand side and right-hand side, respectively.

You have now overloaded the less-than operator so that it takes two SearchResult objects and returns true if the first one should come before the second, and false otherwise. Like so:

searchResultA.name = "Waltz for Debby"
searchResultB.name = "Autumn Leaves"

searchResultA < searchResultB  // false
searchResultB < searchResultA  // true

➤ Back in SearchViewController.swift, change the sorting code to:

searchResults.sort { $0 < $1 }

That’s pretty sweet. Using the < operator makes it very clear that you’re sorting the items from the array in ascending order.

But wait, you can write it even shorter:

searchResults.sort(by: <)

Wow, it doesn’t get much simpler than that! This line literally says, “Sort this array in ascending order”. Of course, this only works because you added your own func < to overload the less-than operator so it takes two SearchResult objects and compares them.

➤ Run the app again and make sure everything is still sorted.

Exercise: See if you can make the app sort by the artist name instead.

Exercise: Try to sort in descending order, from Z to A. Tip: use the > operator.

Excellent! You made the app talk to a web service and you were able to convert the data that was received into your own data model object.

The app may not support every product that’s shown on the iTunes store, but I hope it illustrates the principle of how you can take data that comes in slightly different forms and convert it to objects that are more convenient to use in your own apps.

Feel free to dig through the web service API documentation to add the remaining items that the iTunes store sells: https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/

➤ Commit your changes with a message such as “Add fetching data from web service using synchronous network request”.

You can find the project files for this chapter under 34-Networking in the Source Code folder.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.