Chapter 6. Parsing Networked Data

Many iOS applications need to communicate with other servers or devices. This chapter presents both HTTP and non-HTTP networking in Swift and how data can be parsed from either JSON or XML. It first demonstrates how to load data efficiently from URLs followed by how to stream larger data responses. It then concludes with how to perform both synchronous and asynchronous network requests over protocols other than HTTP.

This chapter will present the following topics:

  • Loading data from URLs
  • Updating the user interface from a background thread
  • Parsing JSON and XML data
  • Stream-based connections
  • Asynchronous data communication

Loading data from URLs

The most common way to load data from a remote network source is to use an HTTP (or HTTPS) URL of the form https://raw.githubusercontent.com/alblue/com.packtpub.swift.essentials/master/CustomViews/CustomViews/SampleTable.json.

URLs can be manipulated with the NSURL class, which comes from the Foundation module (which is transitively imported from the UIKit module). The main NSURL initializer takes a String initializer with a full URL, although other initializers exist for creating relative URLs or for references to file paths.

The NSURLSession class is typically used to perform operations with URLs, and individual sessions can be created through the initializer or the standard shared session can be used.

Tip

The NSURLConnection class was used in older versions of iOS and Mac OS X. References to this class might still be seen in some tutorials, or might be required if Mac OS X 10.8 or iOS 6 needs to be supported; otherwise, the NSURLSession class should be preferred.

The NSURLSession class provides a means to create tasks. These include:

  • Data task: This can be used to process network data programmatically
  • Upload task: This can be used to upload data to a remote server
  • Download task: This can be used to download to a local storage or to resume a previous or partial download

Tasks are created from the NSURLSession class methods and can take a URL argument and an optional completion handler. A completion handler is a lot like a delegate, except that it can be customized per task, and is usually represented as a function.

Tasks can be suspended or resumed to stop and start the process. Tasks are created in a suspended state by default, so they have to be initially resumed to start processing.

When a data task is completed, the completion handler is called back with three arguments: an NSData object, which represents the returned data; an NSURLResponse object, which represents the response from the remote URL server; and an optional NSError object if anything failed during the request.

With this in place, the SampleTable created in the previous chapter can load data from a network URL by obtaining a session, initiating a data task, and then resuming it. The completion handler will be called when the data is available, which can be used to add the content to the table.

Modify the viewDidLoad method of the SampleTable class to load the SampleTable.json file as follows:

