CFBundleIconFiles Is Nice, But It’s Not Ready for Prime Time

June 30th, 2010 Jeremy View Comments

With the release of iPhone OS 3.2 Apple introduced a new way of managing the multiple resolutions of icon files that they require in an application bundle. CFBundleIconFiles was a new key in Info.plist designed to make life easier. It has made its way to the iPhone in iOS 4.0, however there are issues.

Functionally, the key allows developers to move away from naming the application icon “Icon.png” and adding archaic suffixes for all of the various resolutions. Instead, CFBundleIconFiles can be added to the plist (don’t confuse it with the CFBundleIconFile key that is already there) and all of the necessary icons can be called out in its array. The app will automatically select the one it needs based on resolution.

The issues begin with the face that CFBundleIconFiles is not already in the plist (as is CFBundleIconFile – the key for a single icon file), nor is it available in the dropdown list of new keys to add. They continue with Apple’s direction that any file included in your application bundle with a name matching the old convention such as “Icon.png” or “Icon-72.png” will be preferred by the app and will override the files noted in CFBundleIconFiles. And, they don’t stop there. We have had a lot of difficulty getting our apps to recognize the higher resolution files in the key, and if they do recognize them in the simulator then they still may not recognize them on every device. When we finally got everything working using CFBundleIconFiles for an iOS update of FAILApp Free we ran it through the new “Validate Application” feature in Xcode and were greeted with the following error for each of our icon files:

Icon specified in the Info.plist not found under the top level app wrapper

We checked the archived bundle, and all of the icons were there. Several workarounds and fixes have been suggested for this error, but we opted for the simple path of removing the CFBundleIconFiles key from Info.plist, renaming the icon files to conform with the prior convention (that is still preferred by the app), and making sure that no filename was specified in CFBundleIconFile in the plist. Everything worked fabulously and validated without a problem. As per Apple’s instruction, here are the filenames and associated resolutions that we used:

iPhone: Icon.png (57×57)
iPhone 4: Icon@2x.png (114×114)
iPad: Icon-72.png (72×72)
iPhone Settings: Icon-Small.png (29×29)
iPhone 4 Settings: Icon-Small@2x.png (58×58)
iPad Settings: Icon-Small-50.png (50×50)

Bottom line: The ability to name my icons according to my own convention and list them in the application’s info.plist with CFBundleIconFiles is nice, but it is not ready for prime time.

Post to Twitter Tweet This Post

The App Store Approval Process [updated]

May 23rd, 2010 Jeremy View Comments

I just received a call from Apple regarding FAILApp, the partner to our new LOLApp. It, well, FAILed the approval process. The points at which it “crossed the line” were sited as:

  • Public figure defamation
  • Privacy issues
  • Objectionable material

Curiously, it wasn’t the car crashes or poorly dressed brides that were the cause of these things. It was the fact that the likenesses of public figures appeared (in less than favorable ways) in some of the photos, people’s facebook posts were shown (and made fun of), and the word “engrish” was used to describe one of the photo feeds that has to do with humorous misuses of the English language.

Apple’s advice: think conservatively, remove anything we think they might think others might think is objectionable, submit the app, get it approved, and try to add back in the possibly objectionable content via updates.

Thanks Apple.

UPDATE:  It worked! We pulled out the objectionable content, and FAILApp and FAILApp Free were just approved. You can find them in the store by clicking their links.

Post to Twitter Tweet This Post

Categories: App Store Tags: , ,

Scrolling A UITextView When The Keyboard Appears

May 19th, 2010 Jeremy View Comments

It seems like the iPhone SDK should have given developers a more convenient way to scroll a view when the keyboard covers parts of the screen that should be visible for editing. If you are using a UIScrollView there are some relatively easy techniques to use but what if you are not using a scroll view?  In fact, there is a simple way to do this.

The short answer to the question is: Move (translate) the view the required amount when the keyboard appears and translate it back down when the keyboard is dismissed.

In our case we are using a standard UIView which contains a UITextView.  When the UITextView is selected the keyboard pops up to cover most of the text entry area.  We need to scroll the text entry area to the top of the screen to maximize the amount of text visible for user.

