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