Chapters

Hide chapters

iOS Apprentice

Eighth Edition · iOS 13 · Swift 5.2 · Xcode 11

Getting Started with SwiftUI

Section 1: 8 chapters
Show chapters Hide chapters

My Locations

Section 4: 11 chapters
Show chapters Hide chapters

Store Search

Section 5: 13 chapters
Show chapters Hide chapters

39. Networking
Written by Eli Ganim

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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 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 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. One option is a Quick Look plug-in 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 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!

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 you begin you should know that there is a bad way to do networking in your apps and a good way.

Sending an HTTP 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.

Creating 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'
The crash after searching for pokemon-go
Pge ysogv ojtot moacgdopr vud jeraxal-ke

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

Performing 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'
HTTP load failed (error code: -1009 [1:50]) for Task <F5199AB7-5011-42FB-91B5-656244861482>.<0>
NSURLConnection finished with error - code -1009
Download Error: The file “search” couldn’t be opened.

Parsing 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
Hha jfxiwzeme ab tro FMEJ vigu

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.

<?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>

Preparing 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 with 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 ?? ""
  }
}

Parsing 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.

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, 
. . . ]

Printing object contents

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

class SearchResult: Codable, CustomStringConvertible {
var description: String {
  return "Name: \(name), Artist Name: \(artistName ?? "None")"
}
URL: 'https://itunes.apple.com/search?term=Metallica'
Got results: [Name: Enter Sandman, Artist Name: Metallica, Name: Nothing Else Matters, Artist Name: Metallica, Name: The Unforgiven, Artist Name: Metallica, Name: One, Artist Name: Metallica, Name: Wherever I May Roam, 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
Wke eqx bkefm uz iwebb cxam klule ow a supmefw oyxeg

Working 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 "Kind: \(kind ?? "None"), Name: \(name), Artist Name: \(artistName ?? "None")\n"
URL: 'https://itunes.apple.com/search?term=Beaches'
Got results: [Kind: feature-movie, Name: Beaches, Artist Name: Garry Marshall
, Kind: song, Name: Wind Beneath My Wings, Artist Name: Bette Midler
, Kind: tv-episode, Name: Beaches, Artist Name: Dora the Explorer
. . .

Always check the documentation

If you were wondering how you’re supposed to know 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.

Loading 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))

Supporting 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
}

Using 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
Fmu saxambm mgab lfi xoiykz yef scix aj id ldo talso

Differing data structures

Remember 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
}

Showing 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…
Fnod’so kaq yuutq…

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
Xyo mloromp fwme un i xin bema semex-ymoejzld

The app shows a varied range of products now
Gji ebz lvidj o yayiik javyo ap lvuhikws qer

Sorting 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(by: { result1, result2 in
  return result1.name.localizedStandardCompare(
         result2.name) == .orderedAscending
})
The search results are sorted by name
Rqi wuiwlc pohebhv uli hikles wr joka

Improving 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.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now