Creating the client

Now that the utilities are complete, the GitHub client API can be created. Once that is complete, it can be integrated with the user interface.

Talking to the GitHub API

A Swift class will be created to talk to the GitHub API. This will connect to the root endpoint host and download the JSON for the service URLs so that subsequent network connections can be made.

To ensure that network requests are not repeated, an NSCache will be used to save the responses. This will automatically be emptied when the application is under memory pressure:

import Foundation
class GitHubAPI {
  let base:NSURL
  let services:[String:String]
  let cache = NSCache()
  class func connect() -> GitHubAPI? {
    return connect("https://api.github.com")
  }
  class func connect(url:String) -> GitHubAPI? {
    if let nsurl = NSURL(string:url) {
      return connect(nsurl)
    } else {
      return nil
    }
  }
  class func connect(url:NSURL) -> GitHubAPI? {
    if let data = NSData(contentsOfURL:url) {
      if let json = NSJSONSerialization.JSONObjectWithData(
       data,options:nil,error:nil) as? [String:String] {
        return GitHubAPI(url,json)
      } else {
       return nil
      }
    } else {
      return nil
    }
  }
  init(_ base:NSURL, _ services:[String:String]) {
    self.base = base
    self.services = services
  }
}

Tip

As Swift 1.0 doesn't support conditional initializers, the pattern of using a class function to perform the initialization and return an optional value is a common one. With Swift 1.1, the implementation can be written as a convenience initializer init?, which can return an instance or nil.

This can be tested by saving the response from the main GitHub API site at https://api.github.com into an api/index.json file, by creating an api directory in the root level of the project and running curl https://api.github.com > index.json from a Terminal prompt. Inside Xcode, add the api directory to the project by navigating to File | Add Files to Project... or by pressing Command + Option + A, and ensure it is associated with the test target.

It can then be accessed with an NSBundle:

import XCTest
class GitHubAPITests: XCTestCase{
  func testApi() {
    let bundle = NSBundle(forClass:GitHubAPITests.self)
    if let url = bundle.URLForResource("api/index",
     withExtension:"json") {
      if let api = GitHubAPI.connect(url) {
        XCTAssertTrue(true,"Created API")
      } else {
        XCTAssertFalse(true,"Failed to parse (url)")
      }
    } else {
      XCTAssertFalse(true,"Failed to find sample API")
    }
  }
}

Tip

The dummy API should not be part of the main application's target, but rather of the test target. As a result, instead of using NSBundle.mainBundle to acquire the application's bundle, NSBundle(forClass) is used.

Returning repositories for a user

The APIs returned from the services lookup include user_repositories_url, which is a template that can be instantiated with a specific user. It is possible to add a method to the GitHubAPI class that will return the URL of the user's repositories as follows:

func getURLForUserRepos(user:String) -> NSURL {
  let userRepositoriesURL = services["user_repositories_url"]!
  let userRepositoryURL = URITemplate.replace(
   userRepositoriesURL, values:["user":user])
  let url = NSURL(string:userRepositoryURL, relativeToURL:base)!
  return url
}

As this might be called multiple times, the URL should be cached based on the user:

func getURLForUserRepos(user:String) -> NSURL {
  let key = "r:(user)"
  if let url = cache.objectForKey(key) as? NSURL {
    return url
  } else {
    // acquire url as before
    cache.setObject(url, forKey:key)
    return url
  }
}

Once the URL is known, data can be parsed as an array of JSON objects using an asynchronous callback function to notify when the data is ready:

func withUserRepos(user:String, fn:([[String:String]]) -> ()) {
  let key = "repos:(user)"
  if let repos = cache.objectForKey(key) as? [[String:String]] {
    fn(repos)
  } else {
    let url = getURLForUserRepos(user)
    url.withJSONArrayOfDictionary {
      repos in
      self.cache.setObject(repos,forKey:key)
      fn(repos)
    }
  }
}

This can be tested using a simple addition to the GitHubAPITests class:

api.withUserRepos("alblue") {
  array in
  XCTAssertEqual(22,array.count,"Number of repos")
}

Note

The sample data contains 22 repositories in the following file, but the GitHub API might contain a different value for this user in the future: https://raw.githubusercontent.com/alblue/com.packtpub.swift.essentials/master/RepositoryBrowser/api/users/alblue/repos.json.

Accessing data through the AppDelegate

When building an iOS application that manages data, deciding where to declare the variable is the first decision to be made. When implementing a view controller, it is common for view-specific data to be associated with that class; but if the data needs to be used across multiple view controllers, there is more choice.

A common approach is to wrap everything into a singleton, which is an object that is instantiated once. This is typically achieved with private var in the implementation class, with class func that returns (or instantiates on demand) the singleton.

Tip

The Swift private keyword ensures that the variable is only visible in the current source file. The default visibility is internal, which means that code is only visible in the current module; the public keyword means that it is visible outside of the module as well.

Another approach is to use the AppDelegate itself. This is in effect already a singleton that can be accessed with UIApplication.sharedApplication().delegate, and is set up prior to any other object accessing it.

The AppDelegate is used to store the reference to the GitHubAPI, which could use a preference store or other external means to define what instance to connect to, along with the list of users and a cache of repositories:

class AppDelegate {
  var api:GitHubAPI!
  var users:[String] = []
  var repos:[String:[String]] = [:]
  func application(application: UIApplication,
   didFinishLaunchingWithOptions: [NSObject: AnyObject]?)
   -> Bool {
    api = GitHubAPI.connect()
    users = ["alblue"]
    return true
  }
}

To facilitate loading repositories from view controllers, a function can be added to AppDelegate to provide a list of repositories for a given user:

func loadRepoNamesFor(user:String, fn:()->()) {
  repos[user] = []
  api.withUserRepos(user) {
    results in
    self.repos[user] = results.map {
      (r:[String:String]) -> String
      in r["name"]!
    }
    fn()
  }
}
..................Content has been hidden....................

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