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.
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 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.
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:
#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.
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.
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.
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:
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.
Hopefully your console print matches the JSON shown in the image above. If not, you've done something wrong.
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.
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.
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
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.
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.
- (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.
ViewController has an array of Posts, but Images needs one as well. This next step will create this array.
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)
if ([segue.identifier isEqualToString:@"images"]) { ImagesViewController *controller = (ImagesViewController *)segue.destinationViewController; //set posts here }
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):
Run the app. If everything goes well, you should see a list of varied posts in the ImagesViewController with unique comments.
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.
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.
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.
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];
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.
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]];
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:
if(![textfieldNumberOfImages.text isEqualToString:@""]) { ViewController *controller = [self.navigationController.viewControllers objectAtIndex:0]; //set the string property on controller here }
—–
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