User Tools

Site Tools


ios-labs-s14:class-04

Graphic Grant Tracker

This is an iOS tutorial on creating the Graphic Grant Tracker, an iOS app made to track grants in the CS department.

Steps described in this tutorial will not necessarily hold your hand. To review introductory iOS material, please read the iOS Crash Course or check the Building Blocks pages. This tutorial has had instructions and screenshots trimmed from it to make it more difficult; copying and pasting code to get through it doesn't help you at all.

App Motivation

Any professor working in the CS department usually has to juggle dozens of spreadsheets, each which individually track a grant, when performing research. These spreadsheets consist of a series of accounting entries into and out of the account. GGT aims to provide a simple, remote interface for viewing all of this data cleanly and quickly.

Users of the app place all of their spreadsheets in a private directory on the CS network and then run a Ruby script which encrypts them, places them in the user's public directory, and builds the PHP serving script/API. This script is visible at an external URL– the iOS app goes to this URL and transacts with the “server,” receiving a CSV (comma seperated value) of each spreadsheet in JSON (java-script object notation) format. Although there are many features built into the API, for the purposes of this tutorial we will approach its functionality incrementally.

The app parses each spreadsheet, graphs the aggregate totals of interesting accounts, and provides a simple interface for analyzing the data.

Debugging Crashes

All crashes will print you an exception and stacktrace to the console, but these are rarely enlightening. Thankfully, Xcode allows you to set a breakpoint at the point of an exception. This jumps to the offending line 90% of the time, showing you exactly what went wrong.

Setting and Exception Breakpoint

  1. Click the exception panel on the left side (see screenshot)
  2. Click the plus sign in the absolute bottom left of the corner
  3. Select “Add Exception Breakpoint”

Introduction and Setup

Instead of creating the whole interface in one go like the Crash Course, we will incrementally build parts of it each lecture in order to introduce new elements as well as pounding the information through with repetition. Three new concepts will accompany this repetition: custom table cells, persistent device storage, and intermediate API calls (you'll also see your first blocks).

Understand that the finished product represents minimum 60 hours of work. Having you write the whole thing in three lectures would be sadistic, so you will be importing utility classes, models, and the graphing library in order to save time. You will still have to implement almost all of the controller logic.

Any instructions that you've covered twice will be sparse. Look back at your old code, at the old tutorials, or on google!

Create a New Project

  1. Create a new project with a single view. Give it a name.

Setup a Nav Stack with a Root ViewController

  1. Delete existing controller on Storyboard
  2. Add a Navigation Controller, delete its root view controller
  3. Add a new, plain UIViewController and set it as the Nav's root

Create and Set Subclass

  1. Create a new ViewController subclass called RootViewController
  2. Subclass the controller in InterfaceBuilder
  3. Set the title of the new controller to “Grants”

See? Sparse.


Table

Back to our best friend, the Table. For a review on the purpose, structure, and manipulation of views please check the Crash Course.

In the spirit of UX-centric design, we want the user to immediately be presented with his or her grants upon opening the app. RootViewController will display settings and transition buttons on a bar on top of the screen and all grants are listed in the table.

Everything in this section will be similar to the content covered in the Crash Course with one exceptions. Remember you can subclass any view or controller to realize custom behavior. Just like you can subclass UIViewControllers and customize their views to be unique from other controllers, in order to get custom table cells we'll have to roll our own UITableViewCell subclass. This is an important part of using tables, since you will generally want to customize the appearance of your cells.

Add UITableView

  1. Add a table to the RootViewController, resize to fill controller
  2. Connect delegate and data source properties using the controller shortcut as shown below
  3. Create an IBOutlet to RootViewController named “tableGrants”

The table with the new cell added (next step)

The process for adding a UITableViewCell is the same as adding any other view: simply drag, drop, and customize. To “Layout” an object in iOS development means to add and edit views within the object to set up its overall interface.

Layout Custom UITableViewCell

  1. Drag a Table View Cell from the object list onto the table
  2. Layout the cells contents as shown below
  3. Select the “Grant” and “End Date” labels and change their fonts to bold from the pane on the right side of the screen, using the attributes inspector
  4. Downsize the font size of the [totals] label

Just like sub-classing a controller, you must create a new class, with .h and .m files, that will be instantiated to handle the functionality of your new cell by connecting outlets.

Subclass UITableViewCell

  1. Create a new object named “GrantCell,” ensure it subclasses UITableViewCell
  2. Select the cell in storyboard and set the subclass
  3. Return to the Identity Inspector and set the Identifier field to “cell”
  4. Connect “Name of Grant,” “End Date,” and “… remaining” to the header file as public outlets, named labelName, labelEndDate, and labelRemaining respectively
  5. Set the cell's reuse identifier to “cell”

Note that selecting the cell and opening the assistant editor will not pull up the correct source code file. Assistant editor tries to open whichever file complements the current selection on the main pane, but since the GrantCell is contained within the RootViewController, the Assistant picks the controller. You'll have to manually select the appropriate header file by clicking Automatic on top of the Assistant Editor, changing it to Manual, and drilling through your project files for the right one.

When establishing the IBOutlets, do not create braces in the header file! Place them between @interface and @end without braces.

If you're here in this class now, you've elected to work on an iOS project this semester and may have some interest working on future development. Its then safe to assume you'll have to use a table at some point again, ergo you'll have to complete this next step without hand-holding. Remember: Don't Repeat Yourself. These methods should be copy-pasted from old projects or the internet every time, not written from scratch.

Implement Delegate Methods

  1. Find the four UITableView delegate methods. Copy them into RootViewController

You'll undoubtedly have to edit the delegate methods to get the project to compile. After you've figured it out, run your app and ensure you see the dummy values in the cell


API

Did you miss JSON's? Take heart in the fact that XML (the other commonly used HTTP object notation language) is even more fun.

As stated in the intro, we'll be diving into another level of complexity for APIs. Here, instead of connecting to a static URL, you will have to pass arguments to the serving script which authenticate your user, identify the right method, and changing functionality.

The most important service the server provides is converting XLS to CSV and sending the app that data. It goes from this: to this: (note: screenshot taken from finished product; CSV dump to file. Yours will be a JSON)

Here are the functions the server knows to handle:

  1. login- used only for debugging purposes, passwords are set individually at each end
  2. mod- accepts the encryption key, returns all of the .xls files in the current directory (although they have .enc on the end) as well as the system time they were last modified
  3. download- accepts a file name and an encryption key, decrypts the file passed from .enc to .xls, runs the .xls file through PHPExcel framework to convert to .csv (comma separated value), returns the CSV as a heirarchy of nested arrays
  4. ping- returns success to indicate connectivity to the server

The most important bit here is understanding how to pass arguments inline with the URL. A '?' after a URL signifies the start of the list of arguments as key/value pairs. Open your browser and google “hello.” Examine the url that is formed in the top bar.

...?q=hello&rlz=1C1CHMO_enUS567US567&oq=hello&aqs=chrome..69i57j69i60l2j69i59l2j69i60.554j0j8...

A key (or variable) is denoted by the text to the left of an equals sign, while its value sits on the right. The same URL chunk with whitespace:

?q=hello&
rlz=1C1CHMO_enUS567US567&
oq=hello&
aqs=chrome..69i57j69i60l2j69i59l2j69i60.554j0j8

This should be a little clearer. It looks like the variable “q” stands for query and represents my search string. “oq” looks the same, but it may have some different fucntionality. “rlz” seems to represent some sort or geocoding, if I had to guess I'd say it stands for “regional localization.” “aqs” identifies the browser type, followed by some cryptographic key which I'd guess identifies either my Chrome version or my Google account.

Note that ampersands allow you to add multiple pairings onto the URL; ampersands must separate each pairing. This is the easiest way of passing information along to a server, although other and more secure methods abound.

Want to test the script and arguments for yourself? Here is the URL for the ping call. Try and play with changing the “type” variable at least once.

http://pages.cs.wisc.edu/~mihnea/ggt/sheets/ggt_handler.php?type=ping

Before you dive into the API fun, you'll have to prepare the controller for connectivity.

Preparing RootViewController for API Connectivity

  1. Add an NSMutableArray as a property, name it “grants”
  2. Initialize it

Where do you initialize property arrays? At the latest: before you have to use them.

Create Download Method

  1. Create a new method called “download” that takes no parameters and returns nothing.
  2. Copy the following code into it
    NSString *urlString = [NSString stringWithFormat:@"http://pages.cs.wisc.edu/~mihnea/ggt/sheets/ggt_handler.php?type=ping"];
    
    NSURL *url = [NSURL URLWithString:[urlString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
    
    NSURLSession *session = [NSURLSession sharedSession];
    [[session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
                // handle response
                NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
                NSLog(@"%@", json);
    }] resume];

Oh no. What is that notation?!

Its a block. Code in the block executes when the API call finishes. Don't think about it too much.

Test your app and check the data coming back. Ensure the returned JSON matches the same one presented when you manually navigate to the URL.

Replace all the code before NSURLSession with the following and test again:

    NSString *key = @"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08";
    NSString *urlString = [NSString stringWithFormat:@"http://pages.cs.wisc.edu/~mihnea/ggt/sheets/ggt_handler.php?type=download&fname=%@&key=%@", @"samplefile.enc", key];

Model

Things begin to get a little weird here.

At this point we begin to dive into the meat of the app. Remember that this tutorial roughly follows the development of a 6-8 thousand line app ready for distribution. In order to continue to make progress on this tutorial, but still see some interesting things happen, you're going to have to import code from the functional version.

If you take a look at the code and are overwhelmed feel free to ignore it. If, on the other hand, you want to dive deeper into iOS development, you're encouraged to jump in head-first.

Importing Model Objects

  1. Download and unzip the files here
  2. With your project navigator open drag the four files in from the finder window. Check “Copy files to workspace” when prompted
  3. Import “GrantObject.”h into RootViewController source file
  4. Inspect the initWithCSVArray method of GrantObject

GrantObject is the model object representing a grant. Each grant received from the API is parsed and turned into a GrantObject using initWithCSVArray method to do the parsing. The last debugging push should have exposed the CSV array within the JSON under the key data.

Creating Grant Objects

  1. Declare and instantiate a new GrantObject below the JSON logging in the NSURLSession completion block. Instead of calling init after alloc, call the custom constructor initWithCSVArray: and pass it the array represented by the “data” key of the returned JSON.
  2. Add the GrantObject to the grants array.

GGT loads one grant successfully! Unfortunatly the app doesn't do anything terribly interesting with the data just yet. The table should detect changes in the array and reload itself, cells should load and display information for each grant, and we want to cache grants when we load them from the API so the app doesn't have to make an API call every time.

Three convenience methods have been included to simplify the process of formatting GrantObject data. They deal with dates, numbers, and string formatting. The Good Stuff, in other words.

Load Cell Content

  1. Copy the following three convenience methods into RootViewController.
//given a grant, return the end date properly formatted
- (NSString *) formatEndDate:(GrantObject *)grant
{
    
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    [formatter setDateFormat:@"mm/dd/YYYY"];
    NSDate *endDate = [formatter dateFromString:[[grant getMetadata] objectForKey:@"endDate"]];
    [formatter setDateFormat:@"MMMM dd, yyyy"];
    
    return [formatter stringFromDate:endDate];
}

//given a string of currency, format it correctly and return it as an int
- (NSDecimalNumber *) formatCurrency:(NSString *)amount
{
    NSString *ret = [[amount stringByReplacingOccurrencesOfString:@"\"" withString:@""] stringByReplacingOccurrencesOfString:@"," withString:@""];
    ret = [[ret componentsSeparatedByString:@"."] objectAtIndex:0];
    
    return [NSDecimalNumber decimalNumberWithString:ret];
}

//given a grant, format balance and budget so it reads: "balance$ out of budget$ remaining"
- (NSString *) formatBalance:(GrantObject *)grant
{
    NSDecimalNumber *budget = [self formatCurrency:[[grant getBudgetRow] objectForKey:@"Amount"]];
    NSDecimalNumber *balance = [self formatCurrency:[[grant getBalanceRow] objectForKey:@"Amount"]];
    
    NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
    [numberFormatter setNumberStyle: NSNumberFormatterCurrencyStyle];
    NSString *balanceString = [numberFormatter stringFromNumber:balance];
    NSString *budgetString = [numberFormatter stringFromNumber:budget];
    
    balanceString = [balanceString stringByReplacingOccurrencesOfString:@".00" withString:@""];
    budgetString = [budgetString stringByReplacingOccurrencesOfString:@".00" withString:@""];
    
    return [NSString stringWithFormat:@"%@ of %@ remainng", balanceString, budgetString];
}

Changing CellForRow

  1. Import “GrantCell.h”
  2. Change all 2 instances of “UITableViewCell” in the cellForRowAtIndexPath method to GrantCell to signify you are working with the new class
  3. Retrieve the grant at the appropriate index matching that of the current cell
  4. Set the text of the three labels of the cell with the appropriate data. The name of the grant is returned by
[[grant getMetadata] objectForKey:@"title"]

while the endDate and remaining fields can be filled by passing the two utility methods //formatEndDate// and ///formatBalance// the grant object.
-Change //numberOfRowsInSection// to return the number of grants stored in the array

Reloading Table on Model Changes

  1. Call reloadData on tableGrants at the end of the completion block in the download method. This will force the table to call its delegate methods again and reconstruct the table.
  2. Change numberOfRowsInSection to return the number of grants stored in the array

Test your app, ensure the grant's information correctly fills the first and only cell in the table.

The next step is persisting the data over multiple app launches, or “saving” it to the device. There are three ways of persisting data in iOS:

  • CoreData- local SQLite store. Complex and heavy on code.
  • Plist- tree of key-object pairs written to file. Simple, but opaque.
  • NSUserDefaults- an NSDictionary store for “defaults.” Simple, lightweight, and common.

Performance is not generally an issue until N > ~100,000, after which CoreData becomes the most efficient of the three to use. You will use NSUserDefaults to save the grants.

NSUserDefaults can only save dictionaries to memory. Thankfully, there's a protocol for transforming any object to an NSDictionary caled NSCoding. When a model object implements the protocol it provides two methods, initWithCoder and encodeWithCoder which implement instructions for distilling and rebuilding objects to and from dictionaries. NSKeyedArchiver is used to serialize objects through the functions. These methods have already been implemented in GrantObject– check GrantObject.m to examine them.

Caching Grant Objects

  1. Add the following code before the table reload
NSData* save = [NSKeyedArchiver archivedDataWithRootObject:self.grants];
[[NSUserDefaults standardUserDefaults] setObject:save forKey:@"directories"];
[[NSUserDefaults standardUserDefaults] synchronize];
  1. Add the following method to RootViewController
- (void) viewDidAppear:(BOOL)animated {
NSData *save = [[NSUserDefaults standardUserDefaults] objectForKey:@"grants"];

if(save != nil)
    self.grants = [NSKeyedUnarchiver unarchiveObjectWithData:save];
}

Run your app, place debug points to ensure the data is being saved.


Conclusion

This is the end of part 1. In this section of the tutorial we made the first steps in creating the GGT app. Obviously only one grant is downloaded and parsed, but the groundwork has been set for handling any number of grants.

Next time we'll dive deeper into the GrantObject implementation to expose the data to the user. By using Google's CorePlot the app can render graphs of the content onto the screen.

ios-labs-s14/class-04.txt · Last modified: 2014/02/17 13:35 by mbarboi