iOS Programming Recipe 16.2: Populating A UITableView With Data From The Web – iOS 7 and AFNetworking 2.0

This article will be nearly the same as Recipe 16, except that it will cover AFNetworking 2.0 and iOS 7 instead.

Assumptions

  • You have a grasp on Xcode basics. If not go get some education here.
  • I also recommend reading the other two tutorials on creating table view as I’ll be building on some of the concepts in those tutorials: Recipe 11 Part 1 & Recipe 11 Part 2

Designing The Interface

To start off, we’re going to create a project using the Single View Application Template. Title the project “WebTableView”. On the storyboard, drag a new navigation controller into the scene. Control click from the prototype cell of the table view controller to the view controller and choose push from the segue type. Now your storyboard should resemble Figure 1 . You may also have to click and drag the root view arrow to the left side of the navigation controller.

Figure 1: Creating the Navigation Controller

Creating The Navigation Controller

Since we just added a navigation controller which includes a table view controller, we’ll want to add a class for the table view controller.

Create a new class in Xcode by pressing the “+” in the bottom left hand corner of the screen and choose “New File” from the popup. Create a new Objective-C class and give it a name of “TableViewController” with a subclass of UITableViewController. From the storyboard, select the table view controller and change the class to “TableViewController” from the Identity inspector on the right. You should also select the view controller and verify that its class is set to “ViewController”.

Since we actually used the UITableViewController as a subclass to your TableViewController class, the delegate and data sources are already set up for us, but if you simply added a table view to a view you would also need to add the protocol to the view controller which contains the table view.

As we did in past UITableView Tutorials we’ll be using the following data source methods in the TableViewController.m file, since we’ll be using the prepareForSegue method we won’t be using any delegate methods. Code Chunk 1 in your TableViewController.m file shows these methods.

Code Chunk 1: The data source methods to be used


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    // Return the number of sections.
    return 0;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    // Return the number of rows in the section.
    return 0;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:  (NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];

    // Configure the cell...

    return cell;
}

You may notice that since we created the new class that is is subclassed by the UITableViewController, these methods were already set up for us. You’ll also notice a bunch of commented common methods and hints. You can go ahead and remove the commented out methods as well as the delegate method.

Make sure you select the prototype cell from the storyboard and set the reuse identifier to “Cell” from the Attributes Inspector on the right.

The next step is setting up AFNetworking for handling and parsing out JSON (JavaScript Object Notation), which is a web technology for presenting formatted data. Alternatively, we could have used NSURLSession directly, but the industry has pretty much standardized on AFNetworking and now AFNetworking 2.0. AFNetworking 2.0 actually uses NSURLSession under the hood anyway.

Setting Up The Requests

First, you’ll need to add AFNetworking to your project. You can do this by downloading the AFNetworking package from GitHub and adding the files manually, or by installing cocoapods and doing it that way. Check out this guide to get you up and running with AFNetworking: Getting Started With AFNetworking. If you do decide to add the file manually make sure you don’t forget to add the “UIKit+AFNetworking” folder. This will be required for easily downloading images and displaying them asynchronously.

Once you have downloaded AFNetworking and added it to your project, import it at the top of the TableViewController.m implementation file.


1
2
#import "TableViewController.h"
#import "AFNetworking.h"

We’ll go ahead and create a method for making the HTTP request titled “makeRestaurantsRequests” after the didRecieveMemeoryWarning method. We’ll also go ahead and fill it in with the request code for AFNetworking. Code Chunk 2 shows this code.

Code Chunk 2: Setting up the operation


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
-(void)makeRestaurantRequests
{
    NSURL *url = [NSURL URLWithString:@"https://maps.googleapis.com/maps/api/place/textsearch/json?query=restaurants+in+sydney&sensor=false&key=YOUR KEY HERE"];

    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    //AFNetworking asynchronous url request
    AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];

    operation.responseSerializer = [AFJSONResponseSerializer serializer];
    [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {


        NSLog(@"The Array: %@",self.googlePlacesArrayFromAFNetworking);



    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {

        NSLog(@"Request Failed: %@, %@", error, error.userInfo);

    }];

    [operation start];

}

In Code Chunk 2 we first create a NSURL object and set it to the URL that we want to make the JSON request from. Then using that URL we create a NSURLRequest object. Using both of these objects we create an AFHTTPRequestOperation using the NSURLRequest.