To solve this problem we will implement a delegate method of the UITextView called  textViewDidBeginEditing.  Since the UITextView “return” key doesn’t behave the same way as a UITextField we will also include a “Done” button in a toolbar just above the keyboard to dismiss the keyboard when text entry is complete.  To support the “Done” button we will also create our own “done”method.

xib with additional toolbar for above the keyboard

You’ll notice that the toolbar is sitting midway up the xib and not at the bottom. This was necessary to make it appear at the top of the keyboard rather than at some place under it. After creating the toolbar we set it’s opacity to 0 so that it was not visible until the keyboard appeared.

From here, it is a matter of translating the view and making the toolbar visible when editing then translating the view back and hiding the toolbar when we were done.

MakeUseOf Add Comment View

MakeUseOf Add Comment View with Keyboard

This was done with the following code:

Translate up and unhide the toolbar

self.view.transform = CGAffineTransformTranslate(self.view.transform, 0, -85);
self.toolbar.alpha = 1;

Translate back and hide the toolbar

self.view.transform = CGAffineTransformTranslate(self.view.transform, 0, 85);
self.toolbar.alpha = 0;

The CGAffineTransformTranslate method was used to translate the view’s transform by 0 pixels in the x-direction and 85 pixels in the y-direction. Translating by a negative value for x or y moves an object left or up respectively, and translating by a positive value moves an object right or down.

But, it’s no good to have the view jump to it’s new location while the keyboard slides up. So, we wrapped everything in some animation with a duration equal to the duration of the keyboard sliding into view. Here is the final code along with a line to release the keyboard when the Done button is tapped.

- (void)textViewDidBeginEditing:(UITextView *)textView {
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:.3];
self.toolbar.alpha = 1;
self.view.transform = CGAffineTransformTranslate(self.view.transform, 0, -85);
[UIView commitAnimations];
}
- (IBAction)done {
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:.3];
self.toolbar.alpha = 0;
self.view.transform = CGAffineTransformTranslate(self.view.transform, 0, 85);
[UIView commitAnimations];

[self.userComment resignFirstResponder];
}

In practice, you will need to experiment with the location of the toolbar in the xib and the number of pixels to translate in the y-direction to move the text you are editing into view.

We’ve glossed over some things such as wiring up the toolbar in the xib so that its opacity will change when we tell it to and so that our “done” method gets called when we tap the Done button. We also don’t have the toolbar moving exactly with the keyboard, but this could easily be done by adjusting it’s position in the xib and translating it up and down along with the view. But, hopefully, what we have shown here helps you out with your own project. If so, let us know in the comments and tell us the name of the app you are working on so we can look for it when it hits the store.

Post to Twitter Tweet This Post

Create an iPhone-like button in iWeb

May 13th, 2010 Jeremy View Comments

At some point you (like me) may find yourself needing a web button that looks like an iPhone button. So, if you are wondering “How do I make an iPhone button in iWeb?” read on…

The first step is to create a rounded rectangle.

Create a rounded rectangle

Basic rounded rectangle

Resize the rounded rectangle and set it’s Fill to “Advanced Gradient Fill”.

Advanced gradient fill

Add a couple of tabs to the Advanced Gradient Fill, set the one on the right to black, set the right middle to black, set the left middle to 75% grey, and set the one on the left to 50% grey. This creates the look of a glossy black button. The top half is brighter with a slight gradient making it brightest at the top.

Set the Stroke to be “Line” with a size of 2, and change its color to 75% grey. This puts a light border around the button to make it look inset. Naturally, you can leave this off if you don’t like it.

Glossy black gradient

Now you have a button image that can be resized to meet your needs. You can also play with adjusting the corner radius via the blue dot (in the image above). In addition, you can alter the colors in the gradient and alter the Opacity of the button to make it translucent.

Multiple buttons

To add a title to the button, create a text box, set the font to Helvetica, and make it a hyperlink. Set the “Normal” color to 90% grey and the “Rollover” color to white (100% grey). You may also want to select the button itself and make it a hyperlink pointing to the same location. It’s more intuitive this way since you don’t have to click directly on the text to make the button work.

Button title

That does it. It’s a basic button and it doesn’t change when you click on it. But, it looks great and is easily configurable (unlike using an image of a button). If you are curious, the background is an image fill using this image:

with a white rounded rectangle sitting behind the buttons.

Background

