Wednesday, May 13, 2009

iPhone tutorial: Storing and retrieving information using plists


Some applications need to store information in order to be really valuable for the user. Others do it just for fun; for example, storing the highscores in a game. Yet others do it to offer a multi-platform experience, by making it possible to access the same information from several different platforms (web, desktop, mobile), where the iPhone might be one of them. In the last case, the information should probably be stored on a server on the Internet to make it really useful.

There are several ways to store information persistently on the iPhone, but in this tutorial we're going to focus on "plists", or property lists as they really are called. If you already have done some iPhone development or if you've read the previous tutorials, you've already come in contact with property lists. That's because the Resources/Info.plist file present in all XCode template projects is a property list.

If you click on it in XCode, the contents of the file will be presented in a "plist-editor" view, which makes it easy both to browse the information in the plist as well as edit it. If you instead open the file in an editor which understans xml (for example Dashcode), you'll see that Info.plist is an xml file.

Ok, so a plist seems to be a standardised xml format for storing information on the iPhone. That's good, but what kind of format can be stored? Unless you have very specific needs, I would say almost anything. Plists basically contain a collection of key-value pairs, where the keys have to be unique and the values have to be objects.  That is, you cannot store primitive types like 'int' and 'long' without first wrapping them in an Objective C object. You cannot store any Objective C object, though, but you come a long way with the ones that are supported:

NSArray
NSDictionary
NSString
NSData
NSDate
NSNumber(intValue)
NSNumber(floatValue)
NSNumber(boolValue == YES or boolValue == NO)

We said that plists contain a collection of key-value pairs. Such collections are often implemented using data types such as hash tables, hash maps or dictionaries, which more or less are the "same thing", but can also be implemented using arrays. When an array is used, the array indices are the keys, while, in the dictionary case, the keys often are strings. In the list of supported objects above, you can see that NSDictionary and NSArray present. This is extra interesting since a plist itself often is implemented using a dictionary or array object, called the root object.

If you want to read more about property lists at this stage I recommend that you open the API docs browser in XCode and do a full text search for "property list". Then you should find a document called "Property List Programming Guide".

A simple plist application

Now let's get down to programming! Start a new "Window-Based" project in XCode and name it "plist1", by choosing "File/New project" from XCode's menu. We're not going to do anything "graphical" in the tutorial, but we'll use a window-based project anyway because it's an easy way to create a new project.

Resources/foo.plist

How to create a new plist-file in XCode isn't totally obvious, even though it is very easy once you find out how. CTRL-click on the "Resources"-folder/group in XCode and choose "Add/New file" from the pop-up menu. In the template window that appears, choose the "Other" section under the "Mac OS X" heading. There you will be able to select "Property list" - do that and name the file "foo". If you click on Resources/foo.list the plist-editor will appear and you'll see that the plist is empty, apart from the root object which we mentioned above. You'll also see that it is of type 'Dictionary'.

If you click on the small icon at the end of the "Root"-line (to the right) a new line will appear under the root object, indented one level to indicate that it is contained inside the root object. The name of the key is set to "New item", but change this to "key1". We're happy with the default type - String - since we're going to use a string for our first test. Set the value to the text "value1" though, by double-clicking in the value-column.

If you build the project (CMD-B) - an easy way to save all files! - and locate "foo.plist" on your hard disk using 'Finder' in Mac OS X, you'll see that it opens in 'Property List Editor' when you double-click it. That editor looks just like the one that's used in XCode if you click on the file there. If you instead open "foo.plist" with an xml-capable editor - like 'Dashcode' in Mac OS X -  you'll instead see that it contains xml (see the picture).

As you can see, the file actually contains xml. First comes some standard xml boilerplate stuff and after that, there is an opening "tag" called 'plist'. Inside that is the root-node which is of the type 'dict' (NSDictionary). Inside the root node is a 'key'-tag containing 'key1' and a 'string'-tag  containing 'value1'. What all this means is that this file contains a plist, which contains a dictionary, which contains the key-value pair "key1=value1" where the value is of the type 'string' (NSString). Pretty self-explanatory, huh?

Classes/Plist1AppDelegate.m

We're going to "hijack" the 'applicationDidFinishLaunching' method for our short test, so edit the method to make it look like below. The inlined comments tries to explain what's going on, so we won't analyse the code any further.

- (void)applicationDidFinishLaunching:(UIApplication *)application {    

    // Override point for customization after application launch
    [window makeKeyAndVisible];

// create a pointer to a dictionary
NSDictionary *dictionary;
// read "foo.plist" from application bundle
NSString *path = [[NSBundle mainBundle] bundlePath];
NSString *finalPath = [path stringByAppendingPathComponent:@"foo.plist"];
dictionary = [NSDictionary dictionaryWithContentsOfFile:finalPath];

// dump the contents of the dictionary to the console
for (id key in dictionary) {
NSLog(@"bundle: key=%@, value=%@", key, [dictionary objectForKey:key]);
}
}

Test run

Build and run in XCode (CMD-Return) and wait until the iPhone simulator appears. Once it does, click on the source code window in XCode and press CMD-R to bring the console window to the front. In it you should see something like this:

2009-05-14 07:30:07.113 Plist1[659:20b] bundle: key=key1, value=value1

Pretty cool! With just a few lines of code, we were able to read an xml file containing a dictionary and dump the contents of the dictionary to the console. Reading a file from the application bundle which we did here, is an ideal way of handling "static data" like a default configuration or why not the initial high score table of a game? Whenever the user wants to "reset to defaults", you just have to read the data from the bundle again.

Saving a plist to a file

Being able to save the contents of a plist to a file is of course very valuable and very useful. It allows you store "dynamic data" produced by your application to make it available to the user the next time the application is started. If it is the highscore table of a game you're saving, it's essential if the player wants to show the highscores to a friend. If not, the player would have to keep the application running until he or she meets the friend. That's far from practical, especially considering the rather short battery life of the iPhone ;)

Classes/Plist1AppDelegate.m

Replace the "create a pointer to a dictionary" code at the beginning of the 'applicationDidFinishLaunching' method we created above with the following:

// create a pointer to a mutable dictionary
NSMutableDictionary *dictionary;

We're changing the class of the dictionary from NSDictionary to NSMutableDictionary in order to be able to make changes to the dictionary. (Mutable is a fancy way of saying that something can change, or "mutate".)

Then add the following to the end of the 'applicationDidFinishLaunching':

// create a NSNumber object containing the
// integer value 2 and add it as 'key2' to the dictionary.
NSNumber *number = [NSNumber numberWithInt:2];
[dictionary setObject:number forKey:@"key2"];
// dump the contents of the dictionary to the console
for (id key in dictionary) {
NSLog(@"memory: key=%@, value=%@", key, [dictionary objectForKey:key]);
}

// write xml representation of dictionary to a file
[dictionary writeToFile:@"/Users/henrik/Sites/foo.plist" atomically:NO];

What we do here is add a key-value pair to the dictionary (this is possible since we now have a NSMutableDictionary). Both the key and the value has to be objects, so we create a NSNumber object containing the integer 2 and associate it with a NSString object containing the text "key2". To verify that the new key-value pair was added, we once again dump the contents of the dictionary to the console. After that comes the exciting stuff. With a single method call 'writeToFile' on our 'dictionary' object, the contents of the plist is written in xml format to a file of our choice. In this case we chose to save it in a directory called 'Sites' in my home directory. You should replace 'henrik' with your Mac OS X username to make it work on your computer.

Test run

Build and run in XCode (CMD-Return), wait for the iPhone simulator to start and then go back to XCode and press CMD-R to bring the console to the front. There you should see the following:

2009-05-14 17:29:09.064 Plist1[863:20b] bundle: key=key1, value=value1
2009-05-14 17:29:09.075 Plist1[863:20b] memory: key=key1, value=value1
2009-05-14 17:29:09.075 Plist1[863:20b] memory: key=key2, value=2

The first line contains the same information as in the previous test run. The second and third lines dump the contents of our changed (mutated) dictionary and thus we see that the key-value pair we added also shows up (key2). The first line is prefixed with "bundle:" to indicate that it shows the contents of the plist read from the bundle, while the other lines are prefixed with "memory:" to indicate that they show the contents of the plist in the iPhones memory - the one we mutated...

Let's take a look the contents of the file that was written to the "Sites"-directory in our Mac OS X home directory. Locate "foo.plist" in "Finder" and double-click on it to load it into the "Property List Editor" and you'll see that it now also contains the key-value pair we added programmatically in the source code above. You can also open it in an xml capable editor like "Dashcode" to verify that the key-value pair is xml encoded in a similar way as the key-value pair we added from the plist editor in XCode.

Reading a plist from a web server

Being able to read information from a web server in a simple way is a really powerful feature. Thankfully, this feature is available on the iPhone and extremely simple to use if the information you want to read can be contained in a plist.

Configuring the web server in Mac OS X

In order to test this, we need access to a web server. This is not a problem since Mac OS X comes pre-packaged with the Apache web server. All you have to do is enable it. You do this by going to the "System settings" in Mac OS X (the "gears" icon in the dock), select "Sharing" and enable "Web sharing". Please excuse me for a bit sketchy here, but I'm using a Swedish version of Mac OS X and therefore don't know the exact names of these settings in the English version. It's pretty simple though.

When you're on the "web sharing" page in the system settings, you might notice that there also is an edit box where you can see/edit the network name of your computer. Under this edit box there also is a small text which says (freely translated): "Other computers on the local network can access your computer as Name.local", where "Name" is the name of your computer. My computer is called "MacBook", so I'll refer to that from now on.

To test that your web server works, open up a web browser and type the following:

  http://MacBook.local/~henrik

Once again, remember to replace "MacBook" with the name of your computer and replace "henrik" with the username you use in Mac OS X. If it works you should see a small text titled "Your web site" (freely translated). Now test the following URL instead:

  http://MacBook.local/~henrik/foo.plist

Hey, that's the xml representation of our plist! The one we saved to the "/Users/henrik/Sites/foo.plist" path on our hard disk above. How did it end up in our web browser? It turns out that the "Sites"-directory in your home directory contains the files for your local web site.

Now that we have set up and tested our local web site it becomes really easy to experiment with "web enabled" iPhone applications. Just place the files you would read from the web in your Sites-directory.

Classes/Plist1AppDelegate.m

Reading a plist from a web server URL is really simple. Just add the following to the end of 'applicationDidFinishLaunching'

dictionary = [NSDictionary dictionaryWithContentsOfURL:[NSURL URLWithString:@"http://localhost/~henrik/foo.xml"]];
for (id key in dictionary) {
NSLog(@"web key=%@, value=%@", key, [dictionary objectForKey:key]);
}

Test run

Build and run in XCode (CMD-Return) and you should see something like this in the console window:

2009-05-15 07:40:40.691 Settings1[426:20b] bundle: key=key1, value=value1
2009-05-15 07:40:40.692 Settings1[426:20b] memory: key=key1, value=value1
2009-05-15 07:40:40.694 Settings1[426:20b] memory: key=key2, value=2
2009-05-15 07:40:40.719 Settings1[426:20b] url: key=key1, value=value1
2009-05-15 07:40:40.719 Settings1[426:20b] url: key=key2, value=2

It's the last two lines - the ones prefixed with "url:" that are new compared to the last test run. Those lines come from the dump of the dictionary which we read from our web server url, and as you can see they are identicaly to the "memory:"-lines. That's because we wrote the memory-dictionary to a file in the "Sites"-directory of our home directory in Mac OS X and then read it back via the web server in Mac OS X.

Summary

Being able to access information stored in powerful data structures (hash tables, arrays) as easy is this is indeed a blessing, since creating proprietary file formats and then writing parsers for them, etc. can be quite time consuming. That it's equally easy to access information stored on a web server is fantastic since you can "Internet enable" your application with just a few lines of code. The only restriction with the methods presented in this tutorial is that the information as to be stored in the plist xml format , so you can't access any type of information this easily - just the information you have control over yourself.