With AFNetworking 1.x. the serialization was tied to the operation and you would call the correct operation request (IE AFJSONRequestOperation or AFXMLRequestOperation). Now with AFNetworking 2.0, you need to create a serializer property separately. Thus you create an operation and then add the serializer to it. See Recipe 16 for a good comparison.

Once the request and serializer are setup, you can set the completion block using the the setCompletionBlockWithSuccess method. This method uses a block to carry out the operation with two response types, either success or failure. AFNetworking will perform the request on a separate thread (i.e. not the main thread) and then call either the success block (if the request is successful) or the failure block (if the request fails) back on the main thread. Because either of these completion blocks are performed on the main thread, it is safe to add code that interacts with UIKit in either of these blocks.

For this example we’re using the Google places API. You’ll have to sign up for a Google Places API key which will allow 1000 request a day. Follow the instructions here to get signed up so you can get an API key: Google Places API Getting Started.

One of the Google examples uses the following request:


1
https://maps.googleapis.com/maps/api/place/textsearch/json?query=restaurants+in+sydney&sensor=false&key=Your_Key_Here

The Base URL here is https://maps.googleapis.com/maps/api/place/textsearch/json
Then there is a “?”. This denotes the beginning of our arguments. Each subsequent argument is separated by “&”.

So in this example we are using the JSON Places API to search for “restaurants in Sydney”, We are not using data from a GPS device so sensor = false, and of course Google wants our authentication key to make this request.

If you were to type this whole thing into your web browser you’ll get back a bunch of JSON formatted data that looks like this:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
   "html_attributions" : [],
   "next_page_token" : "CkQ0AAAANLLiCozQVXZAA2mRgg3yeG6LzdQM0-w1D54F2d_-    vDUC2xpthn2jf1aUmV1IxYMVY01jO9G8GgqhtM9iF0QJOxIQHin8zi8dVIYt3oFYzBOOCxoUXIIhOkFKi2h51CRLQZ5ENpqN23E",
   "results" : [
      {
         "formatted_address" : "Upper Level, Overseas Passenger Terminal, The Rocks NSW, Australia",
         "geometry" : {
            "location" : {
               "lat" : -33.8583790,
               "lng" : 151.2100270
            }
         },
         "icon" : "http://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png",
         "id" : "f181b872b9bc680c8966df3e5770ae9839115440",
         "name" : "Quay",
         "opening_hours" : {
            "open_now" : true
         },
         "photos" : [
            {………

Which is exactly what you should get if you replace “URL which Returns JSON” in the makeRestaurantRequest with this Google API formatted URL.

Go ahead and make the following changes to the makeRestaurantRequest:


1
NSURL *url = [NSURL URLWithString:@"https://maps.googleapis.com/maps/api/place/textsearch/json?query=restaurants+in+sydney&sensor=false&key=Your Api Key"];

Make sure you put your API key in.

Next call the makeRestaurantRequest method from the viewDidLoad method as shown in Code Chunk 3.

Code Chunk 3: Calling the makeRestaurantsRequests method from the viewDidLoad method


1
2
3
4
5
- (void)viewDidLoad
{
    [super viewDidLoad];
    [self makeRestaurantsRequests];
}

Now if you run the app you should see the output window populate with the JSON data.

Now you will edit your TableViewController.m interface section as shown in Code Chunk 4.

Code Chunk 4: Adding two array properties to hold JSON results


1
2
3
4
5
6
7
8
9
10
#import "TableViewController.h"
#import "ViewController.h"
#import "AFNetworking.h"

@interface TableViewController ()

@property (strong, nonatomic) NSArray *googlePlacesArrayFromAFNetworking;
@property (strong, nonatomic) NSArray *finishedGooglePlacesArray;

@end

Now, we will set the new arrays with data from the request so we can populate the table view. Update the makeRestaurantsRequest as shown in Code Chunk 5.

Code Chunk 5: Populating the array with results from the AFNetworking request


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
-(void)makeRestaurantRequests
{
    NSURL *url = [NSURL URLWithString:@"https://maps.googleapis.com/maps/api/place/textsearch/json?query=restaurants+in+sydney&sensor=false&key=Your Key Here"];

    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    //AFNetworking asynchronous url request
    AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];

    operation.responseSerializer = [AFJSONResponseSerializer serializer];
    [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {

        self.googlePlacesArrayFromAFNetworking = [responseObject objectForKey:@"results"];

        NSLog(@"The Array: %@",self.googlePlacesArrayFromAFNetworking);


    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {

        NSLog(@"Request Failed: %@, %@", error, error.userInfo);

    }];

    [operation start];

}

Setting Up The DataSource

OK! The Rest should look fairly familiar! First we’ll start off by setting the numberOfSectionsInTableView to have a return of 1 since we only want 1 section(Code Chunk 6).

Code Chunk 6: Setting up the number of sections in the table view


1
2
3
4
5
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    // Return the number of sections.
    return 1;
}

Now we’ll update the numberOfRowsInSection method to return the amount of paces returned from the JSON result(Code Chunk 7).

Code Chunk 7: Setting up the number of rows in the table view section


1
2
3
4
5
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    // Return the number of rows in the section.
    return [self.googlePlacesArrayFromAFNetworking count];
}

Last, but not least, we’ll update the cellForRowAtIndexPath method. Code Chunk 8 shows we are creating a dictionary object for each item in the googlePlacesArrayFromAFNetworking array. Then we are setting the cell label and details labels using those values. if the rating is null, we return not rated for the detail label text value.

Code Chunk 8: Setting up the cellForRowAtIndexPath dataSource method


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];

    NSDictionary *tempDictionary= [self.googlePlacesArrayFromAFNetworking objectAtIndex:indexPath.row];

    cell.textLabel.text = [tempDictionary objectForKey:@"name"];

    if([tempDictionary objectForKey:@"rating"] != NULL)
    {
        cell.detailTextLabel.text = [NSString stringWithFormat:@"Rating: %@ of 5",[tempDictionary   objectForKey:@"rating"]];
    }
    else
    {
        cell.detailTextLabel.text = [NSString stringWithFormat:@"Not Rated"];
    }

    return cell;
}

