User Tools

Site Tools


ios-labs-s14:class-03

Intro

Please use the Git instructions from Lecture 2 to download the most recent project. I have made changes to the project that may not be reflected in the previous tutorial, most important of which is the removal of the segue from the table cell to the PostViewController. The repository is located at https://github.com/damouse/cs407_UWMadisonInstagram_final.git

This lecture is going to be harder than the previous. The only way to learn the code and get used to the syntax is the struggle with it a little bit. There are two things you should keep doing to make progress.

  1. use the internet, specifically StackOverflow. If you don't know how to do something, or cant get the syntax right (more likely) Google the method name, what you're trying to do, whatever.
  2. look back at previous code. Most of the code I'll have you write in this lecture has already been provided for you somewhere else in the tutorial or the project. Look back it at for hints as far as structure, method names, and syntax! You only have to figure out something once, after that you just abuse copy/paste!

In this lecture we will be interfacing with the Instagram server, loading the content into custom model objects, and displaying their data within the table. Almost all of the instructions will deal with code!


NSURLConnection

NSURLConnection is a long and painful name for the object that handles iOS's internet connectivity. This class contains everything you need to access any remote API, and is commonly used to back model objects. Although not isolated because of MVC, it still relies on delegation to function. Remember that delegation is the act of nominating another object to react to certain changes in state through the implementation of delegate methods. The object that needs functionality calls the delegate methods on the assigned delegate object.

There are two parts to using NSURLConnection.

  1. Create API call and receiving variable, fire API call
  2. Implement delegate methods

The delegate methods are called as NSURLConnection moves about its business. They are extremely boilerplate, and generally you'll find yourself copying and pasting them constantly instead of writing them from scratch.

There are 4 delegate methods:

  1. didReceiveResponse- called when the first bits are received from a request
  2. didReceiveData- called as data flows in
  3. connectionDidFinishLoading- called when the data has finished arriving
  4. connection:didFailWithError- called if something goes back

Implement Delegate Methods

  1. Copy in the NSURLConnection Delegate methods:
#pragma mark NSURLConnection Methods

- (void) connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {

}

- (void) connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {

}

- (void) connectionDidFinishLoading:(NSURLConnection *)connection {
    NSError *error = nil;
    NSDictionary *data = [NSJSONSerialization JSONObjectWithData: options:NSJSONReadingAllowFragments error:&error];

    [connection cancel];
}

- (void) connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    NSLog(@"Error During Connection: %@", [error description]);
}

Note that the receiver object has been omitted in the code above (it should be right after the colon). This is the data object that the Connection class loads the data into and you have yet to make it. The circle in the screenshot above shows where it should be as a method parameter for the JSONDeserializer class.

A “receiver” object is simply an object that gets filled by another. In the case of NSURLConnection, we must use an NSMutableData object, which stores changeable binary data.

You are asked to initialize this variable here. Initialization looks the same for every class, all of the time for the stock constructor, see previous lecture for hints! Remember, the (strong, nonatomic) chunk comes after each @property and is a way of giving you utility later on. Don't worry about what it does now, just include it every time before your propertyType *propertyName.

Making Receiver Object

  1. Prepare to declare a new instance variable by first adding curly braces after the @interface line in ViewController.m
  2. Declare the variable between the braces. The declaration should look like this: type *name;
  3. The variable should be of type NSMutableData. You may name it whatever you'd like
  4. Initialize the variable in viewDidLoad

Ok. We've implemented the methods and created the receiving variable. Two steps lie between us and talking to the internet: manipulating the data object previously created, and firing the API call. Again, the steps detailed below are almost always like this; I'm having you work through them manually not simply to get practice calling methods. The formation of the NSURL and NSURLRequest objects commonly look like this, treat them as one step and don't get boggled down trying to find meaning in the code.

Initiating API Call

  1. In didReceiveResponse, call the method setLength on your data instance variable, passing 0 as an argument. This clears the data when a response comes through.
  2. In didReceiveData, call appendData on your data instance variable, passing data to it (data is the argument for didReceiveData)
  3. Finally, put your data instance variable in the missing parameter slot in the connectionDidFinishLoading method, as pointed out in the screenshot above
  4. Copy the following code into viewDidLoad
NSURL *url = [[NSURL alloc] initWithString:@"http://pages.cs.wisc.edu/~mihnea/instagram.json"];
NSURLRequest *req = [[NSURLRequest alloc] initWithURL:url];
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:req delegate:self];
[connection start];

We should be hooked up to the internet. As you can see from the post above, the URL we're headed to is http://pages.cs.wisc.edu/~mihnea/instagram.json . In order to check the results of your code, go there now and inspect the JSON that is returned.

Parsing JSON

If you are doing anything on the internet, JSONs are life. JSON stands for JavaScript Object Notation, and is the more-or-less standard language of moving raw data over the open internet.

Two things make up a JSON:

  1. Arrays- a set of objects, denoted by square brackets []
  2. Dictionaries- keys that pair with objects, denoted by curly braces. Keys are text followed by a colon, the object the key represents follows the colon
  3. Primitives- strings, integers, booleans, etc

Sounds simple, except that the two data structures can be infinitely nested within each other, as you can see from the sample JSON linked above.

When NSURLConnection returns the data, we use NSJSONDeserializer to convert the binary data into a dictionary. To navigate the JSON and extract useful information, you move through constituent dictionaries and arrays by using objectForKey and objectAtIndex, respectively.

For example, to retrieve the dictionary under the key “pagination,” you would call

[data objectForKey:@"pagination"]

which returns a dictionary that looks like:

{
next_url: "redacted",
next_max_id: "redacted"
}

This dictionary has two keys that match two strings.

To fetch the first type in the first post

[[[data objectForKey:@"data"] objectAtIndex:0] objectForKey:@"type"]

which returns the string “image”.

JSON may look awful to you at first, but understanding them is absolutely essential to interacting with any server, ever. The rest of this tutorial is written to be annoying, difficult, and painful; JSONS however are naturally like that if you haven't seen them before.

To verify the contents of the resulting dictionary, you're going to set a breakpoint and use the live console to inspect the variables in memory. Typing “po” (for print object) and then the name of an object will dump it to the console. You may ask the console for any properties or even methods of objects in memory, and it will execute the code and show you the result. Alternatively, the pane on the left of the console shows a graphical hierarchy of objects.

Debugging

  1. Set a breakpoint by clicking on the border to the left of the code pane (between the code and the project navigator). Set the breakpoint on the line [connection cancel]; in connectionDidFinishLoading.
  2. Run the app
  3. Type po data into the console when your app pauses. Inspect the data, confirm it matches the JSON in the link

Hopefully your console print matches the JSON shown in the image above. If not, you've done something wrong.


Model

Models represent the data and state of your application, and are important for clarity and structure. You may scoff at creating a custom class here to deal with three bits of data, and you'd be right: there's really no reason to introduce a subclass instead of just moving the array of dictionaries around manually, it introduces complexity and doesn't make it vastly easier to read.

When you write real apps, however, you'll sorely regret skipping out on custom model hierarchies. Nothing I say here will convince you of the benefit of maintaining clearly defined MVC and OOP principles, however; not until you write truly awful code will you realize the benefit. For now, were going to make a Post class to represent each Instagram post as an excercise in good coding practices.

Create Post Object

  1. Got to File»New
  2. Name your object Post, and ensure it subclasses NSObject
  3. Click on Post.h and add property called imageURL of type NSString

Adding a property to your custom class.

Repeat the last two steps above to add 2 more properties, both of type NSString, named “userName” and “caption”.

Now that the class for holding the post data is finished, we have to make an array property on the class that fires the API call to store the resulting objects.

Make Array Property

  1. Add a property to ViewController of type NSMutableArray called posts
  2. Instantiate the array before [connection cancel]; in connectionDidFinishLoading. You can access the array using the dot operator on self: self.posts = …

The array property. The explicit property examples are going to dry up soon, look back at these too get hints on how to add properties later!

Ok, you have the array, you have the class, and you have the JSON. The last step is bringing them together, getting the data from each JSON element into a new Post and adding it to the array. You'll be using a simple for loop that looks like it does in every C derived language:

for(int i = 0; i < loopCondition; i++) { }

Additionally, I'll have you pull the data array out of the JSON for clarity, and pull the dictionary at each index out of that array for each iteration of the loop. After you have that dictionary, you'll have to declare and instantiate a new Post object and fill the properties you gave it earlier with the correct JSON fields and add the Post to the local array. Remember: to access the property of an object, use the dot operator! To access a property that belongs to the class you're coding in, use self as the object.

Remember: there are two methods you need to use to navigate the JSON, one for dictionaries and one for arrays

  1. objectForKey: returns object that matches the passed-in key for a dictionary
  2. objectAtIndex: returns the object at the given index of an NSArray. Do not use [], they don't do anything to Objective-C arrays but will compile!

