Ad

Our DNA is written in Swift
Jump

GeoCorder Breaks Records

After the first full day of sales I am really happy with how GeoCorder is doing. I did not expect such a huge number of downloads. But hey, it’s useful and it’s free… for a limited time.

USA leads the way with 240 downloads, Germany with 43 is not-so-close second and Canada on third place is barely more than the average with 29. In total the top three countries amount to more than half of downloads, 495 in total.

Actually those numbers might give a very good indication about what international distribution of downloads to expect and which languages to target. With English you will cover more than two thirds of market volume and German approx. 10%.

GeoCorder is useful to anybody in any language and therefore I think download number are not skewed by people not downloading because they don’t understand the app’s core content. GeoCorder has so little text that your language does not matter.

I will raise the price to $2 once I reach 1000 downloads. While this will definitely reduce downloads dramatically I hope to turn a couple of bucks for the effort I put into this app.

UPDATE: I dropped the price again to $1 because daily downloads dropped from over 200 to zero.

New App available: GeoCorder

Our latest addition to the app store is GeoCorder, currently free. With it you can simply record GPS tracks in full detail and later e-mail them to contacts in your address book.

Actually I just put it online to get some report data about free apps, so that’s your advantage. You can get it now for free. Later I’ll probably raise the price to 1 Dollar, because I believe that if people like the concept they will also buy it for that amount and the earnings can then go towards further development of the app.

GeoCorder had been rejected by Apple several times. First due to my abusing a standard button for something completely different. Later I had a nasty crashing bug that you would only see if you tried to e-mail a track without network connectivity. Apple thought that this might “confuse users”. And frankly so did I after being able to duplicate the problem on iPhone Simulator.

I persisted, improving my code several times and resubmitting as often. Finally, today, I got the infamous “Your application is Ready for Sale” e-mail. Hooray!

More info here.

String to Array of Characters

Michael asks:

“How do I split the characters in an NSString into an NSArray? In BASIC I could split a string with empty character, but in Object C this does not work”

One way to do it is to just get one character substrings:

NSString *s = @"Hello World";
int i;
NSMutableArray *m = [[NSMutableArray alloc] init];
 
for (i=0;i< [s length]; i++)
{
	[m addObject:[s substringWithRange:NSMakeRange(i, 1)]];
}
 
NSLog([m description]);  // most NS* objects have a useful description
 
[m release];  // don't forget

In Objective C every time you need a starting point and then a length from there it’s call an NSRange. Conveniently there is also a *Make macro for most structures like this. So in this case you just NSMakeRange(position,length) and pass this to a substringWithRange.

Defaulting Can Be Good

Coming from the Windows world I was used to storing program settings into this beast that is known as “The Registry”. So when I came to iPhone I had no clue where to put those settings that you want to keep between program launches. My first instinct was to put them into an NSDictionary and save this to disk.

That’s how I did it for half a dozen programs only to realize today that there would have been an easier method. The magic class to do it all with is called NSUserDefaults. All you need to do is instantiate it and then read and write values for names of your choosing, just like you would interacting with a dictionary.

//Instantiate NSUsersDefaults:
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
 
// to set a default value
[defaults setObject:@"oliver" forKey:@"Username"];
 
// to read a default value
NSString * myName=[[NSUserDefaults standardUserDefaults] stringForKey:@"Username"];

In reality you are interacting with an in-memory copy of your defaults. The documentation mentions that the system will synchronize it frequently with the persistent storage on disk. If you really want to make sure it gets written, like when your program is exiting, then you can call [defaults synchronize]. But that should not be necessary. In my tests that defaults where also successfully persisted if I changed them as late as in ApplicationWillTerminate.

Also great to know is that you are not limited to just keeping string values in your defaults. Any object that is legal for property lists can also be used in the defaults. These are: NSData, NSString, NSNumber, NSDate, NSArray or NSDictionary.

// some demo objects to save
NSNumber *number = [NSNumber numberWithInt:3];
NSDate *now = [NSDate date]; // now
NSArray *array = [NSArray arrayWithObjects:@"First", @"Second", @"Third", nil];
NSDictionary *dictionary = [NSDictionary dictionaryWithObject:@"Text" forKey:@"Key"];
 
// save them all the same way
[defaults setObject:number forKey:@"Number"];
[defaults setObject:now forKey:@"Now"];
[defaults setObject:array forKey:@"Array"];
[defaults setObject:dictionary forKey:@"Dictionary"];
 
// retrieve them again
int n = [[NSUserDefaults standardUserDefaults] integerForKey:@"Number"];
now = (NSDate *)[[NSUserDefaults standardUserDefaults] objectForKey:@"Now"];
array = [[NSUserDefaults standardUserDefaults] arrayForKey:@"Array"];
dictionary = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"Dictionary"];

Note how you can directly retrieve the integer value with the convenience method integerForKey. To get an NSDate you need to use objectForKey, there is no dateForKey as one might assume.

Finally there is one more convenient thing. If the defaults get changed, a notification gets sent to which you can subscribe in multiple classes. This notification gets sent out for every single change, so if you change 4 values in a row, the notification will be sent 4 times.

Subscribe to the notification:

[[NSNotificationCenter defaultCenter] addObserver:selfselector:@selector(settingsChangedNotification:) name:@"NSUserDefaultsDidChangeNotification" object:nil];

And have a function ready to start working once the notification comes:

- (void)settingsChangedNotification:(NSNotification *) notification
{
	NSLog(@"Settings have changed!");
}

There you have it. For any kind of settings you want to keep you can use the methods explained above and save yourself much work. For data that you need to keep secure (e.g. passwords) you will be better advised to use the keychain which I will explain in a future article.

Travelling in Time

You might be aware that the earth is in fact not flat but a globe with lots of time zones spread more or less evenly around its cirumference. There was a time when programmers had to do most of the heavy lifting of times and dates themselves, but luckily Cocoa has some nice classes to take care of the calculations for you.

Here is an example how I would “move” my birth date to the USA. What’s great about it is that it also seems to takes care of daylight savings time. In Austria and Germany this was only introduced 6 years after my birthday, so July 24th in 1974 has timezone GMT+1 whereas July 24th 1984 ends up with GMT+2.

// set up the components to make up my birthday
NSDateComponents *components = [[NSDateComponents alloc] init];
[components setMonth:7];
[components setYear:1974];
[components setDay:24];
[components setHour:12];
[components setMinute:7];
 
// create a calendar
NSCalendar *gregorian = [[NSCalendar alloc]  initWithCalendarIdentifier:NSGregorianCalendar];
 
// default is to use the iPhone's TZ, let's change it to USA
[gregorian setTimeZone:[NSTimeZone timeZoneWithName:@"America/New_York"]];
 
// now we piece it together
NSDate *date74 = [gregorian dateFromComponents:components];
NSLog([date74 description]);  // 1974-07-24 17:07:00 +0100
 
// show the difference the year makes
[components setYear:1984];
NSDate *date84 = [gregorian dateFromComponents:components];
NSLog([date84 description]); // 1984-07-24 18:07:00 +0200
 
// clean up
[components release];
[gregorian release];

Now you might wonder why the output from the NSLog is +0100 and +0200 and not the US timezone. The reason is that Cocoa internally will always automatically convert dates to your current system timezone.

Try it out for yourself? Create a date instance like I have shown above, log it, then change the default timezone for your program and log it again. Even though you did not modify the date instance, you will get a different output.

// components already set up as before
NSDate *date84 = [gregorian dateFromComponents:components];
NSLog([date84 description]); // 1984-07-24 18:07:00 +0200
 
[NSTimeZone setDefaultTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"GMT"]];
NSLog([date84 description]); // 1984-07-24 16:07:00 +0000

So there is lots of date magic (aka “time travel”) available in Cocoa. All you have to get used to is having to write lots of code but you trade this for lots of functionality.

Don't abuse standard buttons

Four days ago I had submitted a small app to the app store and yesterday I received feedback that I had to make some changes. Out of pure laziness I had re-purposed the “compose” button on a tool bar to call up a settings dialog.

“Applications must adhere to the iPhone Human Interface Guidelines as outlined in iPhone SDK Agreement section 3.3.5.

The compose button is to be used to open a new message view in edit mode. Implementing standard buttons to perform other tasks will lead to user confusion. We recommend using a custom icon.”

I must admit that Apple is right, we want to avoid user confusion at any cost. Also kudos to them for really being strict even if this means for us small-time developers that you have to submit your app with changes several times until they give it the nod.

I ended up creating my own button with a custom image of a cockwheel and while being at it I also replaced the play button with a record button, again by my own design.

Mysterious PLIST

I have grown very fond of using NSDictionary and NSMutableDictionary if I want to put diverse data into a single object to pass around in my programs. Also NSDictionary has the built-in capability of saving and reading itself to disk, in the so-called property list (abbreviated as “plist”) format.

Now I was implementing caching of such a NSDictionary in my app and for mysterious reasons the following code would not result in any file being saved:

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *path = [documentsDirectory stringByAppendingPathComponent:@"notworking.plist"];
 
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
NSNumber *numKey = [NSNumber numberWithInt:123];
[dict setObject:@"a Value" forKey:numKey];    // legal: using a NSNumber as key
 
[dict writeToFile:path atomically:YES];
[dict release];

Can you spot the problem? Probably not, if you check the documentation you will read that NSDictionary can hold and save any of the following data types: NSNumber, NSData, NSString, NSDictionary, NSArray. I did not use anything else, therefore the dictionary should be saved as a plist. With lots of experimenting however I found a simple change to the above code that makes it work:

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *path = [documentsDirectory stringByAppendingPathComponent:@"working.plist"];
 
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
NSString *stringKey = @"123";
[dict setObject:@"a Value" forKey:stringKey];    // workaround
 
[dict writeToFile:path atomically:YES];
[dict release];

Simply put you cannot use anything except as a string as the root key of an NSDictionary. The above code will save an XML file looking like this:

<?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>123</key>
        <string>a Value</string>
</dict>
</plist>

If you want to save a dictionary, make sure you use only strings for keys. In another case I was using an NSDate as valid key for an entry in a dictionary. Same result: writing it to a file does not work. Why there is such a limitation I cannot say. In any case I submitted it as bug report to Apple.

Having pondered this a little I realized the most probable explanation. If you look at the way an XML plist is represented in the file you can see that there is no indication as to what data type the <key> represents. Therefore it can only be a string.

Showing Version Info

I found that I need some place to show a version in my apps, because otherwise I and my testers never know if we are talking about the same version. A problem they might be having could have already been fixed previously. Without a version info you are at a loss.

Here’s a version info box, that I came up with. This shows how to access the version string from Info.plist as well as open a new mail to the author.

- (IBAction) showAppInfo:(id)sender
{
	// a convenient method to get to the Info.plist in the app bundle
	NSDictionary *info = [[NSBundle mainBundle] infoDictionary];
 
	// get two items from the dictionary
	NSString *version = [info objectForKey:@"CFBundleVersion"];
	NSString *title = [info objectForKey:@"CFBundleDisplayName"];
 
	UIAlertView *alert = [[UIAlertView alloc] initWithTitle:[@"About " stringByAppendingString:title]
						  message:[NSString stringWithFormat:@"Version %@\n\n© 2009 Drobnik.com", version]
						  delegate:self
						  cancelButtonTitle:@"Dismiss"
						  otherButtonTitles:@"Contact", nil];
	[alert show];
	[alert release];
}
// this function from UIAlertViewDelegate is called when a button is pushed
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
	UIApplication *myApp = [UIApplication sharedApplication];
 
	// cancel button has index 0
	switch (buttonIndex) {
		case 1:
		{
			[myApp openURL:[NSURL URLWithString:@"mailto:oliver@drobnik.com"]];
			break;
		}
		default:
			break;
	}
 
}

Two buttons are displayed side by side. Three or more are vertically stacked. This is how the result from the code about looks like:

All Purpose About Box

Call for Beta Testers – Mobile App Sales Report App

If you have a couple of apps where you get sales reports, haven’t you had the same problem as me, that daily reports are only available for 7 days and it’s a big hassle to keep downloading all of them. And even if you do, you don’t have any overview or analysis or charts…

ASiST First screenshot

Over the past two months I have been developing an App that runs on your iPhone and automatically downloads all new daily and weekly reports once you start the app. The reports are nicely shown with country flags and converted to your favorite currency. The report data is inserted into a local sqlite database which you can also transfer away from the iPhone via the built in web server. So the data is safe even long after Apple removed the report from the iTunes Connect site. There are also charts (1 so far, but more to come) to help you optically analyze sales trends and see how your apps are doing. The iTunes Connect logon is kept on the iphones secure keychain for safekeeping.

I am using it myself and every day I am anxious to see the previous day’s sales for my two apps and what the trend is.

I am looking for about a dozen people who would want to try out the app in different countries. Their benefit will be that they will get to keep the app for free and use forever.

A little more polishing and then I am going to find out wether Apple will permit such an app. Probably not, but if they do then it will be highly useful for everybody who is making the least bit of money in the store.

Future additions planned:

  • automatic downloading of customer feedback from iTunes
  • also downloading of financial reports
  • financial predictions of the current month’s approximate income
  • track when regions that are still below $250 will finally have earned enough to be cashed out
  • suggestions welcome

 To sign up for the BETA drop me an e-mail.

Remove Whitespace from NSString

In Cocoa Touch you always need to write more to achieve the same …

NSString *string = @" spaces in front and at the end ";
NSString *trimmedString = [string stringByTrimmingCharactersInSet:
                                  [NSCharacterSet whitespaceAndNewlineCharacterSet]];
NSLog(trimmedString)

While such a standard task might look excessively much code to write, you gain many additional possibilites of what you could want to trim away. NSCharacterSet also knows these other sets. 

  • alphanumericCharacterSet
  • capitalizedLetterCharacterSet
  • controlCharacterSet
  • decimalDigitCharacterSet
  • decomposableCharacterSet
  • illegalCharacterSet
  • letterCharacterSet
  • lowercaseLetterCharacterSet
  • newlineCharacterSet
  • nonBaseCharacterSet
  • punctuationCharacterSet
  • symbolCharacterSet
  • uppercaseLetterCharacterSet
  • whitespaceAndNewlineCharacterSet
  • whitespaceCharacterSet