We are using this technique to create app content in iWeb that we open in a UIWebView. If you run one of our apps and go to “Technical Support” in the settings you will see a button like this.

Post to Twitter Tweet This Post

Maximize UILabel Contents With Dynamic Font Size

May 5th, 2010 developer View Comments

I recently had a requirement to show as much text in a UILabel as possible within a constrained space.  Realizing that some text in the label would be quite short and other text would be quite long I wanted to accomplish this by automatically scaling the font size down for longer text but allow a larger font size for shorter texts.  I found some code somewhere on the internet that partially met my requirements but it wasn’t scalable so I modified it.  I didn’t keep track of the source but if I remember where I got it from I’ll update the post to give credit but I thought it might be useful for others as well.

Here are a few examples of the code in action:

I simply turned the code snippet I found into a UIFont category to make it more easily accessible and reusable.

To use the category I pass the method the following parameters:

  • UILabel – This is the label which the text is constrained by
  • Text – This is the text which should be resized accordingly
  • Font Name – This is the font name to be used
  • Maximum Font Size – This is the largest font size that should be considered
  • Minimum Font Size – This is the smallest font size that should be considered

UIFont+DynamicSize.h


#import <Foundation/Foundation.h>

@interface UIFont (DynamicSize)
- (UIFont *)dynamicallySizedByLabel:(UILabel *)label text:(NSString *)text withFontName:(NSString *)aFontName maxFontSize:(int)maxFontSize minFontSize:(int)minFontSize;
@end

UIFont+DynamicSize.m


#import "UIFont+DynamicSize.h"

@implementation UIFont (DynamicSize)

- (UIFont *)dynamicallySizedByLabel:(UILabel *)label text:(NSString *)text withFontName:(NSString *)aFontName maxFontSize:(int)maxFontSize minFontSize:(int)minFontSize  {
    /* This is where we define the ideal font that the Label wants to use.
    Use the font you want to use and the largest font size you want to use. */
    UIFont *font = [UIFont fontWithName:aFontName size:maxFontSize];

    int i;
    /* Time to calculate the needed font size.
    This for loop starts at the largest font size, and decreases by two point sizes (i=i-2)
    Until it either hits a size that will fit or hits the minimum size we want to allow (i > 10) */
    for(i = maxFontSize; i > minFontSize; i=i-1)
    {
        // Set the new font size.
        font = [font fontWithSize:i];
        // You can log the size you're trying: NSLog(@"Trying size: %u", i);

        /* This step is important: We make a constraint box
        using only the fixed WIDTH of the UILabel. The height will
        be checked later. */
        CGSize constraintSize = CGSizeMake(label.frame.size.width, 2000.0f);

        // This step checks how tall the label would be with the desired font.
        CGSize labelSize = [text sizeWithFont:font constrainedToSize:constraintSize lineBreakMode:UILineBreakModeWordWrap];

        /* Here is where you use the height requirement!
        Set the value in the if statement to the height of your UILabel
        If the label fits into your required height, it will break the loop
        and use that font size. */
        if(labelSize.height <= label.frame.size.height)
            break;
        }
        // You can see what size the function is using by outputting: NSLog(@"Best size is: %u", i);

    return font;
}
@end

Usage


#import "UIFont+DynamicSize.h"

self.theTitle.font = [self.theTitle.font dynamicallySizedByLabel:self.theTitleLabel

                                        text:self.articleTitleString

                                        withFontName:@"Helvetica-Bold"

                                        maxFontSize:18

                                        minFontSize:10];

Post to Twitter Tweet This Post

Using The Wordpress API & XML-RPC with Objective C

May 3rd, 2010 developer View Comments

OBJECTIVE

Provide an Objective C example of implementing a Wordpress API call using XML-RPC.

BACKGROUND

In a previous post we worked with the Disqus API to post comments from an iPhone app to a blog website.  Today we will take a look at a different approach to posting blog comments.  Many blogs today are powered by Wordpress which has an API that uses XML-RPC.  XML-RPC uses XML over HTTP to make remote procedure calls.  It was created in 1998 and has now evolved into what is more commonly known as SOAP, or Simple Object Access Protocol.

XML-RPC is simpler to use and understand than SOAP because it

  • allows only one method of method serialization, whereas SOAP defines multiple different encodings
  • has a simpler security model
  • does not support (nor require) the creation of WSDL service descriptions, although XRDL provides a simple subset of the functionality provided by WSDL