Instantiate Model Objects

  1. Import Post.h into ViewController.m
  2. Declare and instantiate an NSArray. Assign it the array of posts in the JSON dictionary (stored under the key data)
  3. Create a for loop. Set the loop condition to be the number of items in the array you just made by calling count on the array.
  4. The next instructions are all inside the loop: declare a new Dictionary object, instantiate it with the contents of the current post by accessing the array object at the current index.
  5. Declare and instantiate a new Post object
  6. For each property in the Post object, dig through the dictionary object to find and assign the relevant key. You will have to nest objectForKey calls. The relevant keys are “full_name,” (under the user object) “url,” (under images, standard_resolution) and “text” (under caption)
  7. The first is shown as an example below. This is assuming the post object you created is called post, the dictionary is called entry.
post.userName = [[entry objectForKey:@"user"] objectForKey:@"full_name"];

Run your program, again setting a debug point as mentioned above. Print your array property, and inspect the Post objects on the left side, ensuring everything is correct.


Passing and Loading Posts

If you're here, then the hard part is over. You've successfully integrated your app with a remote server, and using a dumbed-down API call you now have local access to UWMadison's Instagram data.

The next step is passing this data out of ViewController to the other controllers that need it: Images and Posts. Images needs the entire array of Post objects to display its table, Posts needs just one Post object to show the image and comment. To make this handoff between controllers happen, we'll use a method called prepareForSegue, which is a view controller delegate method that gets called when a segue has just been triggered. The next view controller is passed along as a parameter when the method is called, so we can use it to transfer relevant data right before a transition.

Prepare For Segue Method

  1. Add the following method to ViewController.m
- (void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    if ([segue.identifier isEqualToString:@"images"]) {
    
    }
}

Unfortunately, ViewController has two segues, one to Images and one to Settings. To differentiate the two, we must give the relevant segue a unique identifier and check that identifier in the delegate method. If we don't do this, then the method might cast SettingsViewController as an ImagesViewController, and your app will crash when the nonexistant property is called.

Changing Segue Identifier

  1. Open the storyboard
  2. Select the segue between ViewController and ImagesViewController by clicking the arrow connecting the controllers
  3. Open the Attributes Inspector
  4. Set the Identifer field to “images”

Setting the segue identifier.

ViewController has an array of Posts, but Images needs one as well. This next step will create this array.

Creating Receiver Property

  1. Add an NSArray property to ImagesViewController
  2. Import ImagesViewController.h in ViewController.h

What does the import statement do? It lets ViewController know about ImagesViewController: alerts it that Images is a class, and more importantly tells it about Images' public methods and properties. The IDE will normally not allow you to interact with objects you don't have imported. Remember: always import header files, not source code files (.h, not .m)

Setting Receiver Property

  1. Copy the following code inside the if statement of the prepareForSegue method to get a reference to the destination controller
if ([segue.identifier isEqualToString:@"images"]) {
  ImagesViewController *controller = (ImagesViewController *)segue.destinationViewController;
  //set posts here
}
  1. Set the array of the destination controller to be the array of the current view controller (use an equals sign!)

This is what ImagesViewController looks like now, before any changes. You'll be altering the behavior of the table, and therefor altering the delegate methods.

The four required delegate methods of a UITableView (note: there exist more, optional delegate methods):

  1. numberOfSectionsInTableView: how many sections does this table have? See Contacts app for an example of sections: each letter is a section
  2. tableView:numberOfRowsInSection: how many rows are in the given section? (quick hint: the size of an array is given by count method)
  3. tableView:cellForRowAtIndexPath: what should the cell at this index contain?
  4. tableView:didSelectRowAtIndexPath: what should happen when a cell at this index is touched?

Changing Table Behavior

  1. Import Post.h into ImagesViewController.h
  2. Change numberOfRowsInSection method to return the size of the posts array. numberOfSectionsInTableView should still return 1
  3. Declare a Post object inside of cellForRowAtIndexPath after the if statement. Instantiate it with the correct Post entry at the given index of the posts array (remember: indexPath.row is the current index)
  4. Replace the dynamic placeholders of the cell with the Post's data. textLabel should receive the “name” property, detailTextLabel should be assigned the “comment” property.

Run the app. If everything goes well, you should see a list of varied posts in the ImagesViewController with unique comments.

Loading Data into ImageViewController

The ImagesViewController got its data, now its time to pass the data off once more to the PostsViewController and finally see some images!

The steps to make this handoff happen are the same as before: import relevant header files, create properties to receive incoming data, pass the data off to the new controller being presented, and finally have the new controller present the data. There is one notable difference: you will be instantiating and pushing the PostsViewController manually from code instead of a segue from Storyboard. This is just for informative purposes.

Prepare Controller For Data

  1. Import the Post header in PostsViewController
  2. Create a Post property

Its not enough to simple instantiate the view controller like we've done for all the other objects. Remember that InterfaceBuilder contains all of the information about the views belonging to each controller. If you create a view controller with the standard [ [PostsViewController alloc] init], it won't have any views! Instead you must load the storyboard from memory first, then ask for a specific controller by its Storyboard ID.

Changing Storyboard ID

  1. Select the PostsViewController in the storyboard
  2. Click the Identity Inspector
  3. Set “Storyboard ID” to “posts”

Every controller in a navigation stack has access to the navigation stack object itself (that is: the UINavigationController that is maintaining the stack.) Because of this, controllers can use this reference to see what the rest of the stack looks like (what controllers are under me, which controller is root) and to push and pop controllers from the stack. The manual segue will use the pushViewController:animated method of UINavigationController to present Posts.

Manual Controller Segue

  1. Import the PostsViewController
  2. Copy the following code into didSelectRowAtIndexPath at the end of the method
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
PostsViewController *controller = [storyboard instantiateViewControllerWithIdentifier:@"posts"];
    
//remove the correct post from the array here, set controller's post property
    
[self.navigationController pushViewController:controller animated:YES];
  1. Set the Post property of the receiving controller to the correct post from the local array (beneath the comment, before the view controller is pushed onto the nav stack)

Making the PostsViewController present its data is fairly straightforward: load the image provided in the URL and set the textview to the comment string. Both of these are properties on the Post objects.

We will be using another controller lifecycle delegate method here, called viewWillAppear. There are many of these, and they are all optional. Your choice of method depends on when you want code to run.

Loading Image and Comment

  1. Create a new method in PostsViewController that returns void called viewWillAppear. Type the dash, the return type in parenthesis, and then use autocomplete to finish it
  2. Form a new NSURL object (see sample code in ViewController) using the URLWithString class method, passing the url property (which is a string!) from Post. Remember that post is an instance variable on this view controller.
  3. Copy the following two lines of code (assuming your NSURL object is called “url”)

NOTE: you must connect imagePhoto and textviewComment as IBOutlets again on storyboard into PostsViewController! Bug. Check back to the second lecture for example: open storyboard, open assistant editor, select controller, make curly braces, right click and drag from the view into the braces.

NSData *data = [NSData dataWithContentsOfURL:url];
[imagePhoto setImage:[UIImage imageWithData:data]];
  1. Set the textView's content to post's comment string. The method to change the text of a textView is called setText: and it takes an NSString as an argument.


Setting and Passing Settings Data

Consider this the extra credit of this tutorial This section is sparse on instructions and is meant to challenge you if you've made it this far.

The steps here are, briefly:

  1. Create a string property on ViewController (call it whatever you like, you'll see it referred to as maxPosts)
  2. Create an integer property on ImagesViewController
  3. Update ViewController's property from SettingsViewController with the contents of the textField when the controller is about to leave the screen
  4. Update ImagesViewController's property from ViewController
  5. Restrict the table from showing, at maximum, the restricted value passed in.

Create Max Properties

  1. Create a string property for ViewController
  2. Initialize it in viewDidLoad. Using an if statement, check if the property equals nil. If it does, instantiate it with the string literal @“-1”
  3. Create an int property for ImagesViewController (note: as a primitive, int cannot have the typical (strong, nonatomic) entry in its property declaration. Omit it!)

Setting up for "Reverse Segue"

  1. Import ViewController into SettingsViewController
  2. Add method viewWillDisappear to SettingsViewController. Its return type is void
  3. Copy the following code into this method
if(![textfieldNumberOfImages.text isEqualToString:@""]) {
        ViewController *controller = [self.navigationController.viewControllers objectAtIndex:0];
        
        //set the string property on controller here
}
  1. Set the max property of ViewController to the “text” property of the textfield.

Pass Data to Posts

  1. In prepareForSegue, set the int property of the receiving ImagesViewController to the local maximum images variable
  2. Convert from NSString by calling intValue on the local instance variable.

Constrain Table

  1. Change numberOfRowsInSection to return the size of the posts array IFF the maxPosts integer property is -1 OR the size of the posts array is less than the max integer, ELSE return maxPosts

—–

Conclusion

This is the end of the iOS crash course! Thanks for sticking through, I hope you learned something about iOS development, even if that something was only “Its not for me.”

If you elect to study iOS after this week, we'll dive deeper into the theory for a day or two, then start tearing through features. You'll learn how to play with maps, interface with social platforms, use graphics, and more.

If you have any questions about your class projects, please contact me, I have a good bit of experience in Android and iOS development. If you're doing an HTML5 app, you can contact me but bewarned: I might try to talk you out of it.

Thanks! - Mickey Barboi

ios-labs-s14/class-03.txt · Last modified: 2014/02/07 12:55 by mbarboi