Creating a PreferenceBundle for the iPhone.
IPhone Settings Within Settings.app
This document assumes that the reader has intermediate to expert knowledge and skills in the creating of applications for a jailbroken iPhone or iPod Touch. If you have any questions regarding this material, feel free to comment below.
Settings Bundles vs. Preference Bundles
Settings Bundles
Settings bundles are what you would include if you were creating an AppStore application. Generally, they can be found within your application’s app bundle. Creating these is very simple. Create a directory within your application bundle called Settings.bundle. Within this directory, create a file called Root.plist. This file will be the first plist that Settings reads. You can keep most or all of your settings within Root.plist. You can supplement this file with more plists as needed. Extensive information can be found by following this link to apple’s reference guide. http://developer.apple.com/iphone/…/TP40007072-CH13-SW10
The three downsides to this are enormous. Number one, you cannot execute code within the Settings application. Number two, you are limited to using only certain types of table cells within your settings. And most importantly, your Settings.bundle needs to be located in your application bundle, in ~/Applications/. This cannot be if you are coding for a jailbroken environment which wants your application to live in /Applications/
Preference Bundles
Preference bundles are the method that Apple uses to present settings to the user. Notable examples are the application settings which can be found in Settings (Preferences.app) for iPod, Phone, Safari, and the rest. Using a Preference bundle allows you to execute your own code within Settings. This is helpful if you need to set something other than a simple bool/string/whatever, generate a dynamic list of options, or present the user with a grid of thumbnails. All these can be seen within the default Settings that Apple includes.
One of the downsides to Preference bundles is that they must be contained within /System/Library/PreferenceBundles/. This generally is not a real issue, so long as you tell your application’s repository host to package the file accordingly. Another hurdle to overcome is to actually get Settings to show your Preference bundle. Through much trial and error within Cydia control files, this has been overcome very quickly, cleanly, and effectively. I will offer sample code on how to have your application insert itself into Settings. I can offer you no advice on doing this within Installer or Icy, as I do not use those applications, and I highly recommend that other developers do not use them either. The final hurdle you must overcome with a Preference bundle is figuring out how to make one. Until now, this has not been documented at all. Luckily, the process is actually fairly simple, and I will try to shed what light that I can.
Preparation
In order to make a Preference bundle, you must have the proper headers. Preference bundles appropriately link to the Preferences.framework. To get this framework’s headers, you will need to follow a couple of steps.
First, open Cydia and search for class-dump. Install that.
Secondly, enter these three commands into either Terminal on your iPhone, or your favorite SSH
- mkdir –p /tmp/classes/Preferences.framework
- cd /tmp/classes/Preferecnes.framework
- class-dump –H /System/Library/PrivateFrameworks/Preferences.framework/Preferences
Once done, move the files it created to wherever you keep your header files in a directory called Preferences. This location will depend on where you built your toolchain.
Congratulations, you’ve just completed the necessary set up. See, this isn’t all that bad.
The Preference Bundle’s Skeleton
The actual bundle itself will be coded and built as if it were an executable. Essentially, it is. To be a little more precise, a Preference Bundle is nothing more than a series of hook that Preferences.app catches and reads.
The first thing to do is create YourAppSettings.h. Immediately, you will need to import a few files. Add these lines to the beginning of your file:
#import <Foundation/Foundation.h> #import <UIKit/UIKit.h> #import <Preferences/PSListController.h>
Next, you will need to subclass the PSListController. This is a subclass of PSViewController. PSViewController, as its name implies is a type of view controller. I am making the assumption that if you are making a Preference bundle, then you are familiar with basic concepts such as view controllers. Subclass these like so
This the base class of your Preference bundle. Get to know it well. It is also important to note that each LocalizedListController is a “screen” in the Settings application. That is, for each page, you will need to create a new subclass of it.
Adding Some Meat To The Bones
Of course, you are going to want to actually display your settings to the user. So to the same YourAppSettings.h that we are building, add your first controller like so. Notice that we are subclassing the LocalizedListController that we just prototyped.
Notice, that so far, we are doing the exact same thing you’d do within your application’s main executable file. This is important to note because as the need arises, you can very simply add in some ivars and methods for each list.
This is all well and good, but what does it do? As of right now, nothing. So let’s create YourAppSettings.m to pair up with the YourAppSettings.h that we are building up. The first thing we want to do is make your LocalizedListController actually do something. As stated before, it is the actual hook into Preferences.app. So let’s have it do something.
@implementation LocalizedListController - (NSArray *)localizedSpecifiersForSpecifiers:(NSArray *)s { int i; for(i=0; i<[s count]; i++) { if([[s objectAtIndex: i] name]) { [[s objectAtIndex: i] setName:[[self bundle] localizedStringForKey:[[s objectAtIndex: i] name] value:[[s objectAtIndex: i] name] table:nil]]; } if([[s objectAtIndex: i] titleDictionary]) { NSMutableDictionary *newTitles = [[NSMutableDictionary alloc] init]; for(NSString *key in [[s objectAtIndex: i] titleDictionary]) { [newTitles setObject: [[self bundle] localizedStringForKey:[[[s objectAtIndex: i] titleDictionary] objectForKey:key] value:[[[s objectAtIndex: i] titleDictionary] objectForKey:key] table:nil] forKey: key]; } [[s objectAtIndex: i] setTitleDictionary: [newTitles autorelease]]; } } return s; }; - (id)navigationTitle { return [[self bundle] localizedStringForKey:_title value:_title table:nil]; } @end
The first method we entered, the localizedSpecifiersForSpecifiers, will load the specifiers form your plist. Simply put a PSSpecifier, or specifier, is a table cell. There are a few different types, but each is a subclass of the basic PSSpecifier. A slightly more detailed look into specifiers reveals that they are objects that contain several important bits of information, such as exactly which type of cell to display, what the cell’s title should be, the default values of these cells, if a certain action should be taken when the user interacts with the cell, and so on. All of these bits if information will be provided by a plist you create that lives within your Preferences bundle.
The second method we provided is the method that takes the title provided by your plist and sets it as the UINavigtionController within Settings’ title.
These are the basic operations that you want all of your list controllers to perform. Now we will move on to the LocalizedListControllers’ subclass. Remember that you will want to subclass it for each page in your settings. Right now, we are focused with the main page, although ALL pages will be set up like this. In your YourAppSettings.m, enter this implementation to match the interface we added earlier:
What we are doing is telling Settings that we want to load our settings from the plist file called YourAppSettings.plist. We load the specifiers from our plist, and feed it through the method we established in the LocalizedListController implementation. Now, we can compile this and have it work. So let’s create a makefile to automate the process for us.
The Makefile
You may expect a detailed and complicated makefile to be involved in making the Prefrence bundle, but the truth is that it isn’t. You merely have to create a makefile exactly as you would a standard application and add in one more flag
-bundle
That’s it.
The Specifier Plist
As you may have guessed, the Preference bundle won’t display a single thing without having a plist to read from. So we’ll go ahead and create that now. This is going to be very similar to the plists you would create if you were making a Settings bundle. You can view several samples of these by either navigating on your iPhone to /Applications/Preferences.app or /System/Library/PreferenceBundles and viewing all the plists there. Also, in Cydia, you may download and view the contents of my PocketTouchSettings.bundle, which is included with the application PocketTouch. Here is a link to the main plist for PocketTouch’s settings. http://www.touchrepo.com/guides/preferencebundles/PocketTouch.plist . Please study it well. I won’t go into too much detail, as it is mostly the same as creating a plist for the Settings bundle. Create an array of dicts in our plist laid out in the arrangement you would like your cells within Settings to appear.
To create a group cell, use xml similar to this:
<dict> <key>cell</key> <string>PSGroupCell</string> <key>label</key> <string>Group Title</string> </dict>
Any other cell that follows this will be part of that group. Entering a new PSGroupCell will create a new group. To create cell within a group, use xml similar to this:
<dict> <key>cell</key> <string>PSLinkListCell</string> <key>detail</key> <string>AnotherPageController</string> <key>label</key> <string>AnotherPage</string> </dict>
The “cell” key tells Settings to use a cell that has a disclosure chevron on the right side to indicate that tapping that cell will reveal another page. Actually tapping the cell will make the Settings UINavigationController push to another page. The “detail” key tells Settings which subclassed version of your LocalizedListController it should use to populate the next page of settings. The “label” key tells Settings what to label the link cell.
Of course, this is useless without adding the new controller to your PreferenceBundle. So go ahead and reopen YourAppSettings.h and add a new subclass. It will be identical to the first subclass your created that houses your main settings. In this case, we’ll have something like this:
Look familiar? Now open up YourAppSettings.m and create AnotherPageController’s implementation:
It should be exactly the same as the last subclass, with the exception of the name of the class. Also, we are changing the loadSpecifiersFromPlistName to accurately represent the plist that we want this LocalizedListController to read from and control.
Now, create a new plist for this page, and we have another page of settings. Repeat these steps until you have all the pages you require. Recompile and enjoy!
Even More Control
Buttons
As stated before, one of the benefits is the ability to execute code. There are a couple of ways to do this. If you are trying to make it obvious to the user that they are about to make Settings “do something,” then you will most likely want to create a button cell. To do this, add something like this to your specifier plist:
<dict> <key>action</key> <string>performAction:</string> <key>cell</key> <string>PSButtonCell</string> <key>label</key> <string>Perform An Action</string> </dict>
By now, you should know what you are looking at. But you will notice the addition of an “action” key. This key is the name of the method that will be executed when user taps that button. Notice how the method name ends with a colon (:). This is not decoration and is absolutely crucial.
To make your Prefence bundle actually do something with that, reopen your YourAppSettings.m and scroll to the implementation that controls the plist that contains your button. Add in the function where appropriate. It will look something like this:
@implementation YourAppSettings - (NSArray *)specifiers { NSArray *s = [self loadSpecifiersFromPlistName:@"YourAppSettings" target: self]; s = [self localizedSpecifiersForSpecifiers:s]; return s; } -(void)performAction:(id)param { /*Add code to be executed here. Anything goes, so don’t feel limited by simply being in Settings */ } @end
Recompile and enjoy!
Performing Actions Sans Buttons
Sometimes you may need to perform actions at when the user flips a switch. Sometimes you may want to dynamically populate a list based on the files in a directory. Sometimes you may want to do something after the user enters a string. Who knows what you may need to do. In all of these examples, using a button would be inappropriate and would degrade the user experience. Settings allows for these types of situations by enabling your specifier to be its specified cell type and perform an action. To add an action to any cell, you will need to add a “set” tag to it. Do so like this
<dict> <key>cell</key> <string>PSSwitchCell</string> <key>default</key> <true/> <key>defaults</key> <string>com.yourapp.identifier</string> <key>key</key> <string>YourBool</string> <key>label</key> <string>A Switch With Background Code</string> <key>set</key> <string>setSomethingHere:specifier:</string> </dict>
In this example, I have used a standard switch cell. You’ll notice that I added in a “set” key. Look closely at the attached string: “setSomethingHere:specifier:”. That is the name of the function that is executed. It is absolutely crucial that the string follows this format. The first part is what you will use to label the function. The specifier is required and will enable you to refer to this specific specifier within your Preference bundle. Make sure to include both colons (:).
In your YourAppSettings.m, you will want to scroll to the implementation that controls the plist that contains the specifier that we are working with. Add the function where appropriate, but take note of some special required code here. The application will look like this:
@implementation YourAppSettings - (NSArray *)specifiers { NSArray *s = [self loadSpecifiersFromPlistName:@"YourAppSettings" target: self]; s = [self localizedSpecifiersForSpecifiers:s]; return s; } -(void)setSomethingHere:(id)value specifier:(id)specifier { [self setPreferenceValue:value specifier:specifier]; [[NSUserDefaults standardUserDefaults] synchronize]; if(value == kCFBooleanTrue){ /*Add code here*/ } else { /*Add code here*/ } } @end
You can do whatever you want, but make sure to include the lines where you actually set the value:
[self setPreferenceValue:value specifier:specifier]; [[NSUserDefaults standardUserDefaults] synchronize];
The first line sets the value for the specifier using the data you provided within the plist (the settings file, the key, etc). The second line synchronizes the settings so that Settings will accurately reflect the value of this key to the user. That way the user doesn’t keep trying to reset this variable.
The next couple of lines you’ll notice, the if() statement is to illustrate that when dealing with Boolean values, as often you will be, you must refer to them as kCFBooleanTrue and kCFBooleanFalse.
Extras
Taking a look through the headers you obtained through class dumping Preferences.framework reveals quite a lot of things you can do to add some polish to your application. One of the best effects is to remove settings that the user does not need to see. An example of this would be in the Date & Time settings within Settings. When you have it set to set the date and time automatically, the “Time Zone” and “Set Date & Time” cells animate and disappear from the user’s view. This helps to keep things as clean and simple as possible. Turning off “Set Automatically” will make the specifiers slide back into the user’s view.
The easiest way to accomplish this is by simply adding this method into the controller for the specific specifier you want to animate:
[self removeSpecifier:[specArray objectAtIndex:13] animated:YES];
Note, in this case specArray is the array of specifiers that we generate when we load from the plist. To add the specifier, use something like this:
[self insertSpecifier:[specArray objectAtIndex:4] afterSpecifier:specifier animated:YES];
This is another reason that it is important to feed your PreferenceBundle the “specifier:” within its “set” tag. You can refer to the user selected specifier by calling “specifier.” It really helps to simplify things for you. Although, you can add and remove specifiers from anywhere within your specifier array, it does not have to be the immediately following specifier. For example, take a look at these methods contained within PSListController.h
- (void)insertSpecifier:(id)fp8 atIndex:(int)fp12 animated:(BOOL)fp16; - (void)insertSpecifier:(id)fp8 afterSpecifier:(id)fp12 animated:(BOOL)fp16; - (void)insertSpecifier:(id)fp8 afterSpecifierID:(id)fp12 animated:(BOOL)fp16; - (void)insertSpecifier:(id)fp8 atEndOfGroup:(int)fp12 animated:(BOOL)fp16; - (void)insertSpecifier:(id)fp8 atIndex:(int)fp12; - (void)insertSpecifier:(id)fp8 afterSpecifier:(id)fp12; - (void)insertSpecifier:(id)fp8 afterSpecifierID:(id)fp12; - (void)insertSpecifier:(id)fp8 atEndOfGroup:(int)fp12; - (void)insertContiguousSpecifiers:(id)fp8 atIndex:(int)fp12 animated:(BOOL)fp16; - (void)insertContiguousSpecifiers:(id)fp8 afterSpecifier:(id)fp12 animated:(BOOL)fp16; - (void)insertContiguousSpecifiers:(id)fp8 afterSpecifierID:(id)fp12 animated:(BOOL)fp16; - (void)insertContiguousSpecifiers:(id)fp8 atEndOfGroup:(int)fp12 animated:(BOOL)fp16; - (void)insertContiguousSpecifiers:(id)fp8 atIndex:(int)fp12; - (void)insertContiguousSpecifiers:(id)fp8 afterSpecifier:(id)fp12; - (void)insertContiguousSpecifiers:(id)fp8 afterSpecifierID:(id)fp12; - (void)insertContiguousSpecifiers:(id)fp8 atEndOfGroup:(int)fp12; - (void)removeSpecifier:(id)fp8 animated:(BOOL)fp12; - (void)removeSpecifierID:(id)fp8 animated:(BOOL)fp12; - (void)removeSpecifierAtIndex:(int)fp8 animated:(BOOL)fp12; - (void)removeSpecifier:(id)fp8; - (void)removeSpecifierID:(id)fp8; - (void)removeSpecifierAtIndex:(int)fp8; - (void)removeLastSpecifier; - (void)removeLastSpecifierAnimated:(BOOL)fp8;
Neat, huh? Preferences.app offers more than enough flexibility to suit almost any need.
Another popularly requested effect is the you see when you open Settings. Scrolling down to the bottom, you will see an inactive cell with the text “Loading Applications…” Obtaining the same effect is incredibly simple. First of all, have your controller load the specifiers from the plist as needed, just like before with no modifications whatsoever. Then add this method to the controller:
- (void)viewDidBecomeVisible;
This will execute after the view becomes visible. You will want to dynamically create an array of specifiers (or read them from a plist, whatever you need to do) and call one of these functions:
- (void)addSpecifier:(id)fp8; - (void)addSpecifier:(id)fp8 animated:(BOOL)fp12; - (void)addSpecifiersFromArray:(id)fp8; - (void)addSpecifiersFromArray:(id)fp8 animated:(BOOL)fp12;
As you can see, working within the realm of Preferences.app is incredibly simple. Take a look through your headers and pick out the methods that strike you as something you’ll use. Some of the most useful, in my opinion are the viewDidBecomeVisible and viewWillReappear.
- (void)viewDidBecomeVisible; - (void)viewWillRedisplay;
There are literally tons of other useful methods that cover everything from presenting UIAlerts to overriding the standard UINavigationController functions.
Getting Preferences.app to Acknowledge your Preference Bundle’s Existence
What to do
As stated before, a Preference bundle is a little trickier than a Settings bundle when it comes to getting Settings to hook it. With a Settings bundle, the mere existence of the Settings bundle will cause Settings to show it. With a Preference bundle, a PSLinkCell must be added to Preferences.app’s main settings plist for both the iPhone and the iPod Touch. Add the specifier as the second to last specifier in the array, as for some odd reason, Settings will not display the last specifier. The specifier will look similar to this:
<dict> <key>bundle</key> <string>YourAppSettings</string> <key>cell</key> <string>PSLinkCell</string> <key>hasIcon</key> <integer>1</integer> <key>isController</key> <integer>1</integer> <key>label</key> <string>Your App</string> </dict>
The two plists that need to be amended are
/Applications/Preferences.app/Settings-iPhone.plist and /Applications/Preferences.app/Settings-iPod.plist
remember, not everybody will have the same device as you. Looking at the specifier, it should be pretty apparent to you what does what. You may notice the “hasIcon” key. You can add an icon to any specifier, but not by using “hasIcon.” Instead, you will simply add the “icon” key to your specifier and attach a string that contains the name of your icon. The icon must be in the PreferenceBundle. The icon will appear on the left of the specifier’s cell and can be any width.
After a bit of trial and error while trying to find the best way to insert this specifier into the two main settings files pain free and as simple as possible, it was decided that having the application insert the specifier was the absolute best way. What we want to do is expand your application to accept arguments. The repository manager hosting your package can simply add to the install and uninstall procedure these two lines:
- /Applications/YourApp.app/YourApp -–installPrefBundle
- /Applications/YourApp.app/YourApp –removePrefBundle
This has two main benefits. Primarily, it makes the install/uninstall process is simple and very easily managed, removing headaches from everybody involved. It also is a tremendous help when users have issues with your app’s settings not showing up in Settings. Simply have them issue the appropriate command via Terminal on their device, or SSH. Whichever is easiest for them.
How to do it
This is very simple code. I will provide you with the main.m for PocketTouch. I borrowed the technique from Sam Steele’s MobileScrobbler.app. What the app does is accepts the arguments presented, and reacts accordingly. Upon completing the tasks, it closes. This should be a fairly easy file to adapt to our own needs. Remember that it is covered by GPL.
/* main.m - PocketTouch * Copyright (c) 2007 -(c)2008 Skylar Cantu * * This file is part of PocketTouch. * * PocketTouch/main.m is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 * as published by the Free Software Foundation. * * PocketTouch/main.m is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA */ /* PreferenceBundle install mathods borrowed from MobileScrobbler, * (c) 2007 Sam Steel */ #import <Foundation/Foundation.h> #import <UIKit/UIKit.h> #import "PTApp.h" void insertPrefBundle(NSString *settingsFile) { int i; NSMutableDictionary *settings = [NSMutableDictionary dictionaryWithContentsOfFile: settingsFile]; for(i = 0; i < [[settings objectForKey:@"items"] count]; i++) { NSDictionary *entry = [[settings objectForKey:@"items"] objectAtIndex: i]; if([[entry objectForKey:@"bundle"] isEqualToString:@"PocketTouchSettings"]){ return; } } [[settings objectForKey:@"items"] insertObject: [NSDictionary dictionaryWithObjectsAndKeys: @"PSLinkCell", @"cell", @"PocketTouchSettings", @"bundle", @"PocketTouch", @"label", [NSNumber numberWithInt:1], @"isController", [NSNumber numberWithInt:1], @"hasIcon", nil] atIndex: [[settings objectForKey:@"items"] count] - 1]; [settings writeToFile:settingsFile atomically:YES]; } void removePrefBundle(NSString *settingsFile) { int i; NSMutableDictionary *settings = [NSMutableDictionary dictionaryWithContentsOfFile:settingsFile]; for(i = 0; i < [[settings objectForKey:@"items"] count]; i++) { NSDictionary *entry = [[settings objectForKey:@"items"] objectAtIndex: i]; if([[entry objectForKey:@"bundle"] isEqualToString:@"PocketTouchSettings"]) { [[settings objectForKey:@"items"] removeObjectAtIndex: i]; } } [settings writeToFile:settingsFile atomically:YES]; } int main(int argc, char *argv[]) { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; if(argc > 1 && !strcmp(argv[1],"--installPrefBundle")) { insertPrefBundle(@"/Applications/Preferences.app/Settings-iPhone.plist"); insertPrefBundle(@"/Applications/Preferences.app/Settings-iPod.plist"); return 0; } else if(argc > 1 && !strcmp(argv[1],"--removePrefBundle")) { removePrefBundle(@"/Applications/Preferences.app/Settings-iPhone.plist"); removePrefBundle(@"/Applications/Preferences.app/Settings-iPod.plist"); return 0; } int ret = UIApplicationMain(argc, argv, @"PTApp", @"PTApp"); [pool release]; return ret; }
This is really all there is to know about Preference bundles. If you have any other questions, feel free to contact me via email, or by the user name SkylarEC at http://www.ipodtouchfans.com/
Thank you for reading this document. I hoped I was able to help you out.
Addendum I
PreferenceLoader
After I first wrote this document, Thomas Moore (volatile-dev) has created a MobileSubstrate based solution to adding your PreferencesBundle into Preferences called PreferenceLoader. His solution is very eloquent in that it requires no changes to be made within Preferences.app.
To use PreferenceLoader, you must create a new plist file called whatever. I’d recommend YourAppSettings.plist. This plist will be installed to /Library/PreferenceLoader/Preferences/. Your PrefrenceBundle will still be contained in /System/Library/PreferenceBundles/. This plist should contain all the PSSpecifier information that you would ordinarily insert into the Settings-iPhone and Settings-iPod plists. The only difference is that you would omit the “hasIcon” key and use the “icon” key in its place. Here, is a sample plist taken directly from Moore’s PreferenceLoader documentation:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>entry</key> <dict> <key>bundle</key> <string>TestSettings</string> <key>cell</key> <string>PSLinkCell</string> <key>icon</key> <string>TestIcon.png</string> <key>isController</key> <true/> <key>label</key> <string>Test</string> </dict> </dict> </plist>
Read more about PreferenceLoader at Moore’s site: volatile-dev.com
More comments at: ipodtouchfans.com
Addendum II
PreferencesSpecifierPlistFormat
In the below link, you will find a great writeup by kennytm about the plist that Preferences actually reads. It contains many definitions and clarifications; and is well worth bookmarking if you are making a PreferenceBundle.
http://code.google.com/p/networkpx/wiki/PreferencesSpecifierPlistFormat
Comments
3 Comments on Creating a PreferenceBundle for the iPhone.
-
sj on
Sat, 7th Nov 2009 10:21 pm
-
Syam on
Wed, 6th Oct 2010 6:57 pm
-
Curie Rathnayake on
Mon, 2nd May 2011 9:39 am
Hey Skylar, I was wondering if you could post a sample makefile for those (like me) who aren’t completely used to making them (mostly for iPhone jb apps etc), I am kind of accustomed to using IDE’s and XCode. I know, I really need to learn, I usually do that best by example. Any help would be much appreciated, thank you!
Hi Skylar, I have created a mobile substrate application. I need to hook the iPhone’s core prefernces and want to change the main settings from my app. For example, I need to enable/disable cellular data of iPhone from my application. Let me know is it possible? If so how can I do that? Looking forward for your reply.
Regards,
Syam
plz help me,,, to create a new language for the iPhone
Looking forward for your reply.
Regards,
Curie Rathnayake
(Sri Lanka)
Tell me what you're thinking...
and oh, if you want a pic to show with your comment, go get a gravatar!