IMPLEMENTATION

As always, to speed up the development process we will leverage a publicly available framework used by many other developers.  You will need to download the XML-RPC framework and integrate it into your project before proceeding.  You will also want to visit each of the links below.

To post a new comment we will use the wp.newComment method which has the following parameters:

  • int blog_id
  • string username
  • string password
  • int post_id
  • struct comment
    • int comment_parent
    • string content
    • string author
    • string author_url
    • string author_email

Our process flow is as follows:

  1. User enters a comment, provides a user name and email address, and presses a “Submit” button
  2. Construct the call to wp.newComment
  3. Execute the XML-RPC Request

STEP 1

For step one we will assume an AddCommentViewController.m exists in our project and the following three headers have been imported:


#import "XMLRPCRequest.h"
#import "XMLRPCResponse.h"
#import "XMLRPCConnection.h"

There should also be some instance variables for a user name, email address, the comment text itself, and a submit button.

STEP 2

After the user presses the submit button we want to construct our call to “wp.newComment”.  The comment structure is implemented as an NSDictionary in Objective C and the key names are the same as the parameters above.  To get started type the following code:

- (IBAction)submit {
NSDictionary *commentStructure =
          [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:0], @"comment_parent",
                                                          self.userComment.text, @"content",
                                                             self.userName.text, @"author",
                                                      @"http://www.someurl.com", @"author_url",
                                                            self.userEmail.text, @"author_email", nil];

 NSMutableArray *args = [NSArray arrayWithObjects:[NSNumber numberWithInt:0], @"", @"", [NSNumber numberWithInt:[self.feedItem.postID intValue]], commentStructure, nil];
 NSString *server = [[[NSString alloc] initWithString:@"http://www.siteurl.com/xmlrpc.php"] autorelease];         // the server
 NSString *method = [[[NSString alloc] initWithString:@"wp.newComment"] autorelease];                             // the method
 XMLRPCRequest *request = [[XMLRPCRequest alloc] initWithHost:[NSURL URLWithString:server]];
 [request setMethod:method withObjects:args];
 id response = [self executeXMLRPCRequest:request];
 [request release];
}

Let’s take a closer look at the code above. In the first several lines of code we are building an NSDictionary for each of the parameter items.

In line 9 we build the array which contains all of the required method parameters. The first parameter is a blog ID which may or may not be different in your case. The second and third parameters are left blank because in our case we
are posting anonymous comments. The fourth parameter is the post ID and finally the comment structure (NSDictionary).

In line 10 we create a string containing the URL path of the xmlrpc.php script which is listening for our remote call.

In line 11 we create a string containing the method name we wish to execute.

In line 12 we create an XMLRPCRequest object using our URL path to the xmlrpc.php script.

In line 13 we set the method name and arguments for our XMLRPCRequest object.

Finally, we execute the request in line 14.

STEP 3

Add the following code to execute the request and perform any necessary error handling (omitted below).

- (id)executeXMLRPCRequest:(XMLRPCRequest *)req {
 XMLRPCResponse *userInfoResponse = [XMLRPCConnection sendSynchronousXMLRPCRequest:req];
 return [userInfoResponse object];
}

SOLUTION

At this point you should have posted a comment successfully in Wordpress.

LINK SUMMARY

Post to Twitter Tweet This Post

Categories: iPhone OS Programming Tags:

Moving objects in an NSMutableArray

May 1st, 2010 developer View Comments

A glaring omission from the NSMutableArray class is the ability to move an object from one index to another.  There are methods for adding and deleting objects but not for simply moving.  One great approach is to use a category on NSMutableArray.  When we were looking to optimize our code we found a very well documented blog post on this topic so we’ll just point you to our original source.  It’s quite useful and very simple to implement.

http://www.icab.de/blog/2009/11/15/moving-objects-within-an-nsmutablearray/

Post to Twitter Tweet This Post

Using the Disqus API with Objective C

April 30th, 2010 developer View Comments

OBJECTIVE

Provide an Objective C example of implementing the Disqus API.

BACKGROUND

Recently Rade | Eccles developed a blog reader app for a website which uses Disqus as a commenting platform. We had a requirement to allow users to post comments from the app directly to the website’s Disqus sytem.