18 comments:

  1. Its great to know about the more functionality about the Iphone. Thanks for sharing.
    Web Design Quote

    ReplyDelete
  2. hi,
    i realize this is a really elementary question, but i've not really found an answer that is explained in the simple terms that fit my level of experience. in other words everything i've found is more complicated than what i'm trying to understand. here's my question: i'd like to store the user's 'login' and 'password' in a plist. i'd like to check to see if this plist is populated, and if so, skip the 'login' screen. how do i save the user's input from a text field to a plist file (the first time they run the app), and then how do i skip the login screen if they have already filled it out?

    any help, pointers,urls, etc will be truly appreciated. thanks.

    ReplyDelete
  3. Great work!!

    For petertripp67;
    To save to a plist:

    NSString *password= textFieldName.text;
    [dictionary setObject:password forKey:@"key2"];

    if([dictionary objectForKey:@"key2"])
    //its populated
    else
    //its not populated

    ReplyDelete
  4. When I try to run this, I encounter the following error at this line of code:

    *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '*** -[NSCFDictionary setObject:forKey:]: mutating method sent to immutable object'

    NSNumber *number = [NSNumber numberWithInt:2];
    [dictionary setObject:number forKey:@"key2"];

    Any clues??

    ReplyDelete
  5. @Sandeep: My guess is that you forgot to change the dictionary to a mutable variant. Search for the word 'mutable' in post and you will find out how to accomplish this. Let me know if it didn't work.

    ReplyDelete
  6. @henrikb: Thanks for sharing such a nice piece of information..

    ReplyDelete
  7. i want to store Tennis Scores, can i create an XML file and retrieve the data properly in a plist file. eg

    'PlayerA'
    'Name'Venus Williams'/Name'
    'Sets' 2 '/Sets'
    'Score'
    'Set1' 6-4 '/Set1'
    'Set2' 6-3 '/Set3'
    '/Score'
    '/PlayerA'


    'PlayerB' etc ......"

    Regards Paul Cutler

    P.S. great tutorial

    ReplyDelete
  8. even with using NSMutableDictionary:
    *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '*** -[NSCFDictionary setObject:forKey:]: mutating method sent to immutable object'

    is still happening. is this an api issue with version 3.2.1 of XCode?

    ReplyDelete
  9. no you just need to change the [NSDictionary dictionaryWithContentsOfFile:filePath] to [NSMutableDictionary dictionaryWithContentsOfFile:filePath]

    ReplyDelete
  10. Thanks for all the comments and corrections. Please note that I just published a follow-up post on this post here:

    http://humblecoder.blogspot.com/2010/03/revisited-storing-and-retrieving.html

    The main reason for posting this new post is to describe how to "properly" store plists in the Documents directory. Hope it will be of some use.

    ReplyDelete
  11. Nice post. I have been searching for articles about valves and actuators and your post really helps. Thanks a lot for posting this. iphone

    ReplyDelete
  12. so can you use this method to read sms on the web?

    ReplyDelete
  13. ...or modify the sms plist to archive the sms when it is deleted so you can retrieve it when you backup?

    ReplyDelete
  14. Hello,

    You have a published a very Nice Article.....

    But i have 1 Question After adding 2 nd record inside the foo.plist why i am not able to see that record inside the foo.plist when i check that inside Resources Folder.

    Please Reply ASAP.


    Thanks

    ReplyDelete
  15. @brad i missed the second NSDictionary, thanks to you it worked like a charm. thanks a million.

    ReplyDelete
  16. heyy…can u please helpin me completing ma assignment???its about creating a plist and then reading and writin data in it nad displayin the data present in the plist…please m in need of really quick help..:(..ma project contain an add button pressin it will open a form nd its entries lyk name class stream and ol will get saved in plist aftr clcikin add person button…there will b anothr button colled display..it will display all the names which we hav added in plist..plzz reply asap..:(plzz upload it tutorail plzz ny1??.:(ma techr is nt movin 1 step fwd since last 2 weeks cuz of tis assignmnt..n m cmpletly new to xcode..:/

    ReplyDelete
  17. I tried to add value = 3 for key3. But this replaces my value = 2 n i get output on console as --

    2012-03-27 13:14:05.051 plist[2282:207] Memory : Key = key3, Value= 3
    2012-03-27 13:14:09.003 plist[2282:207] Memory : Key = Key 1, Value= val1

    Is there any way through which I can also maintain previous record and add new one?

    ReplyDelete