Accessing repositories from view controllers

In the MasterViewController (created from the Master Detail template; or a new subclass of a UITableViewController), define an instance variable AppDelegate that is assigned in the viewDidLoad method:

class MasterViewController:UITableViewController {
  var app:AppDelegate!
  override func viewDidLoad() {
    app = UIApplication.sharedApplication().delegate
     as? AppDelegate
    …
  }
}

The table view controller provides data in a number of sections and rows. The numberOfSections method will return the number of users, with the section title being the username (indexed by the users list):

override func numberOfSectionsInTableView(tableView: UITableView)
 -> Int {
  return app.users.count
}
override func tableView(tableView: UITableView,
 titleForHeaderInSection section: Int) -> String? {
  return app.users[section]
}

The numberOfRowsInSection function is called to determine how many rows are present in each section. If the number is not known, 0 can be returned while running a background query to find the right answer:

override func tableView(tableView: UITableView,
 numberOfRowsInSection section: Int) -> Int {
  let user = app.users[section]
  if let repos = app.repos[user] {
    return repos.count
  } else {
    app.loadRepoNamesFor(user) {
      Threads.runOnUIThread {
        tableView.reloadSections(
         NSIndexSet(index: section),
         withRowAnimation: .Automatic)
      }
    }
    return 0
  }
}

Tip

Remember to reload the section on the UI thread, as otherwise the updates won't display correctly.

Finally, the repository name needs to be shown in the value of the cell. If a default UITableViewCell function is used, then the value can be set on the textLabel; if it is loaded from a storyboard prototype cell, then the content can be accessed appropriately using tags:

override func tableView(tableView: UITableView,
 cellForRowAtIndexPath indexPath: NSIndexPath)
 -> UITableViewCell {
  let cell = tableView.dequeueReusableCellWithIdentifier(
   "Cell", forIndexPath: indexPath) as UITableViewCell
  let user = app.users[indexPath.section]
  let repo = app.repos[user]![indexPath.row]
  cell.textLabel.text = repo
  return cell
}

When the application is run, the list of repositories will be shown, grouped by the user.

Accessing repositories from view controllers

Adding users

At the moment, the list of users is hard-coded into the application. It would be preferable to remove this hard-coded list and allow users to be added on demand. Create an addUser function in the AppDelegate class:

func addUser(user:String) {
  users += [user]
  users.sort({ $0 < $1 })
}

This allows the detail controller to call the addUser function and ensure that the list of users is ordered alphabetically.

Note

The $0 and $1 are anonymous parameters expected by the sort function. This is a shorthand form of users.sort({ user1, user2 in user1 < user2}).

The add button can be created in the MasterDetailView in the viewDidLoad method, such that the insertNewObject method is called when tapped:

override func viewDidLoad() {
  super.viewDidLoad()
  let addButton = UIBarButtonItem(barButtonSystemItem: .Add,
   target: self, action: "insertNewObject:")
  self.navigationItem.rightBarButtonItem = addButton
  …
}

When the add button is selected, a UIAlertView dialog can be shown with a delegate that will be called back to add the user. As before, the delegate must maintain a reference to itself until it completes, as otherwise, the object will be deallocated immediately.

Tip

There is a replacement for UIAlertView in iOS 8 called UIAlertController. This can be considered if iOS 8 and above are being targeted.

Add (or replace) the insertNewObject function in the MasterViewController as follows:

func insertNewObject(sender: AnyObject) {
  let alert = UIAlertView(title: "Add user",
   message: "Please select a user to add",
   delegate: AddAlertDelegate(app,tableView),
   cancelButtonTitle: "Cancel",
   otherButtonTitles: "Add")
  alert.alertViewStyle = .PlainTextInput
  alert.textFieldAtIndex(0)?.placeholder = "Username"
  alert.show()
}

The AddAlertDelegate performs two functions; when completed, it calls the addUser on the AppDelegate; and secondly, it invokes the reloadData on the table, which ensures that the data is correctly shown. To do this, the delegate needs to have references to both the app delegate and the tableView, which are passed in the initializer:

class AddAlertDelegate: NSObject, UIAlertViewDelegate {
  var capture:AddAlertDelegate?
  var tableView:UITableView
  var app:AppDelegate
  init(_ app:AppDelegate,_ tableView:UITableView) {
    self.app = app
    self.tableView = tableView
    super.init()
    capture = self // prevent immediate deallocation
  }
  func alertView(alertView: UIAlertView,
   clickedButtonAtIndex buttonIndex: Int) {
    if buttonIndex == 1 {
      if let user = alertView.textFieldAtIndex(0)?.text {
        app.addUser(user)
        Threads.runOnUIThread {
          self.tableView.reloadData()
        }
      }
    }
    capture = nil
  }
}

Now the users can be added in the UI by clicking the add (+) button on the top-right of the application. Each time the application is launched, the users array will be empty, and users can be re-added.

Adding users

Tip

Users could be persisted between launches using NSUserDefaults.standardUserDefaults and the setObject:forKey and stringArrayForKey methods. The implementation of this is left to the reader.

Implementing the detail view

The final step is to implement the detail view, so that when a repository is selected per-repository information is shown. At the time the repository is selected from the master screen, the username and repository name are known. These can be used to pull more information from the repository and add items into the detail view.

Update the view in the storyboard to add four labels and four label titles for username, repository name, number of watchers, and number of open issues. Wire these into outlets in the DetailViewController:

@IBOutlet weak var userLabel: UILabel?
@IBOutlet weak var repoLabel: UILabel?
@IBOutlet weak var issuesLabel: UILabel?
@IBOutlet weak var watchersLabel: UILabel?

To set content on the details view, user and repo will be stored as (optional) strings, and the additional data will be stored in string key/value pairs. When they are changed, the setupUI method should be called to redisplay content:

var user: String? { didSet { setupUI() } }
var repo: String? { didSet { setupUI() } }
var data:[String:String]? { didSet { setupUI() } }

The setupUI call will also need to be called after the viewDidLoad method is called, to ensure that the UI is set up as expected:

override func viewDidLoad() { setupUI() }

In the setupUI method, the labels might not have been set, so they need to be tested with an if let statement before setting the content:

func setupUI() {
  if let label = userLabel { label.text = user }
  if let label = repoLabel { label.text = repo }
  if let label = issuesLabel {
    label.text = self.data?["open_issues_count"]
  }
  if let label = watchersLabel {
    label.text = self.data?["watchers_count"]
  }
}

If using the standard template, the splitViewController of the AppDelegate needs to be changed to use return true after the detail view is amended:

func splitViewController(
 splitViewController: UISplitViewController,
 collapseSecondaryViewController 
  secondaryViewController:UIViewController!,
 ontoPrimaryViewController
  primaryViewController:UIViewController!) -> Bool {
  return true
}

Note

splitViewController:collapseSecondaryViewController determines whether or not the first page shown is the master (true) or detail (false) page.

Transitioning between the master and detail views

The connection between the master view and the detail view is triggered by the showDetail segue. This can be used to extract the selected row from the table, which can then be used to extract the selected row and section:

override func prepareForSegue(segue: UIStoryboardSegue,
 sender: AnyObject?) {
  if segue.identifier == "showDetail" {
    if let indexPath = self.tableView.indexPathForSelectedRow() {
      // get the details controller
      // set the details
    }
  }
}

The details controller can be accessed from the segue's destination controller—except that the destination is the navigation controller, so it needs to be unpacked one step further:

// get the details controller
let controller = (segue.destinationViewController as
 UINavigationController).topViewController
 as DetailViewController
// set the details

Next, the details need to be passed in, which can be extracted from the indexPath as in the prior parts of the application:

let user = app.users[indexPath.section]
let repo = app.repos[user]![indexPath.row]
controller.repo = repo
controller.user = user

The data needs to be acquired asynchronously using the withUserRepos method created previously. As this returns an array of repositories, it is necessary to filter out the one with the desired name:

app.api.withUserRepos(user) {
 repos -> () in
  controller.data = repos.filter({$0["name"] == repo}).first
}

Finally, to ensure that the application works in split mode with a SplitViewController, the back button needs to be displayed if in split mode:

controller.navigationItem.leftBarButtonItem =
 self.splitViewController?.displayModeButtonItem()
controller.navigationItem.leftItemsSupplementBackButton = true

Running the application now will show a set of repositories and when one is selected, the details will be shown:

Transitioning between the master and detail views

Tip

If a crash is seen when displaying the detail view, check in the Main.storyboard that the connector for a non-existent field is not defined. Otherwise, an error similar to this class is not key value coding-compliant for the key detailDescriptionLabel might be seen, which is caused by the Storyboard attempting to assign a missing outlet in the code.

Loading the user's avatar

The user might have an avatar or icon that they have uploaded to GitHub. This information is stored in the user info, which is accessible from a separate lookup in the GitHub API. Each user's avatar will be stored as a reference with avatar_url in the user info document such as https://api.github.com/users/alblue, as follows:

{
  … 
  "avatar_url": "https://avatars.githubusercontent.com/u/76791?v=2",
  … 
}

This URL represents an image that can be used in the header for the user's repository.

To add support for this, the user info needs to be added to the GitHubAPI class:

func getURLForUserInfo(user:String) -> NSURL {
  let key = "ui:(user)"
  if let url = cache.objectForKey(key) as? NSURL {
    return url
  } else {
    let userURL = services["user_url"]!
    let userSpecificURL = URITemplate.replace(userURL,
     values:["user":user])
    let url = NSURL(string:userSpecificURL, relativeToURL:base)!
    cache.setObject(url,forKey:key)
    return url
  }
}

This looks up the user_url service from the GitHub API, which returns the following URI template:

  "user_url": "https://api.github.com/users/{user}",

This can be instantiated with the user, and then the image can be loaded asynchronously:

import UIKit
...
func withUserImage(user:String, fn:(UIImage -> ())) {
  let key = "image:(user)"
  if let image = cache.objectForKey(key) as? UIImage {
    fn(image)
  } else {
    let url = getURLForUserInfo(user)
    url.withJSONDictionary {
      userInfo in
      if let avatar_url = userInfo["avatar_url"] {
        if let avatarURL = NSURL(string:avatar_url,
         relativeToURL:url) {
          if let data = NSData(contentsOfURL:avatarURL) {
            if let image = UIImage(data: data) {
              self.cache.setObject(image,forKey:key)
              fn(image)
} } } } } } }

Once the support to load the user's avatar has been implemented, it can be added to the view's header to display in the user interface.

Displaying the user's avatar

The table view that presents the repository information by user can be amended so that along with the user's name, it also shows their avatar at the same time. Currently this is done in the tableView:titleForHeaderInSection method, but an equivalent tableView:viewForHeaderInSection method is available that provides more customization options.

Although the method signature indicates that the return type is UIView, in fact it must be a subtype of UITableViewHeaderFooterView. Unfortunately there is no support for editing or customizing these in Storyboard, so they must be implemented programmatically.

To implement the viewForHeaderInSection method, obtain the username as before, and set it to the textLabel of a newly created UITableViewHeaderFooterView. Then, in the asynchronous image loader, create a frame that has the same origin but a square size for the image, and then create and add the image as a subview of the header view. The method will look like:

override func tableView(tableView: UITableView,
 viewForHeaderInSection section: Int) -> UIView? {
  let cell = UITableViewHeaderFooterView()
  let user = app.users[section]
  cell.textLabel.text = user
  app.api.withUserImage(user) {
    image in
    let minSize = min(cell.frame.height, cell.frame.width)
    let squareSize = CGSize(width:minSize, height:minSize)
    let imageFrame = CGRect(origin:cell.frame.origin,
     size:squareSize)
    Threads.runOnUIThread {
      let imageView = UIImageView(image:image)
      imageView.frame = imageFrame
      cell.addSubview(imageView)
      cell.setNeedsLayout()
      cell.setNeedsDisplay()
    }
  }
  return cell
}

Now when the application is run, the avatar will be shown overlaying the user's repositories.

Displaying the user's avatar
..................Content has been hidden....................

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