Friday, December 5, 2014

Multiline SKLabelNode? Hell Yes Please XD

Woah. Another Noobies Tutorial yayyyyyy....

Apple, naughty Apple. You made SpriteKit and SKLabelNode, but not make multiline labelnode. This is supposed to be expected!! OMG!! UILabel has multiline function, why not SKLabelNode?

Ok, lets not get crazy. Relax I am here to solve your woes.



As normal, I make this simple method to handle multiline SKLabelNode. Before we go on,
I'd like to give you alternatives (because some people already make some solution).

https://github.com/downrightsimple/DSMultilineLabelNode

The above isn't really a labelnode object. What it does is write down all the text with labelnode on a dummy space and capture it as image, and use that image as labels. Basically in your app it becomes SKSpritenode. This maybe is ok if you want a quick implementation. But capturing things into an image has lots of problems - namely memory usage, sizes (for example you need to cater for bigger screens making sure the image is not pixelated at high res, etc, etc).

https://github.com/nickfalk/NORLabelNode

The second method above is probably what you want if you prefer to have SKLabelNode that can use the "\n" (linebreaks). This is OK-ish. But I really don't like manually putting the line breaks. Especially if I change the wording then the line breaks need to be redone again, which SUCKSKKKKSK... but this solution is actually not bad.

OKAY! My method (which is obviously the best and smartest method XD) is to use SKLabelNodes, auto creating them through for loops depending on the length of string you have. You specify the rough character width per line, and this method autowraps it. It's like MAGIC. I know you will like this 10million times. XD

This is how the app will look like when you run it. Those are SKLabelNodes!


Ok now for the code. Read the comments to understand what each line does.


// For the sake of testing, I just use a dummy NSString and hardcode the string I want to display. 
// This string can be anything - even a string of paragraph that you retrieve from the web or pdf or whatever.
NSString *tmp = @"This is a long string where you do not need to bother about linebreaks and my method breaks it up into multiple SKLabelNode to roughly fit a customized width. Yep it works."; // long string - just type whatever in here
        
// parse through the string and put each words into an array.
NSCharacterSet *separators = [NSCharacterSet whitespaceAndNewlineCharacterSet];
NSArray *words = [tmp componentsSeparatedByCharactersInSet:separators];
        
int len = [tmp length];
int width = 20; // specify your own width to fit the device screen
        
// get the number of labelnode we need.
int totLines = len/width + 1;
int cnt = 0; // used to parse through the words array
        
// here is the for loop that create all the SKLabelNode that we need to 
// display the string.

for (int i=0; i<totlines; i++) {
 int lenPerLine = 0;
            NSString *lineStr = @"";
            
            while (lenPerLine<width) {
                if (cnt>[words count]-1) break; // failsafe - avoid overflow error 
                lineStr = [NSString stringWithFormat:@"%@ %@", lineStr, words[cnt]];
                lenPerLine = [lineStr length];
                cnt ++;
                // NSLog(@"%@", lineStr);
            }
 // creation of the SKLabelNode itself
            SKLabelNode *_multiLineLabel = [SKLabelNode labelNodeWithFontNamed:@"Oxygen Light"];
            _multiLineLabel.text = lineStr;
 // name each label node so you can animate it if u wish
 // the rest of the code should be self-explanatory
            _multiLineLabel.name = [NSString stringWithFormat:@"line%d",i];
            _multiLineLabel.horizontalAlignmentMode = SKLabelHorizontalAlignmentModeCenter;
            _multiLineLabel.fontSize = 20;
            _multiLineLabel.fontColor = [SKColor colorWithRed:1 green:1 blue:1.0 alpha:1.0];
            _multiLineLabel.position = CGPointMake(size.width/2, size.height/2+100-20*i);
            [self addChild:_multiLineLabel];
}

So that's all. Easy right? Feel free to improve it more. This method is ideal for games in SpriteKits.
For example when you want to make credits info, or a short storyline, or some instructions. It makes
creating and positioning the multiline text easy.

Have fun!

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:

{            artistId = 417817523;            artistName = "Andreas Illiger";            artistViewUrl = "https://itunes.apple.com/us/artist/andreas-illiger/id417817523?uo=4";            artworkUrl100 = "http://a709.phobos.apple.com/us/r30/Purple/v4/41/69/11/416911c6-f073-0819-4d83-c0358e337b58/mzm.yqhunxyp.jpg";            artworkUrl512 = "http://a709.phobos.apple.com/us/r30/Purple/v4/41/69/11/416911c6-f073-0819-4d83-c0358e337b58/mzm.yqhunxyp.jpg";            artworkUrl60 = "http://a216.phobos.apple.com/us/r30/Purple/v4/81/88/79/81887962-8334-ed87-7acd-3b49c44f96c8/Icon.png";            averageUserRating = "4.5";            averageUserRatingForCurrentVersion = "4.5";            bundleId = "com.andreasilliger.tinywings";            contentAdvisoryRating = "4+";            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\U2022 simple but skillfull \"one button\" (ok... maybe \"one tap\") arcade game about the dream of flying\U2028\n\U2022 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\U2022 upgrade your nest by fulfilling tasks\n\U2028\U2022 Two game modes: \"Day Trip\" and \"Flight School\"\n\U2022 Play as the mama bird or one of her four children\n\U2022 15 hand-designed levels in the new \"Flight School\" mode\n\U2022 iCloud support (even syncs your game between the iPhone & iPad versions)\n\U2022 optimized for iPhone 5";            features =             (                gameCenter            );            fileSizeBytes = 14598517;            formattedPrice = "$0.99";            genreIds =             (                6014,                7001,                7003            );            genres =             (                Games,                Action,                Arcade            );            ipadScreenshotUrls =             (            );            isGameCenterEnabled = 1;            kind = software;            languageCodesISO2A =             (                NL,                EN,                FR,                DE,                IT,                ES            );            price = "0.99";            primaryGenreId = 6014;            primaryGenreName = Games;            releaseDate = "2011-02-18T21:28:46Z";            releaseNotes = "- optimized for iPhone 5\n- bug fixes\n\nIt finally happened: The first official Tiny Wings Facebook page is online - get the latest news, FREE RINGTONES, Wallpapers and more :).\n\nhttp://www.facebook.com/TinyWingsFanPage";            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"            );            sellerName = "Andreas Illiger";            sellerUrl = "http://www.andreasilliger.com/";            supportedDevices =             (                iPad2Wifi,                iPhone4,                iPadWifi,                iPadThirdGen4G,                iPodTouchFifthGen,                iPad3G,                iPodTouchourthGen,                iPadFourthGen4G,                iPhone5s,                iPad23G,                "iPhone-3GS",                iPadThirdGen,                iPadMini,                iPhone5c,                iPhone4S,                iPadFourthGen,                iPhone5,                iPodTouchThirdGen,                iPadMini4G            );            trackCensoredName = "Tiny Wings";            trackContentRating = "4+";            trackId = 417817520;            trackName = "Tiny Wings";            trackViewUrl = "https://itunes.apple.com/us/app/tiny-wings/id417817520?mt=8&uo=4";            userRatingCount = 216905;            userRatingCountForCurrentVersion = 18877;            version = "2.0.2";            wrapperType = software;        }


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!


Related Posts Plugin for WordPress, Blogger...