Now we’ll want to connect the dataSource outlet to the table view controller. There are two ways to do this, either select the tableView in the storyboard and click and drag from the connections inspector to the table view controller as shown in Figure 2, or simply add self.tableView.dataSource = self and self.tableView.delegate = self to the viewDidLoad method inside the TableViewController class. Since we aren’t actually using the delegate, that step is optional.

Figure 2: Connecting the DataSource and Delegate

Connecting the DataSource

Now if you run the app it should work right? Not quite. The Table view probably looks blank huh? That’s where that asynchronous request operation comes in. The table data source methods are loading before there is any information received from the request. To fix this, reload the table in the success block. This will refresh the table view after the data is loaded.Code Chunk 9 shows this change.

Code Chunk 9: Reloading the table view so data will show


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
-(void)makeRestaurantRequests
{
   NSURL *url = [NSURL URLWithString:@"https://maps.googleapis.com/maps/api/place/textsearch/json?query=restaurants+in+sydney&sensor=false&key=AIzaSyDfdpFcPFSr-0lTaxsyia84VU0VAsgEt1c"];

   NSURLRequest *request = [NSURLRequest requestWithURL:url];
   //AFNetworking asynchronous url request
   AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];

   operation.responseSerializer = [AFJSONResponseSerializer serializer];
   [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {

       self.googlePlacesArrayFromAFNetworking = [responseObject objectForKey:@"results"];

       NSLog(@"The Array: %@",self.googlePlacesArrayFromAFNetworking);

       [self.tableView reloadData];


   } failure:^(AFHTTPRequestOperation *operation, NSError *error) {

       NSLog(@"Request Failed: %@, %@", error, error.userInfo);

   }];

   [operation start];

}

Alright, Now you should have a result similar to Figure 3.

Figure 3: The Complete Table View

The Complete Table View

The last thing to do in the table view controller is set up the prepareForSegue method. This allows us to transfer data to the new view controller that we’ll be sent to when tapping a row. In Code Chunk 10 we are creating an NSIndexPath object and setting it to the index path for the selected row. Then we select the item in the googlePlacesArrayFromAFNetworking array and choose the item for the selected row at index path. Then we set the NSDictionary Object “restaurantDetail” in the ViewController class to this result.

code Chunk 10: Add the prepareForSegue method


1
2
3
4
5
6
7
8
9
10
#pragma mark - Prepare For Segue

-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];  
    ViewController *detailViewController = (ViewController *)segue.destinationViewController;
    detailViewController.restaurantDetail = [self.googlePlacesArrayFromAFNetworking objectAtIndex:indexPath.row];
}

Notice we are creating an instance of the ViewController class, so you'll need to import the ViewController.h file at the top of the TableViewController.m file as shown in **Code Chunk 11**.

Code Chunk 11: Importing the ViewController class


1
2
3
#import "TableViewController.h"
#import "ViewController.h"
#import "AFNetworking.h"

Setting Up The Detail View Controller

Now you will add four labels and a UIImage view to the view controller and arrange them as shown in Figure 4. The Address area label should be a multi-line label with at least 4 lines. Resize the label and change the properties from the attributes inspector. Create three new properties in the ViewController.m file by control-clicking and dragging from each of them. Name them restauranteNameLabel,restaurantImageView, and restaurantAddressLabel respectively. When you are done your ViewController.m interface should look like Code Chunk 12. Notice we also imported UIIMageView+AFNetworking.h at the top of the file. This will allow us to load the image asynchrounously using AFNetworking.

Figure 4: The Finished View Controller layout

The View Controller Layout

Code Chunk 12: The ViewController.m interface.


1
2
3
4
5
6
7
8
9
10
#import "ViewController.h"
#import "UIImageView+AFNetworking.h"

@interface ViewController ()

@property (weak, nonatomic) IBOutlet UILabel *restaurantNameLabel;
@property (weak, nonatomic) IBOutlet UIImageView *restaurantImageView;
@property (weak, nonatomic) IBOutlet UILabel *restaurantAddressLabel;

@end

Now update the ViewController.h and .m files as shown in Code Chunk 13 and 14

Code Chunk 13: Creating a dictionary property in ViewController.h


1
2
3
4
5
6
7
#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

@property (strong, nonatomic) NSDictionary *restaurantDetail;

@end

Code Chunk 14: Creating properties for the image view and labels.


1
2
3
4
5
6
7
8
9
- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    self.restaurantNameLabel.text = [self.restaurantDetail objectForKey:@"name"];
    [self.restaurantImageView setImageWithURL:[NSURL URLWithString:[self.restaurantDetail objectForKey:@"icon"]]];
    self.restaurantAddressLabel.text = [self.restaurantDetail objectForKey:@"formatted_address"];
}

Now you should be able to select one of the table view items and view the restaurant icon and address as shown in Figure 5.

Figure 5: The Complete app with a row selected

The Complete App

A Couple More Things

With AFNetworking 1.x, you had to manually parse out XML. Now with AFNetwowrking 2.0 This is no longer necessary. If you would like to receive XML, simply change the serialization from

AFJSONResponseSerializer

to

AFXMLResponseSerializer

.

Sometimes the format of the response will change. Either you will receive an NSArray as the responseObject or you will receive an NSDictionary. In the case of of the Google Places API, the format is an NSDictionary inside an NSArray inside an NSDictionary. Your response may be different, as such you may have to change the code to handle your format. Typically, most requests should return an array of dictionary objects.

Ok guys, That’s it, I know this tutorial is a little redundant, but a lot of folks were asking for it.

Enjoy!

Get the source code here.

About Joe Hoffman

I am An Electrical Engineer that spends a lot of my off time doing web development and other programming. Currently I am trying to expand my knowledge with iOS and I find that writing tutorials helps me learn more thoroughly as well as provide some useful info to others.

Comments

  1. The contents of this message body was deleted as it is not too helpful to the audience. That being said, however, it was helpful to me. Thanks very much for the feedback and keeping me honest.

    -Joe Hoffman

  2. Hey Joe – sweet stuff here.

    Couple of things that weren’t clear. I had to set the cell Identifier on the storyboard to be “cell” in order for it to work. Also had to change the cell layout from “custom” to “right detail”

    I was however able to get this working using a custom API I’ve created. Keep up these awesome real world tutorials!

  3. This is so helpful, thank you!

    However, I’m stuck on one part. For whatever reason my tableview just isn’t working properly. I had to add

    cell = [[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier]autorelease];
    cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;

    to get the subtitle and arrow and still the tableview overlaps the status bar and doesn’t push to the detailView. My code and connections are all perfect and exactly like yours! i am however embedding this in my project which has a tabbar that I added programmatically in the appdelegate – maybe that interferes? Any help would be appreciated!

    • Joe Hoffman says:

      Hi Shabna,

      These things can be a little tough to diagnose without looking at it. I’ll send you an email and maybe I can take a look at your app.

  4. Tim Foerster says:

    This was very, very helpful ! It solved my problem that I have had for weeks and weeks, thank you so much.

  5. Hey Joe,

    I think all your tutorials are awesome, especially this one. I have restarted this specific tutorial 5 times just to be se sure I’m not missing something. However, I keep running into this weird issue when I run the project, my Table view screen is blank as described after Code Chunk 8/Figure 2… I have all the code below that section entered as you do that should fix that problem but I’m not getting any results pushed into my array and the array is outputting null indicating there’s no data to insert into my listed view. My output is below, any thoughts on this? I’m sure I’m over looking something….. Thanks!

    2014-04-26 04:42:05.125 WebTableViewRound2[261:60b] The Array: (null)

    • Joe Hoffman says:

      Hi Brandon,

      If you have done everything as suggested, then it looks like maybe there is something wrong with the API call you are making to google. Try putting the URL into your browser and see what you get in return. Also I did notice a small error when I published> looks like the & symbols were replaced with

      &amp;

      Try this URL instead: https://maps.googleapis.com/maps/api/place/textsearch/json?query=restaurants+in+sydney&sensor=false&key=Your Key Here

      don’t forget to change your key.

      • I have been having the same issue. Pasting in the URL did not solve the problem either. Maybe it is is an issue with the bundle identifier. But in that case I believe an error would be recieved, rather than a blank array.

    • Hey Joe, thanks a bunch for making this tutorial. It’s amazingly helpful!

      I’ve been going through this tutorial for days and am returning no errors but can’t seem to find why my table view is blank. I am using a custom API that I created and know that the JSON is being parsed because I can see the output window being populated with the JSON data. Do you think you could take a look at my code to help me find the issue? It’s making me absolutely craaaazy!

      Thanks again for this gem 🙂 Will be sure to check out more of your tutorials!!

      • Joe Hoffman says:

        Hi Nikki,

        First guess: did you properly assign the delegate and datasource? Try adding

        self.tableView.delegate = self;
        self.tableView.datasource = self;

        to the ViewDidLoad method. Also put a breakpoint in the delegate methods and see if they are being called. IE like the cellForRow method.

        • Hey Joe!
          I was finally able to find the issue!!
          The structure of the JSON was nuts and I wasn’t able to figure it out correctly, which is why nothing was getting displayed. It turned out to have a lot of unnecessary redundancies in the structure and was something like an array within a dictionary within a dictionary within an array within a dictionary or something crazy like that. What a mess! So glad I was finalllyy able to resolve it. Thanks for all the help!

  6. HI,

    My app returns this:

    failed: unacceptable content-type: text/plain” UserInfo=0x7fa0bb40ae10 {com.alamofire.serialization.response.error.response= { URL: http://playfm.com.tr/service/top20/getlist.aspx } { status code: 200, headers {
    “Cache-Control” = private;
    “Content-Length” = 11662;
    “Content-Type” = “*/*; charset=utf-8”;
    Date = “Fri, 19 Sep 2014 19:41:56 GMT”;
    Server = “Microsoft-IIS/7.5”;
    “X-AspNet-Version” = “2.0.50727”;
    “X-Powered-By” = “ASP.NET”;
    } }, NSErrorFailingURLKey=http://playfm.com.tr/service/top20/getlist.aspx, com.alamofire.serialization.response.error.data=<5b7b2273 61726b69 4944223a 22534e47 33454535 30304522 2c227361 6e617463 69416422 3a225461 726b616e …..

    With a huge lift of numbers. Is it giving back NSData if so how can I convert/decode this?

    • Joe Hoffman says:

      HI Deniz,

      looks to be something up with your URL. looks like it’s sending back a 200 status code rather than JSON data. I don’t think it’s a problem with the code, but a problem with either the url or the site is not properly returning JSON.

Speak Your Mind

*

css.php
Privacy Policy