The RepositoryBrowser project

The client is a Master Detail application called RepositoryBrowser. This sets up a template that can be used on a large device with a split view controller, or a navigator view controller on a small device. In addition, actions to add entries are created.

To build the APIs necessary to display content, several utility classes are needed:

  • The URITemplate class processes URI templates with a set of key/value pairs
  • The Threads class allows functions to be run in the background or in the main thread
  • The NSURLExtensions class provides easy parsing JSON objects from a URL
  • The DictionaryExtensions class provides a means of creating a Swift dictionary from a JSON object
  • The GitHubAPI class provides access to the GitHub remote API

URI templates

URI templates are defined in RFC 6570 at https://tools.ietf.org/html/rfc6570. They can be used to replace sequences of text surrounded by {} in a URI. Although GitHub's API uses optional values {?...}, the example client presented in this chapter will not need to use these, and so they can be ignored in this implementation.

The template class replaces the parameters with values from a dictionary. To create the API, it is useful to write a test case first, following test-driven development. A test case class can be created by navigating to File | New | File... | iOS | Source | Test Case Class and creating a subclass of XCTestCase in Swift. The test code will look like:

import XCTest
class URITemplateTests: XCTestCase {
  func testURITemplate() {
    let template = "http://example.com/{blah}/blah/{?blah}"
    let replacement = URITemplate.replace(
     template,values: ["blah":"foo"])
    XCTAssertEqual("http://example.com/foo/blah/",
     replacement,"Template replacement")
  }
}

The replace function requires string processing. Although the function could be a class function or an extension on String, having it as a separate class makes testing easier. The function signature looks like:

import Foundation
class URITemplate {
  class func replace(template:String, values:[String:String])
   -> String {
    var replacement = template
    while true {
      // replace until no more {…} are present
    }
    return replacement
  }
}

Tip

Make sure that the URITemplate class is added to the test target as well; otherwise, the test script will not compile.

The parameters are matched using a regular expression such as {[^}]*}. To search or access this from a string involves a Range of String.Index values. These are like integer indexes into the string, but instead of referring to a character by its byte offset, the index is an abstract representation. (Some character encodings such as UTF-8 use multiple bytes to represent a single character.)

