Jan 16

plists are wonderful for storing small amounts of semi-structured data when you don’t want the overhead of using a full-blown database. OS X, as you have no doubt noticed, uses plists extensively to store configuration data.

I like to use plists to create configuration driven applications. On the App Store you can find TRIBE and The Green Book. These two application look very different, but they use exactly the same code base. The difference is a couple of plist configuration files. Our client is very happy with this, because they are busy churning out many different titles for the App Store using this method.

Reading a plist file from the application bundle just requires a few lines of code, and some error handling. I like to place that code in a nice convenience method like this:
 

- (id)readPlist:(NSString *)fileName {
   NSData *plistData;
   NSString *error;
   NSPropertyListFormat format;
   id plist;

   NSString *localizedPath = [[NSBundle mainBundle] pathForResource:fileName ofType:@"plist"];
   plistData = [NSData dataWithContentsOfFile:localizedPath]; 

   plist = [NSPropertyListSerialization propertyListFromData:plistData mutabilityOption:NSPropertyListImmutable format:&format errorDescription:&error];
   if (!plist) {
      NSLog(@"Error reading plist from file '%s', error = '%s'", [localizedPath UTF8String], [error UTF8String]);
      [error release];
   }

   return plist;
}

I’m not too fond of using id as return values or parameters to methods. I prefer stronger type checks, so I typically wrap the readPlist method in two methods that return either an array or a dictionary.

- (NSArray *)getArray:(NSString *)fileName {
   return (NSArray *)[self readPlist:fileName];
} 

- (NSDictionary *)getDictionary:(NSString *)fileName {
   return (NSDictionary *)[self readPlist:fileName];
}

Writing to a plist file is not much more difficult:

- (void)writePlist:(id)plist fileName:(NSString *)fileName {
   NSData *xmlData;
   NSString *error; 

   NSString *localizedPath = [[NSBundle mainBundle] pathForResource:fileName ofType:@"plist"];
   xmlData = [NSPropertyListSerialization dataFromPropertyList:plist format:NSPropertyListXMLFormat_v1_0 errorDescription:&error];
   if (xmlData) {
      [xmlData writeToFile:localizedPath atomically:YES];
   } else {
      NSLog(@"Error writing plist to file '%s', error = '%s'", [localizedPath UTF8String], [error UTF8String]);
      [error release];
   }
}

Note that writing to a file inside the app bundle is not good if you want your data to stick around after the application is upgraded since the bundle will be overwritten. You can look at the Apple’s SQLite code examples for code snippets on how to copy files out of the bundle before using them.

written by Nick \\ tags: , ,

Apr 09

Say you want each customer to accept your Terms of Service, or some other legalese, before they can use your iPhone application.

Display the screen upon startup

The standard AppDelegate code generated by Xcode creates and displays your first application screen. Here’s a way that you can use the hidden property of UIView to display your new screen first without disrupting the flow of the rest of the application.

// Create window
self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];
[window setBackgroundColor:[UIColor whiteColor]];

// Set up main view navigation controller
MainMenuViewController *navController = [[MainMenuViewController alloc] init];

// Create a navigation controller using the new controller
navigationController = [[UINavigationController alloc] initWithRootViewController:navController];
navigationController.navigationBarStyle = UIBarStyleDefault;

[navController release];

// Create Terms of Service screen
tosController = [[TermsOfServiceViewController alloc] init];
[window addSubview:[tosController view]];
navigationController.view.hidden = YES;

// Add the navigation controller's view to the window
[window addSubview:[navigationController view]];
[window makeKeyAndVisible];

Most of this is boilerplate code, more or less generated by Xcode. What’s new starts at line 14. Instantiate the new TermsOfServiceViewController and add it to the view. Standard stuff. The important line is #17, which hides the navigationController.

Dismiss the new screen

Now that the Terms of Service screen is displayed and the Main Menu is hidden we need a way to dismiss the ToS screen and return to the normal application flow.

In your AppDelegate class create a new method:

- (void)termsOfServiceAccepted
{
	tosController.view.hidden = YES;
	navigationController.view.hidden = NO;
}

In the TermsOfServiceController you would have a button that the user has to tap:

UIButton *acceptButton = [[UIButton buttonWithType:UIButtonTypeRoundedRect] retain];
acceptButton.frame = CGRectMake(kLeftMargin,
						applicationFrame.size.height - kBottomMargin - kButtonHeight,
						applicationFrame.size.width - kLeftMargin - kRightMargin,
						kButtonHeight);
[acceptButton setTitle:NSLocalizedString(@"ButtonAcceptTermsOfService", @"") forStates:UIControlStateNormal];
[acceptButton addTarget:self action:@selector(termsOfServiceAccepted:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:acceptButton];

When the button is tapped, the selector is called which in turn calls the above method in the AppDelegate.

- (void)termsOfServiceAccepted:(id)sender
{
	id applicationDelegate = [[UIApplication sharedApplication] delegate];
	[applicationDelegate termsOfServiceAccepted];
}

Only show the screen once

With this code the ToS screen is displayed every time the application is started. That will quickly get annoying. So let’s add some code to only show it once by using a boolean stored in NSUserDefaults.

NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
if (![userDefaults boolForKey:TERMS_OF_USE_ACCEPTED]) {
	tosController = [[TermsOfServiceViewController alloc] init];
	[window addSubview:[tosController view]];
	navigationController.view.hidden = YES;
}

And in the termsOfServiceAccepted method set the boolean in NSUserDefaults.

- (void)termsOfServiceAccepted
{
	tosController.view.hidden = YES;
	navigationController.view.hidden = NO;

	// Store acceptance in UserDefaults
	NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
	[userDefaults setBool:YES forKey:TERMS_OF_USE_ACCEPTED];
}

Now the ToS screen is only shown until the user taps the accept button. Your customers will be happy, as will your lawyers.

written by Nick \\ tags: , ,

Apr 07

First you need to add your file to the Resources folder of your Xcode project. Then you can access the file like this (assuming the file is called MyFile.txt):

NSString *filePath = [[NSBundle mainBundle] pathForResource:@"MyFile" ofType:@"txt"];
NSData *myData = [NSData dataWithContentsOfFile:filePath];
if (myData) {
	// do something useful
}

Here’s a complete example reading a help text file into a UIWebView.

NSString *filePath = [[NSBundle mainBundle] pathForResource:@"HelpDoc" ofType:@"htm"];
NSData *htmlData = [NSData dataWithContentsOfFile:filePath];
if (htmlData) {
	[webView loadData:htmlData MIMEType:@"text/html" textEncodingName:@"UTF-8" baseURL:[NSURL URLWithString:@"http://iphoneincubator.com"]];
}

If you want to read the file into a string, which you can then display in a UITextView, for example, then do this:

NSString *filePath = [[NSBundle mainBundle] pathForResource:@"important" ofType:@"txt"];
if (filePath) {
	NSString *myText = [NSString stringWithContentsOfFile:filePath];
	if (myText) {
		textView.text= myText;
	}
}

written by Nick \\ tags: , , ,