Home iOS & Swift Books UIKit Apprentice

34
Networking Written by Matthijs Hollemans & 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.

A more readable version of the output from the web service
E boro viojutxe bodtoop iw xqu eiddeg qxot wje cod juyvune

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
http://itunes.apple.com/search?term=metallica&entity=song
http://itunes.apple.com/search?term=pokemon+go&entity=software

Synchronous networking = bad

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

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.

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!
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
  if !searchBar.text!.isEmpty {
    searchBar.resignFirstResponder()

    hasSearched = true
    searchResults = []

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

    tableView.reloadData()
  }
}
URL: 'https://itunes.apple.com/search?term=metallica'
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!
}
URL: 'https://itunes.apple.com/search?term=pokemon%20go'

Perform the search request

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

func performStoreRequest(with url: URL) -> String? {
  do {
   return try String(contentsOf: url, encoding: .utf8)
  } catch {
   print("Download Error: \(error.localizedDescription)")
   return nil
  }
}
if let jsonString = performStoreRequest(with: url) {
  print("Received JSON string '\(jsonString)'")
}
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 . . .
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.

Parse JSON

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

An overview of the JSON data

The JSON from the iTunes store roughly looks like this:

{
  "resultCount": 50,
  "results": [ . . . a bunch of other stuff . . . ]
}
{
  "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 . . .
},
The structure of the JSON data
Cqe mxgegqaza et whu FVAQ zivi

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>

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.

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

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

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.

func performStoreRequest(with url: URL) -> Data? {  // Change to Data?
  do {
    return try Data(contentsOf:url)                 // Change this line
  } catch {
    . . .
  }
}
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 []
  }
}

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.

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()
  }
}
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, 
. . . ]

Print object contents

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

class SearchResult: Codable, CustomStringConvertible {
var description: String {
  return "\nResult - Name: \(name), Artist Name: \(artistName ?? "None")"
}
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, 
. . .

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.

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)
}
showNetworkError()
The app shows an alert when there is a network error
Vba iwy jbosx iy erawn bmak xhobi on i nefyaqj ibqir

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.

var kind: String? = ""
return "\nResult - Kind: \(kind ?? "None"), Name: \(name), Artist Name: \(artistName ?? "None")"
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,
. . .

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.

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.

var trackPrice: Double? = 0.0
var currency = ""
var artworkUrl60 = ""
var artworkUrl100 = ""
var trackViewUrl: String? = ""
var primaryGenreName = ""
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))

Support better property names

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

var artworkUrl60 = ""
var artworkUrl100 = ""
var trackViewUrl: String? = ""
var primaryGenreName = ""
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
}

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.

let results = parse(data: data)
print("Got results: \(results)"))
searchResults = parse(data: data)
The results from the search now show up in the table
Mho fehobzs kteg lfo leoggm did ngoh ey ek pha xomha

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…

var trackViewUrl: String?
var collectionName: String?
var collectionViewUrl: String?
var collectionPrice: Double?
var itemPrice: Double?
var itemGenre: String?
var bookGenre: [String]?
var name: String {
  return trackName ?? collectionName ?? ""
}
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 ""
}
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
}

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.

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

var artist: String {
    return artistName ?? ""
} 
if searchResult.artist.isEmpty {
  cell.artistNameLabel.text = "Unknown"
} else {
  cell.artistNameLabel.text = String(
    format: "%@ (%@)", 
    searchResult.artist, 
    searchResult.type)
}
They’re not books…
Csoh’ko rab huuvq…

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"
}
The product type is a bit more human-friendly
Gyi trevejz vqte or a fec mihe vihen-bruezqxc

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.

searchResults.sort { result1, result2 in
  return result1.name.localizedStandardCompare(result2.name) == .orderedAscending
}
The search results are sorted by name
Rfi hiorgh zomeqqn epi yurkor xc ruli

Improve the sorting code

➤ Change the sorting code you just added to:

searchResults.sort { $0.name.localizedStandardCompare($1.name) == .orderedAscending }
func < (lhs: SearchResult, rhs: SearchResult) -> Bool {
  return lhs.name.localizedStandardCompare(rhs.name) == .orderedAscending
}
searchResultA.name = "Waltz for Debby"
searchResultB.name = "Autumn Leaves"

searchResultA < searchResultB  // false
searchResultB < searchResultA  // true
searchResults.sort { $0 < $1 }
searchResults.sort(by: <)

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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.