The rangeOfString method takes a string or regular expression and returns a range if there is a match present (or nil if there isn't). This can be used to detect whether a pattern is present, or to break out of the while loop:

// replace until no more {…} are present
if let parameterRange = replacement.rangeOfString(
  "\{[^}]*\}",
  options: NSStringCompareOptions.RegularExpressionSearch) {
  // perform a replacement of parameterRange
} else {
  break
}

The parameterRange contains a start and end index that represent the locations of the { and } characters. The value of the parameter can be extracted with replacement.substringWithRange(parameterRange). If it starts with {?, it is replaced with an empty string:

// perform a replacement of parameterRange
var value:String
let parameter = replacement.substringWithRange(parameterRange)
if parameter.hasPrefix("{?") {
  value = ""
} else {
  // substitute with real replacement
}
replacement.replaceRange(parameterRange, with: value)

Finally, if the replacement is of the form {user}, then the value of user is acquired from the dictionary and used as the replacement value. To get the name of the parameter, startIndex has to be advanced to the successor, and endIndex has to be reversed to the predecessor to account for the { and } characters:

// substitute with real replacement
let start = parameterRange.startIndex.successor()
let end = parameterRange.endIndex.predecessor()
let name = replacement.substringWithRange(
 Range<String.Index>(start:start,end:end))
value = values[name] ?? ""

Now, when the test is run by navigating to Product | Test or by pressing Command + U, the string replacement will pass.

Note

The ?? is an optional test that is used to return the first argument, if it is present, and the second argument, if it is nil.

Background threading

Background threading allows functions to be trivially launched on the UI thread or on a background thread, as appropriate. This was explained in Chapter 6, Parsing Networked Data, in the Networking and user interface section. Add the following as Threads.swift:

import Foundation
class Threads {
  class func runOnBackgroundThread(fn:()->()) {
    dispatch_async(dispatch_get_global_queue(
     DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),fn)
  }
  class func runOnUIThread(fn:()->()) {
    if(NSThread.isMainThread()) {
      fn()
    } else {
      dispatch_async(dispatch_get_main_queue(), fn)
    }
  }
}

The Threads class can be tested with the following test case:

import XCTest
class ThreadsTest: XCTestCase {
  func testThreads() {
    Threads.runOnBackgroundThread {
      XCTAssertFalse(NSThread.isMainThread(), 
       "Running on background thread")
      Threads.runOnUIThread {
        XCTAssertTrue(NSThread.isMainThread(),
         "Running on UI thread")
      }
    }
  }
}

When the tests are run by pressing Command + U, the tests should pass.

Parsing JSON dictionaries

As many network responses are returned in JSON format, and to make JSON parsing easier, extensions can be added to the NSURL class to facilitate acquiring and parsing of content loaded from network locations. Instead of designing a synchronous extension that blocks until data is available, using a callback function is better practice. Create a file named NSURLExtensions.swift with the following content:

extension NSURL {
  func withJSONDictionary(fn:[String:String] -> ()) {
    let session = NSURLSession.sharedSession()
    session.dataTaskWithURL(self) {
      data,response,error -> () in
      if let json = NSJSONSerialization.JSONObjectWithData(
        data, options: nil, error: nil) as? [String:AnyObject] {
        // fn(json)
      } else {
        fn([String:String]())
      }
    }.resume()
  }
}

This provides an extension for an NSURL to provide a JSON dictionary. However, the data type returned from the JSONObjectWithData method is [String:AnyObject], not [String:String]. Although it might be expected that it could just be cast to the right type, the as will perform a test and if there are mixed values (such as a number or nil) then the entire object is considered invalid. Instead, the JSON data structure must be converted to a [String:String] type. Add the following as a standalone function to NSURLExtensions.swift:

func toStringString(dict:[String:AnyObject]) -> [String:String] {
  var result:[String:String] = [:]
  for (key,value) in dict {
    if let valueString = value as? String {
      result[key] = valueString
    } else {
      result[key] = "(value)"
    }
  }
  return result
}

This can be used to convert [String:AnyObject] in the JSON function:

fn(toStringString(json))

The function can be tested with a test class using the data: protocol by passing in a base64 encoded string representing the JSON data. To create a base64 representation, create a string, convert it to a UTF-8 data object, and then convert it back to a string representation with a data: prefix:

import XCTest
class NSURLExtensionsTest: XCTestCase {
  func testNSURLJSON() {
    let json = "{"test":"value"}".
     dataUsingEncoding(NSUTF8StringEncoding)!
    let base64 = json.base64EncodedDataWithOptions(nil)
    let data = NSString(data: base64, 
     encoding: NSUTF8StringEncoding)!
    let dataURL = NSURL(string:"data:text/plain;base64,(data)")!
    dataURL.withJSONDictionary {
      dict in
      XCTAssertEqual(dict["test"] ?? "", "value",
       "Value is as expected")
    }
  }
}

Parsing JSON arrays of dictionaries

A similar approach can be used to parse arrays of dictionaries (such as those returned by the list repositories resource). The differences here are the type signatures (which have extra square brackets [] to represent the array) and the fact that a map is being used to process the elements in the list:

func withJSONArrayOfDictionary(fn:[[String:String]] -> ()) {
  … 
  if let json = NSJSONSerialization.JSONObjectWithData(
   data, options: nil, error: nil) as? [[String:AnyObject]] {
    fn(json.map(toStringString))
  } else {
    fn([[String:String]]())
  }

The test can be extended as well:

let json = "[{"test":"value"}]".
 dataUsingEncoding(NSUTF8StringEncoding)!
…
dataURL.withJSONArrayOfDictionary {
  dict in XCTAssertEqual(dict[0]["test"] ?? "", "value",
 "Value is as expected")
}
..................Content has been hidden....................

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