let url = NSURL(string: "https://raw.githubusercontent.com/
  alblue/com.packtpub.swift.essentials/master/
  CustomViews/CustomViews/SampleTable.json")!
let session = NSURLSession.sharedSession()
let encoding = NSUTF8StringEncoding
let task = session.dataTaskWithURL(url,completionHandler:
 {data,response,error -> Void in
  let contents = String(NSString(data:data,encoding:encoding)!)
  self.items += [(url.absoluteString!,contents)]
  // table data won't reload – needs to be on ui thread
  self.tableView.reloadData()
})
task.resume()

This creates an NSURL and an NSURLSession, and then creates a data task and immediately resumes it. After the content is downloaded, the completion handler is called, which passes the data as an NSData object. The NSString initializer is used to decode UTF8 text from the NSData object and is explicitly cast to a String so that it can be added to the items list.

Note

The NSURLSession class also provides other factory methods, including one that takes a configuration argument that includes options such as whether responses should be cached, whether network connections should go over the cellular network, and whether any cookies or other headers should be sent with the task.

Finally, the item is added to the items and the tableView is reloaded to show the new data. Note that this does not work immediately if it is not run on the main UI thread; the table has to be rotated or moved in order to redraw the display. Running on the UI thread is covered in the Networking and user interface section later in this chapter.

Dealing with errors

Errors are a fact of life, especially on mobile devices with intermittent connectivity. The completion handler is called with a third argument, which represents any error raised during the operation. If this is nil then the operation was a success; if not then the localizedDescription property of the error can be used to notify the user.

For testing purposes, if an error is detected add the localizedDescription to the items in the list. Modify the viewDidLoad method as follows:

let task = session.dataTaskWithURL(url, completionHandler:
 {data,response,error -> Void in
  if (error == nil) {
    let contents = String(NSString(data: data,encoding:encoding)!)
    self.items += [(url.absoluteString!,contents)]
  } else {
    self.items += [("Error",error.localizedDescription)]
  }
  // table data won't reload – needs to be on UI thread
  self.tableView.reloadData()
})

An error can be simulated using a nonexistent hostname or an unknown protocol in the URL.

Dealing with missing content

Errors are reported if the remote server cannot be contacted, such as when the hostname is incorrect or the server is down. If the server is operational, then an error will not be reported. It is still possible that the file requested is not found or the server experiences an error while serving the request. These are reported with HTTP status codes.

Note

If an HTTP URL is not found, the server sends back a 404 status code. This can be used by the client to determine whether a different file should be accessed or whether another server should be queried. For example, browsers will often ask the server for a favicon.ico file and use this to display a small logo; if it is missing, then a generic page icon is shown instead. In general, 4xx responses are client errors, while 5xx responses are server errors.

The NSURLResponse object doesn't have the concept of an HTTP status code, because it can be used for any protocol, including ftp. However, if the request used HTTP, then the response is likely to be HTTP and so the request can be cast to an NSURLHttpResponse, which has a statusCode property. This can be used to provide more specific feedback when the file is not found. Modify the code as follows:

if (error == nil) {
  let httpResponse = response as NSHTTPURLResponse
  let statusCode = httpResponse.statusCode
  if (statusCode >= 400 && statusCode < 500) {
    self.items += [("Client error (statusCode)",
     url.absoluteString!)]
  } else if (statusCode >= 500) {
    self.items += [("Server error (statusCode)",
     url.absoluteString!)]
  } else {
    let contents = String(NSString(data:data,encoding:encoding)!)
    self.items += [(url.absoluteString!,contents)]
  }
} else {...}

Now if the server responds but indicates that either the client made a bad request or the server suffered a problem, the user interface will be updated appropriately.

Nested if and switch statements

Sometimes, the error handling logic can get convoluted by handling different cases, particularly if there are different values that need to be tested. In the previous section, both the NSError and HTTP statusCode needed to be checked.

An alternative approach is to use a switch statement with where clauses. These can be used to test multiple different conditions and also show which part of the condition is being tested. Although a switch statement requires a single expression, it is possible to use a tuple to group multiple values into a single expression.

Another advantage of using a tuple is that it permits the cases to be matched on types. In the case of networking, some URLs are based on http or https, which means that the response will be of an NSHTTPURLResponse type. However, if the URL is of a different type (such as a file or ftp protocol), then it will be of a different subtype of NSURLResponse. Unconditionally casting to NSHTTPURLResponse with as will fail in these cases and cause a crash.

The tests can be rewritten as a switch block as follows:

switch (data,response,error) {
  case (_,_,let e) where e != nil:
    self.items += [("Error",error.localizedDescription)]
  case (_,let r as NSHTTPURLResponse,_) 
   where r.statusCode >= 400 && r.statusCode < 500:
    self.items += [("Client error (r.statusCode)",
     url.absoluteString!)]
  // see note below
  case (_,let r as NSHTTPURLResponse,_) 
   where r.statusCode >= 500:
    self.items += [("Server error (r.statusCode)",
      url.absoluteString!)]
  default:
    let contents = String(NSString(data: data,encoding:encoding)!)
    self.items += [(url.absoluteString!,contents)]
}

In this example, the default block is used to execute the success condition and the prior case statements are used to match the error conditions that can be seen.

Note

The final case statement causes Swift 1.1 in Xcode 6.1 to hang with 100 percent CPU utilization; as a result, it is commented out in the GitHub repository. A future version of Xcode is likely to fix this behavior. This bug occurs because the second and third case statements match the same expression with different where clauses.

The case (_,_,let e) where e != nil pattern is an example of a conditional pattern match. The underscore, which is called a wildcard pattern in Swift (also known as a hole in other languages), is something that will match any value. The third parameter, let e, is a value binding pattern and has the effect of let e = error in this case. Finally, the where clause adds the test to ensure that this case only occurs when e is not nil.

Tip

It is possible to use the error identifier instead of let e in the case statement; using case (_,_,_) where error != nil will have the same effect. However, it is a bad practice to capture values outside the switch statement for case matching purposes, since if the error variable is renamed, then the case statement might become invalid. Generally use let patterns inside case statements to ensure that the correct expression value is being matched.

The second and third cases perform a let assignment, a type test, and conversion. When case (_,let r as NSHTTPURLResponse,_) is matched, not only is the value of this part in the tuple assigned the constant r, but it is also cast to NSHTTPURLRepsonse. If the value is not of type NSHTTPURLResponse, then the case statement is automatically skipped. This is equivalent to an if test with an is expression, followed by a cast with as.

Although the patterns are the same in both, the where clauses are different. The first where clause looks for the case where r.statusCode is 400 or greater and less than 500, while the second is matched where r.statusCode is 500 or greater. (Note that as described earlier, the duplicate case causes an infinite loop in the Swift compiler for Xcode Version 6.1.)

Tip

Regardless of whether nested if statements or the switch statement is used, the code that performs the test is likely to be very similar. It typically comes down to developer preference, but more developers are likely to be familiar with nested if statements. In Swift, the switch statement is more powerful than in other languages, so this kind of pattern is likely to become more popular.

Networking and user interfaces

One outstanding problem with the current callback approach is that the callback cannot be guaranteed to be called from the main thread. As a result, user interface operations might not work correctly or might throw errors. The right solution is to set up another call using the main thread.

Accessing the main thread in Swift is done in the same way as in Objective-C: using Grand Central Dispatch or GCD. The main queue can be accessed with dispatch_get_main_queue, which is used by the thread that all UI updates should occur on. Background tasks are submitted with dispatch_async to a queue. To invoke the reloadData call on the main thread, wrap it as follows:

dispatch_async(dispatch_get_main_queue(), {
  self.tableView.reloadData()})

This style of call will be valid for both Objective-C and Swift (although Objective-C uses the ^ (caret) as a block prefix). However, Swift has a special syntax to deal with functions that take blocks; the block can be promoted out of the function's argument and left as a trailing argument. This is known as a trailing closure:

dispatch_async(dispatch_get_main_queue()) {
  self.tableView.reloadData()
}

Although this is a minor difference, it makes it look like dispatch_async is more like a keyword such as if or switch which takes a block of code. This can be used for any function whose final argument is a function; there is no special syntax needed in the function definition. Additionally, the same technique works for functions defined outside of Swift; in the case of dispatch_async, the function is defined as a C language function and can be transparently used in a portable way.

Running functions on the main thread

Whenever the UI needs to be updated, the update must be run on the main thread. This can be done using the previous pattern to perform updates, since they will always be threaded. However, it can be a pain to remember to do this each time it is required.

It is possible to build a Swift function that takes another function and runs it on the main thread automatically. NSThread.isMainThread can be used to determine whether the current thread is the UI thread or not, so to run a block of code on the main thread, regardless of whether it's on the main thread or not, the following can be used:

func runOnUIThread(fn:()->()) {
  if NSThread.isMainThread() {
    fn()
  } else {
    dispatch_async(dispatch_get_main_queue(), fn)
  }
}

This allows the code to be submitted to the background thread simply:

self.runOnUIThread(self.tableView.reloadData)

Tip

Note that due to the lack of parenthesis, the reloadData function is not called, but it is passed in as a function pointer. It is dispatched to the correct thread inside the runOnUIThread function.

If there is more than one function that needs to be called, an inline block can be created. Since this can be passed as a trailing closure to the runOnUIThread method, the parenthesis are optional:

self.runOnUIThread {
  self.tableView.backgroundColor = UIColor.redColor()
  self.tableView.reloadData()
  self.tableView.backgroundColor = UIColor.greenColor()
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset