Thursday, June 19, 2014

How To: Create a Dynamic "More Apps..." Page & How to Customize UITableView Cells


BOO!!

WTF man. This short movie is super scary. I hope they make it into a longer one. It is called "Lights Out". You can watch it here:



Ok, what an intro eh? LOL :D


So anyway, this will be a simple tutorial on how to make your own More Apps... page INSIDE your own app. You can also simply create a viewcontroller with buttons in them and each button click goes to your app, but the problem is, when you update your apps or added new apps, you also need to update that viewcontroller, which sucks. :P

Anyway, I was playing around with iTunes Search API and found out that the reply is in JSON. And I love JSON, so I started to extract the data and put it in a simple TableView and Voila!

Initially I thought it'd be great to have a personal app so I can have a birdseye view of all of my apps. Especially the current price that I set to each one. Sometimes I set a different price (eg do a promo 50% price cut) and then I totally forgot to set it back somehow. Sure I can just open App Store but then App Store data display in the developer page is too simple. I also would like to see ratings, and current version number too all in one shot. So that is why I made this More Apps.. page. It is DYNAMIC. That is it displays exactly how it is in the Appstore. New apps released will be automatically available to this page!

THE iTUNES API.


You can read details about iTunes Search API here: https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html

For this tutorial, I use this basic URL request:

https://itunes.apple.com/search?term=DEVNAME&entity=software&attribute=softwareDeveloper

DEVNAME is a custom input by you. So if you want to see all of your apps, you key in your developer name as registered in iTunes. For simplicity I just add a UITextField in the UI and hook that up to the request URL. I also added a single UITableView to the MainView (where we will display and customize all the data)


Remember to connect all of the IBOutlets, Delegates and DataSource for TableView.

THE SEARCH REQUEST AND DATA PROCESSING


There are many ways to do a URL request. That is, in a way, doing a HTML Form submission inside an iOS app. 

This is how I do it:

NSString *str = [NSString stringWithFormat:@"https://itunes.apple.com/search?term=%@&entity=software&attribute=softwareDeveloper", devName.text];
    str = [str stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];

    NSURL *urlToSend = [[NSURL alloc] initWithString:str];
    NSURLRequest *urlRequest = [NSURLRequest requestWithURL:urlToSend
                                                cachePolicy:NSURLRequestReturnCacheDataElseLoad
                                            timeoutInterval:30];
        
    [self performSelectorOnMainThread:@selector(setBusyMsg:) withObject:@"Request Data" waitUntilDone:NO];
        
    NSData *urlData;
    NSURLResponse *response;
    NSError *error;
    urlData = [NSURLConnection sendSynchronousRequest:urlRequest
                                    returningResponse:&response
                                                error:&error];
    
    NSString *returnstring=[[NSString alloc]initWithData:urlData encoding:NSASCIIStringEncoding];
    
    
    // NSLog(@"Ret string %@",returnstring);
    
    NSData *jsonData = [returnstring dataUsingEncoding:NSUTF8StringEncoding];
    NSError *e;
    
    self.appDict = [NSDictionary dictionaryWithDictionary:[NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:&e]];
    
    NSLog(@"%@",appDict);

  self.appArray = [NSMutableArray arrayWithArray:[appDict objectForKey:@"results"]];


The devName is the UITextField that we added at the top. Note (see in project) that we are running this code in an autoreleasepool. And I use a custom made "progress window" called BusyAgent to show the status while searching. This eliminates the impression of "app hanging" while the app is processing in the background. Then we update the status in mainthread using [self performSelectorOnMainThread...] method.

If you are getting a matched result for your search query (ie your developer name), the log window will display NSDictionary data containing all available data of your apps. appDict is a NSDictionary initialized in viewdidload. You can add in codes to handle the error and timeout too, but Im not doing that for this tutorial.

There are 2 main dictionary keys from iTunes data - resultCount and results. resultCounts value is the number of apps found matched to the devName.text. And results are the array of each apps data and each app data is encoded as NSdictionary.

So basically we get a NSDictionary with value of arrays of NSDictionary! What we need is the app data in results. So we load the value of "results" into NSMutableArray, and now we have an array of apps data!

In simplified form, this:

{
  resultCount = 2;
  results = (
                   {
                        appdata1a= @"data1a";
                        appdata1b= @"data1b";
                   },
                   {
                        appdata2a= @"data2a";
                        appdata2b= @"data2b";
                   },
                 )
}


In a nutshell, { } = Dictionary. ( ) = Array

Lets take a deeper look into a real life data so we can format it into our app. For demonstration I typed Andreas Illiger in devName.text. :D Here's the data I got:



{
 "resultCount":1,
 "results": [
{"ipadScreenshotUrls":[], "appletvScreenshotUrls":[], "artworkUrl60":"http://is4.mzstatic.com/image/thumb/Purple3/v4/3d/57/31/3d573127-31d5-e4a9-9f8e-986c4616d467/source/60x60bb.jpg", "artworkUrl512":"http://is4.mzstatic.com/image/thumb/Purple3/v4/3d/57/31/3d573127-31d5-e4a9-9f8e-986c4616d467/source/512x512bb.jpg", "artworkUrl100":"http://is4.mzstatic.com/image/thumb/Purple3/v4/3d/57/31/3d573127-31d5-e4a9-9f8e-986c4616d467/source/100x100bb.jpg", "artistViewUrl":"https://itunes.apple.com/us/developer/andreas-illiger/id417817523?uo=4", "isGameCenterEnabled":false, "kind":"software", "features":[], 
"supportedDevices":["iPhone-3GS", "iPadWifi", "iPad3G", "iPodTouchThirdGen", "iPhone4", "iPodTouchFourthGen", "iPad2Wifi", "iPad23G", "iPhone4S", "iPadThirdGen", "iPadThirdGen4G", "iPhone5", "iPodTouchFifthGen", "iPadFourthGen", "iPadFourthGen4G", "iPadMini", "iPadMini4G", "iPhone5c", "iPhone5s", "iPhone6", "iPhone6Plus", "iPodTouchSixthGen"], "advisories":[], 
"screenshotUrls":["http://a4.mzstatic.com/us/r30/Purple/v4/3c/2f/f0/3c2ff0b5-b119-52d1-c7ea-4cefc0020dc8/screen320x320.jpeg", "http://a2.mzstatic.com/us/r30/Purple/v4/12/e7/a3/12e7a3b6-c961-75ec-5cbe-cfe383377905/screen320x320.jpeg", "http://a1.mzstatic.com/us/r30/Purple/v4/91/55/d1/9155d1df-1c0e-462a-2cba-2cdaa30cd536/screen320x320.jpeg", "http://a5.mzstatic.com/us/r30/Purple/v4/94/cf/07/94cf071d-8c17-8748-f8a6-314d80bb9f45/screen320x320.jpeg", "http://a5.mzstatic.com/us/r30/Purple/v4/4b/b4/86/4bb48662-6c71-9248-8556-4e132e9e504a/screen320x320.jpeg"], "averageUserRatingForCurrentVersion":4.5, "trackCensoredName":"Tiny Wings", "languageCodesISO2A":["NL", "EN", "FR", "DE", "IT", "ES"], "fileSizeBytes":"14409728", "sellerUrl":"http://www.andreasilliger.com/", "contentAdvisoryRating":"4+", "userRatingCountForCurrentVersion":5244, "trackViewUrl":"https://itunes.apple.com/us/app/tiny-wings/id417817520?mt=8&uo=4", "trackContentRating":"4+", "wrapperType":"software", "version":"2.1", "currency":"USD", 
"description":"You have always dreamed of flying - but your wings are tiny. Luckily the world is full of beautiful hills. Use the hills as jumps - slide down, flap your wings and fly! At least for a moment - until this annoying gravity brings you back down to earth. But the next hill is waiting for you already. Watch out for the night and fly as fast as you can. Otherwise flying will only be a dream once again.\n\nTiny Wings was chosen as the iPhone Game of the Year in App Store Rewind 2011 in Europe and many other countries.\u2028Thank you Apple and a big thank you to all Tiny Wings fans!\n\nHighlights:\u2028\n• simple but skillfull \"one button\" (ok... maybe \"one tap\") arcade game about the dream of flying\u2028\n• the world is changing every day - so it does in this game! Procedural generated graphics will make \"tiny wings\" look different every day you play\n• upgrade your nest by fulfilling tasks\n\u2028• Two game modes: \"Day Trip\" and \"Flight School\"\n• Play as the mama bird or one of her four children\n• 15 hand-designed levels in the new \"Flight School\" mode\n• iCloud support (even syncs your game between the iPhone & iPad versions)\n• optimized for iPhone 5", "artistId":417817523, "artistName":"Andreas Illiger", "genres":["Games", "Arcade", "Action"], "price":0.99, "trackId":417817520, "trackName":"Tiny Wings", "bundleId":"com.andreasilliger.tinywings", "releaseDate":"2011-02-18T21:28:46Z", "primaryGenreName":"Games", "isVppDeviceBasedLicensingEnabled":true, "currentVersionReleaseDate":"2014-08-14T00:01:15Z", "releaseNotes":"- Tuna islands / 5 new Level\n- bugfixes", "sellerName":"Andreas Illiger", "primaryGenreId":6014, "minimumOsVersion":"4.3", "formattedPrice":"$0.99", "genreIds":["6014", "7003", "7001"], "averageUserRating":4.5, "userRatingCount":219294}]
}



Look at all those data! OM NOM NOM!! HAHAHA!! We get quite useful data here, version number (keyname "version" duh), app name (with the "trackName" keyname), and more. You can't see them clearly here, but do copy and paste in any XCode files (.h or.m and XCode will change it to a clearly format - like below)



Now we can use these data in our app. But how do we put many details in our UITableView? Easy, we just customize the cell by code. There are many ways to customize cells, for example by using Prototype Cells in Interface builder etc, but me, personally I prefer doing things programatically.

So everything is done in UITableView delegate called - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath.

CUSTOMIZING UITABLEVIEW CELLS





- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *MyIdentifier = @"MyIdentifier";
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
    
    NSDictionary *individualApp = [self.appArray objectAtIndex:indexPath.row];
    
    NSString * documentsDirectoryPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];

    if (cell == nil)
    {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
                                       reuseIdentifier:MyIdentifier];
        
        UIImageView *iconImg = [[UIImageView alloc] init];
        iconImg.tag = 1;
        iconImg.frame = CGRectMake(5, 10, 20, 20);
        iconImg.layer.cornerRadius = 5;
        iconImg.layer.masksToBounds = YES;
        
        [cell.contentView addSubview:iconImg];
        
        
        UILabel *appNameLabel = [[UILabel alloc] initWithFrame:CGRectMake(35.0, 0.0, 300.0, 30.0)];
        [appNameLabel setTag:2];
        [appNameLabel setBackgroundColor:[UIColor clearColor]]; // transparent label background
        [appNameLabel setFont:[UIFont boldSystemFontOfSize:12.0]];
        [cell.contentView addSubview:appNameLabel];
        
        UILabel *appVersionLabel = [[UILabel alloc] initWithFrame:CGRectMake(35.0, 15.0, 300.0, 30.0)];
        [appVersionLabel setTag:3];
        [appVersionLabel setBackgroundColor:[UIColor clearColor]]; // transparent label background
        [appVersionLabel setFont:[UIFont systemFontOfSize:12.0]];
        [cell.contentView addSubview:appVersionLabel];
        
        UILabel *appPriceLabel = [[UILabel alloc] initWithFrame:CGRectMake(35.0, 30.0, 300.0, 30.0)];
        [appPriceLabel setTag:4];
        [appPriceLabel setBackgroundColor:[UIColor clearColor]]; // transparent label background
        [appPriceLabel setFont:[UIFont systemFontOfSize:12.0]];
        [cell.contentView addSubview:appPriceLabel];
      
    }
    
    for (UIView *view in [cell.contentView subviews]) {
        
        
        if (view.tag==1) {
            UIImageView *icn = (UIImageView *)view;
            icn.image = [UIImage imageWithContentsOfFile:[NSString stringWithFormat:@"%@/%@.png", documentsDirectoryPath, [individualApp objectForKey:@"trackId"]]];
        }
        
        if (view.tag==2) {
            UILabel *lbl1 = (UILabel *)view;
            lbl1.text = [individualApp objectForKey:@"trackName"];
        }
        
        if (view.tag==3) {
            UILabel *lbl1 = (UILabel *)view;
            lbl1.text = [NSString stringWithFormat:@"Version: %@ (%@)",[individualApp objectForKey:@"version"],[self getRatingText:[individualApp objectForKey:@"averageUserRatingForCurrentVersion"]]];
        }
        
        if (view.tag==4) {
            UILabel *lbl1 = (UILabel *)view;
            lbl1.text = [NSString stringWithFormat:@"Price: %@",[individualApp objectForKey:@"price"]];
        }
        
    }

    
    return cell;
}

There are 2 parts of codes in this delegate function. The first is inside "if (cell==nil)" and the second part is outside of that if statement. What does this mean? You need to understand what this delegate does. It basically returns the Cell object to the table. This means, when you reload the table (ie refresh) or simply scroll the table, the cells will be drawn according to the codes in "if (cell==nil)", ONCE. This is where we customize the components inside our cell.

Each cells have a "view" object and it is called contentView. So basically we can addSubview ANYTHING into this contentView. Here we see, I added UIImageView, and 3 UILabels. The creation and addition to contentView must only be ONCE when cell is undefined. But once it is created, there is no need to do so. So, what is happening in the second part of the code? The rest of the code simply updating the content of each object. Note the use of object's TAG number to identify each components.

To extract any app data from the appArray variable (this variable is retained through out the viewcontroller),  call out the dictionary object at a particular index.

NSDictionary *individualApp = [self.appArray objectAtIndex:index];

Then from this dictionary, we can then extract the value of each keys available.  For example to get ratings for currentversion,

[individualApp objectForKey:@"averageUserRatingForCurrentVersion"];

Easy right? Now you can use the data and format it in your app as you like. You can also filter the list according to Categories only by if statement like so:

NSArray *cat = [individualApp objectForKey:@"genres"];
if ([cat[0] isEqualToString:@"Photo & Video"]) {

   // display
}

In your app, you can remove the UITextField and simply hardcode your developer name in the request URL. And execute searchForApps in your viewDidload / viewDidAppear method.

See the app in action with my name :D


PS. I forgot to mention, the icons are saved in your sandbox. So it will take some space. But the icon i chose is a small one. Each icon is about 10kB. You might wanna choose a temporary directory to save them in instead of the normal Documents directory of your sandbox. Or simply write a delete routine everytime you close the page.

Ciao!


No comments:

Post a Comment