What is Disqus? (http://www.disqus.com)

“Disqus (dis·cuss • dĭ-skŭs’) is all about changing the way people think about discussion on the web. We’re big believers in the conversations and communities that form on blogs and other sites. Disqus Comments is for bloggers and publishers who wish to use Disqus on their sites, while Disqus Profile is for the rest of us who are commenting on sites powered by Disqus.”

Disqus provides some online API documentation; however, it is language agnostic.  The Disqus API documentation can be found here: http://wiki.disqus.net/API

DISQUS API INTRO

There are three types of Disqus objects that this API provides access to: forums, threads, and posts. A post is any comment written by a Disqus user. Each post belongs to a thread, which represents a particular topic of conversation. Each thread belongs to a forum. A forum represents a website that is using Disqus. For example, your blog might constitute a single forum, and each blog post would have its own thread.

The API is executed over HTTP using the http://disqus.com/api/(method_name)/ endpoint.  Some methods are a GET request and some are a POST.  All responses are returned using JSON.

IMPLEMENTATION

To speed up the development process we will leverage two publicly available frameworks used by many other developers.  You will need to download both frameworks and integrate them into your project before proceeding.

  1. ASIHTTPRequest – An excellent, easy to use wrapper around the CFNetwork API that makes some of the more tedious aspects of communicating with web servers easier. It is written in Objective-C and works in both Mac OS X and iPhone applications. You can download the code and view the documentation here: http://allseeing-i.com/ASIHTTPRequest/
  2. JSON Framework – This framework implements a strict JSON parser and generator in Objective-C. You can download the code and view the documentation here: http://code.google.com/p/json-framework/

To post a comment to Disqus we need to use the “create_post” API at the http://disqus.com/api/create_post/ endpoint.  This method is an HTTP POST and requires the following parameters:

  • Thread ID
  • Message
  • Author Name
  • Author Email

The message, author name, and author email will come from our app but we need to ask Disqus what the correct thread ID is before we can successfully post a comment.

Our process flow is as follows:

  1. User enters a comment, provides a user name and email address, and presses a “Submit” button
  2. Request the thread ID from Disqus based on the URL of the thread using the “get_thread_by_url” method (GET)
  3. Parse the JSON response to access the thread ID
  4. POST the comment using the “create_post” method

STEP 1

For step one we will assume an AddCommentViewController.m exists in our project and the following three headers have been imported:


#import "ASIFormDataRequest.h"
#import "ASIHTTPRequest.h"
#import "SBJSON.h"

There should also be some instance variables for a user name, email address, the comment text itself, and a submit button.

STEP 2

After the user presses the submit button we want to construct our call to “get_thread_by_url”. This method requires two parameters: the forum key and the URL of the thread in which to post a comment. A forum key can be shared among trusted moderators of a forum, and is used to perform actions associated with that forum. The creator of a forum (an administrator or website owner) can get the forum’s key through the API (see below). The API documentation provides instructions on how the administrator can obtain the forum key if necessary.

- (IBAction)submit {
    NSString *DisqusForumKey = [[NSUserDefaults standardUserDefaults] objectForKey:self.feed.forumKey];

    NSString *urlString = [[[NSString alloc] initWithString:@"http://disqus.com/api/get_thread_by_url/"] autorelease];
    urlString = [urlString stringByAppendingFormat:@"?api_version=1.1&url=%@&forum_api_key=%@", blogPostURL, DisqusForumKey];
    NSURL *url = [[NSURL alloc] initWithString:urlString];

    ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
    [request setDelegate:self];
    [request setDidFinishSelector:@selector(requestDidFinishForThreadID:)];
    [request startAsynchronous];
}

Let’s take a closer look at the code above. In line two we are setting a variable which contains the forum key for the current thread.  You may or not be able to simply hard code the forum key into your code.  In our case we were dealing with multiple distinct and separate forums (databases) each with their own set of threads.

In line four we construct a string with our method endpoint.

In line five we add to that string an optional api version parameter, the URL of the thread, and the forum key.

In line six we construct an NSURL object from our new URL string.

In line eight we construct an ASIHTTPRequest object using our NSURL object.

In line nine we set the delegate to self so we can handle the callback when a response is received.

In line 10 we need to specify a custom callback method because we will be handling two different types of JSON responses in our view controller since we are making two different API calls.  If we didn’t specify a custom didFinishSelector the default callback method would be  - (void)requestFinished:(ASIHTTPRequest *)request. Here we will set requestDidFinishForThreadID as our desired callback.

Finally in line eleven we kick off the asynchronous HTTP request.

STEP 3

Now we need to parse the thread ID out of the JSON response in our custom callback method. All responses are JSON objects with three fields:

  • succeeded: Indicates whether the call completed successfully or encountered an error.
  • code: “ok” if succeeded, otherwise an short description of the error that occurred.
  • message: The body of the response, which will depend on the method being used. In case of error, contains a longer description of the error that occurred.

- (void)requestDidFinishForThreadID:(ASIHTTPRequest *)request {
     NSString *responseString = [request responseString];
     SBJSON *jsonParser = [SBJSON new];
     NSDictionary *dict = (NSDictionary*)[jsonParser objectWithString:responseString];
     [jsonParser release];

     // get thread id
     NSDictionary *messageDictionary = (NSDictionary *)[dict objectForKey:@"message"];
     NSString *threadID = (NSString *)[messageDictionary objectForKey:@"id"];

}

In line two we extract the response string from the HTTP request.

In line three we create a JSON Parser.

In line four we use the JSON parser to return a dictionary of parameters.

In line eight we get a dictionary from the “message” field of the JSON response. The call to the “get_thread_by_url” method returns a thread object in the message field. This object has several parameters which are documented in the Disqus API; however, we are only interested in the ID parameter.

In line ten we retrieve the actual thread ID for the specified URL from the message dictionary.

STEP 4

After we have parsed the thread ID we are ready to construct the call to the “create_post” method to submit the comment to the Disqus database. Add the code below to the requestDidFinishForThreadID method so it looks like this:


- (void)requestDidFinishForThreadID:(ASIHTTPRequest *)request {
 // Use when fetching text data
 NSString *responseString = [request responseString];

 SBJSON *jsonParser = [SBJSON new];
 NSDictionary *dict = (NSDictionary*)[jsonParser objectWithString:responseString];
 [jsonParser release];

 // get thread by url results
 NSDictionary *messageDictionary = (NSDictionary *)[dict objectForKey:@"message"];
 NSString *threadID = (NSString *)[messageDictionary objectForKey:@"id"];

 // assemble the POST data
 NSString *DisqusForumKey = [[NSUserDefaults standardUserDefaults] objectForKey:self.feed.forumKey];

 NSURL *url = [[NSURL alloc] initWithString:@"http://disqus.com/api/create_post/"];
 ASIFormDataRequest *requestPostComment = [ASIFormDataRequest requestWithURL:url];
 [requestPostComment setPostValue:@"1.1" forKey:@"api_version"];
 [requestPostComment setPostValue:threadID forKey:@"thread_id"];
 [requestPostComment setPostValue:userName.text forKey:@"author_name"];
 [requestPostComment setPostValue:userEmail.text forKey:@"author_email"];
 [requestPostComment setPostValue:userComment.text forKey:@"message"];
 [requestPostComment setPostValue:DisqusForumKey forKey:@"forum_api_key"];
 [requestPostComment setDelegate:self];
 [requestPostComment startAsynchronous];

}

Lines 14 and 16 should be familiar as we are getting the forum key and constructing the POST for the “create_post” method.

In line 17, this time we will use ASIFormDataRequest which is used to make an HTTP POST.

In lines 18-23 we set the required parameters for this method call.

Next, we set the delegate to self to receive the response in the callback method. In this case, since we didn’t specify a didFinishSelector the default requestFinished method will be called. Here we could check for errors/failures an issue an appropriate message to the user if necessary.

Finally we kick off the asynchronous HTTP POST.

SOLUTION

At this point as long as the forum keys and the thread URL is correct you should have posted a comment successfully in Disqus.

LINK SUMMARY

Disqus – http://www.disqus.com

Disqus API – http://wiki.disqus.net/API

ASIHTTPRequest – http://allseeing-i.com/ASIHTTPRequest/

JSON Framework – http://code.google.com/p/json-framework/

Post to Twitter Tweet This Post

Categories: iPhone OS Programming Tags: