Adam Howitt's Blog

Sep 19
2009

Working with Dates and Date offsets in iPhone Development

It's been a slow iPhone development week partly because I'm in the peak week of my marathon training but the main reason is that the work I did was pretty frustrating so it put me off doing more until today.

When is a date not a date?

When it's a datetime, silly. Working with dates in iPhone means you work with the misleading named class NSDate which not only stores a date and a time but the timezone data too. That little caveat aside I can dive into my issue dujour.

I'm building a UITableView where the groups are the week numbers and the cells are the days within those weeks calculated from a start date for the whole table. So if the start date is Monday September 21st 2009, the first section header is "week 1" and the first cell is Monday September 21st 2009.

The week section is easy to calculate:

NSUInteger section = [indexPath section];
NSUInteger row = [indexPath row];
NSUInteger currentWeek = section+1;

Next up is the calculation for the first cell. It should be easy right? The first cell of week 1 represents 0 weeks after the start date for the tableview and 0 days after the start, so our formula for dayOffset:

NSUInteger dayOffset = (section-1)*7+rows;

So day 1 of week 1 has a dayOffset of 0, day 1 of week 2 has a dayOffset of 7 etc. All that remains is to add the dayOffset to my startDate and I've got the date for the cell.

The challenge isn't finishing - starting is the hardest part

Working with an NSDate object my first instinct was to look at the NSDate class and find a function to add a set number of days to a date and found that there is only the function addTimeInterval:
- (id)addTimeInterval:(NSTimeInterval)seconds;
That's seconds for those of you who missed the nuance. So it appears I have to turn my dayOffsets into seconds before I can add them to a start date. Fine.
NSUInteger secondsOffset = dayOffset*24*60*60;

First a quick side-bar. As I mentioned earlier, I'm passing in a date, not a date time so I set my date formatter object to parse MM/dd/yy dates (where MM is 2 digit month vs mm which is 2 digit minutes).

NSDateFormatter *df = [[NSDateFormatter alloc] init];
[df setDateFormat:@"MM/dd/yy"];
NSDate *frDate = [df dateFromString:[tableSourceFile objectForKey:@"startDate"]];
NSLog(@"%@",[frDate description]);
The NSLog confirms that my date "09/21/09" has now become "2009-09-21 00:00:00 -0500". This seems like a reasonable summary, I guess :-)

I added my dayOffset to my date:

NSDate *cellDate = [frDate addTimeInterval:secondsOffset];
and then as I drag down through my UITableView I see it all unfold perfectly. Wait. Everything is fine but on Sunday November 1st 2009 instead of ticking over in Week 7 Day 1 to Monday November 2nd 2009 I get Sunday November 1st 2009 again. WTF?

I'm a little embarrassed at the hour and a half of bashing my head against this, double checking my start date, my logic, my days to seconds calculations, NSLogging out every variable involved to the log. It seems the first November 1st is "2009-11-01 00:00:00 -0500" and the second is "2009-11-01 23:00:00 -0600". A timezone switch? I plead late night development insanity on this but eagle-eyed wide-awake super-designer Jeff spots that it's not a change in timezone but the change in daylight savings time.

So turning days into seconds becomes nuts if you cross DST boundaries. To workaround this I have the bright idea to pass in noon as part of my start date because if the date shifts by 1 hour either way it still remains the same day:

NSDateFormatter *df = [[NSDateFormatter alloc] init];
//Change dateformatter to look for time
[df setDateFormat:@"MM/dd/yy HH:mm"];
NSDate *frDate = [df dateFromString:[tableSourceFile objectForKey:@"startDate"]];
//Correctly writes out "2009-09-21 12:00:00 -0500"
NSLog(@"%@",[frDate description]);
//Now do the addition:
NSDate *cellDate = [frDate addTimeInterval:secondsOffset];

I've created... a monster!

It works! By setting the time as noon I've got it working but I have a nagging feeling I'm missing something obviously simpler. It's close to midnight so I wrap up and leave for the night kicking myself for missing the obvious.

The next morning I pull up the docs and start digging around again looking in forums for date manipulations. I'm not sure of the exact path to enlightenment but I find the "Date and Time Programming Guide for Cocoa" and, more importantly, the section "Calendrical Calculations".

It *appears* that by using NSDateComponents to build an offset and NSCalendar to specify a basis for the calculations I can add the offset in days to a date based on the gregorian calendar. Since I'm adding days not seconds I wonder if this will avoid the issue I jammed thru.

Pressure creates diamonds

I didn't have time until this morning to test my theory so I sat down and plugged at it earlier.
//Create my dateformatter object
NSDateFormatter *df = [[NSDateFormatter alloc] init];
//Change dateformatter back to date only
[df setDateFormat:@"MM/dd/yy"];
NSDate *frDate = [df dateFromString:[tableSourceFile objectForKey:@"startDate"]];
//NSLog confirms my date"2009-09-21 00:00:00 -0500"
NSLog(@"%@",[frDate description]);
//Create an offset object
NSDateComponents *offsetComponents = [[NSDateComponents alloc] init];
//Pass in our offset in days
[offsetComponents setDay:(section*7)+row];
//Select a calendar for the calculations
NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
//With the gregorian calendar add my offset object to the start date
NSDate *cellDate = [gregorian dateByAddingComponents:offsetComponents toDate:frDate options:0];
//Release the offset and the calendar
[offsetComponents release];
[gregorian release];
Week 1 Day looks fine and dandy "2009-09-21 00:00:00 -0500", what about the mysterious DST cutover on Week 6 day 7? "2009-11-01 00:00:00 -0500" and then Week 7 Day 1: "2009-11-02 00:00:00 -0600". It behaves the way I expected so when I output my date to the UITableViewCell textLabel I get the day and can ignore the time:
//Display 3 character day of week, 2 digit day and 3 character month e.g. Mon, 21 Sep
[df setDateFormat:@"EEE, dd MMM"];
cell.textLabel.text = [edf stringFromDate:today];
Voila. 3 hours of development time to add a number of days to a date because my first instinct was such a wild goose chase. I've emailed the iPhone docs team to let them know that it would be great to add the link to the section on "Calendar Calculations" to the addTimeInterval method detail since, IMHO, it's the first place a new iPhone/cocoa developer would think to look.

I hope this helps someone else dodge the bullet and let me know if there are typos/errors/omissions in the code since typing in anger like this rarely reproduces perfect code! Have a great weekend.

Sep 08
2009

Testing iPhone StoreKit purchases

I spent a significant amount of time troubleshooting the iPhone StoreKit framework for our WalkJogRun iPhone app this weekend. I finally discovered that the documentation was slightly misleading after discovering a post on the Apple developer forums.

I had followed the StoreKit developer guide for testing which instructs you to 1. Create an account in iTunes Connect 2. Go to the Store Settings app and sign out 3. Sign in as your test account 4. Go to your app and test the purchase process

The problem comes at step 3 when you sign in. You are typically (but not consistently) told that the login has never been used in the App Store before so you'll need to review your details, which begins an account mini-interview to pick a country and enter credit card details. If you go anywhere near this process, you'll not be able to use the account when you go to your app and finally login with a series of different alerts, mainly "Your Password has Changed" or confirm your billing info.

The solution, I discovered from an Apple forum post by "Kuga", is NOT to sign in once you logout of your real account - instead just go straight to your app after you sign out in the Store Settings and use the in-app login.

Note that if you ran into the "your password has changed" error you'll find that it's damn near impossible (I couldn't work it out) how to turn an account you completed an interview for back into an account you can use for testing.

Country Specific Testing Notes

Instead of hard coding pricing into your app, if you've followed the store kit programming guide you'll pull the pricing and descriptions from iTunes Connect but initially pricing is shown in the country you were using before you logged out in the store settings.

Ben Gottlieb, author of the awesome Crosswords and Satchel iphone apps, gave me a great tip at the last NSCoder night here in Chicago for in app purchase accounts - if you use gmail you can create "throwaway aliases" to use when creating users, since each email account you use must be a unique email address that has never been used for testing and not an existing apple id. So, for example, if your email address was aardvark@gmail.com you can create fake aliases with the "+ syntax" so I would create a test user "aardvark+itcUS@gmail.com" for a US iTunes Connect test user account. The +itcUS gets ignored by Google and the email related to that account still goes into gmail for the aardvark@gmail.com account. It saves you creating a ton of email accounts for testing.

If you've created a test user for the same country as your regular account you'll have no trouble finalizing your purchase. If you've chosen a store in another country for your test user you'll be told initially that your account is only valid for that country store and you're unable to finish your purchase. This is a good thing!

Close your application and go to the store settings and you should see that you are actually logged in with that new test account for a different country. Close the store settings and reopen your app. At the point where pricing info is shown you should now see it localized to the test user country and you can complete the purchase in that language. Just be aware that the dialogs will all be in your target language so before you test the Japan store you might want to memorize the order of the buttons :-)

Verifying Receipts

I threw a simple alert in my "provide content" function to give a visual confirmation initially:

- (void) provideContent:(NSString *)productId {
    /* This is where I download the product based on the product Id to store on their phone */
    UIAlertView *av = [[UIAlertView alloc] initWithTitle:@"Purchase complete" message:@"Thanks for buying, sucka" delegate:self cancelButtonTitle:@"Done" otherButtonTitles:nil];
    [av show];
    [av release];
}

This is where I'll pull down the actual content if my purchase was successful. Since you should have your server validate receipts before delivering content you need to send the encoded receipt data over http. This piece isn't well documented but a great post on StackOverflow from JDAndrea provides the code necessary to pass the signed receipt by URL to your own server, in that case a PHP box. Since I'm a ColdFusion developer I wrote the following snippet to handle the work:


<cfset stcReceiptSend = structNew() />
<cfset stcReceiptSend["receipt-data"] = url.receipt />
<cfset receipt = serializeJSON(stcReceiptSend)>
<cfhttp method="POST" url="https://sandbox.itunes.apple.com/verifyReceipt">
    <cfhttpparam value="#receipt#" type="XML" />
</cfhttp>
<cftry>
<cfset result = deserializeJSON(cfhttp.filecontent)>
<cfmail from="mytestemail@example.com" to="mytestemail@example.com" subject="iPhone in-app Receipt Verification" type="html">
<cfdump var="#result#">
</cfmail>
<cfif result.status eq "0">
<cfcontent reset="yes"><cfoutput>0</cfoutput>
<cfelse>
<cfcontent reset="yes"><cfoutput>1</cfoutput>
</cfif>
<cfcatch>
<cfcontent reset="yes"><cfoutput>1</cfoutput>
</cfcatch>
</cftry>
It's very simple and just turns the encoded receipt data from your app into a serialized JSON packet which is posted to the itunes sandbox. Note for production releases you'll need to change that url to be the real itunes URL. If the result is a 0 you have a valid itunes transaction and I send an email dumping out the transaction keys to my email for reference before returning a 0 (success) to the code in my iPhone app. Anything other than a 0 in the result.status means something isn't right and you should pass back something other than a 0 to the iPhone app so you don't deliver the content they "unlocked".

That's it for my StoreKit testing summary - hope it helps remove some obstacles I encountered along the way. If you're looking for tips on this stuff I strongly recommend three sites which have proven critical to my forward motion: iPhoneSDK, Apple Discussions and StackOverflow. The iPhone Reference Library is a fourth but that *should* go without saying. I've found it particular useful searching for exact classnames to find the detailed API overview of properties and methods I need.

Sep 04
2009

iPhone Development and an AppStore Wishlist

I've been doing A LOT of iPhone development since Feb 14th of this year and I'm happy to report our iPhone app has been selling well. Much better than the first release which, well, I have to admit, had a beta feel to it. The latest release is far more solid and it's pretty telling in the great reviews we've been getting.

Now that I finally feel like I understand the majority of what I've been doing I'm happy to break the seal on my experiences as a CF developer learning Objective C and playing in Apple's world.

Being crotchety for today though I feel like I have to start with some gripes and a wishlist for the appstore.

Gripes

This morning I woke up to an email from Apple to tell me it's time to renew my Apple Developer subscription fee for iPhone development. It's just $100 I hear you say but since May 5th when our app hit the store, Apple has had it's cut of our revenue as we agreed to the tune of around $2500. Surely they could add a provision that says, if you generate over a certain dollar value we'll waive the renewal fee as thanks for the easy money.

Wishlist for the Appstore

Referral summaries

I'm a nerd and I hate spending money if I can't see the value of my investment. I've submitted feature requests a few times with a few bumps to the iTunes Connect folk to see if they can add some rudimentary analytics.

I do analytics for a living, helping my clients understand if they are getting value for every dollar they spend on marketing. For example if I run an ad in Google AdWords that costs me 10 cents and my app revenue per unit is $2, I have to convert 1 in 20 visitors (5%) to break even and if my conversion rate is better, I'm in the money. Same with facebook ads and any other kind of marketing medium.

The problem with the appstore from a vendor perspective is that although I can send people to the appstore, I only get a raw number once a day of how many sales I made in each country.

The first step for me is for apple to introduce referral URL variables. Instead of just:

http://itunes.com/apps/walkjogrunroutes
I'd like to add a URL variable to track each ad I use:
http://itunes.com/apps/walkjogrunroutes?rf=adwords1234
where adwords1234 represents a code I created to track a specific AdWords ad.

Hell, even one step before that could be a simple examination of the referral URL so they can segment sales by referring site.

If I know how many clicks I paid for in my Google AdWords campaign and how many sales those clicks generated each day, I have my conversion rate. Presently nothing exists like this.

AppStore conversion rates

This is tied to the previous point but the AppStore is a big black box of mystery. I send people in but have no idea how many times my app page gets viewed per day, how many people went direct to the app, vs searching vs category browsing.

Did they find the app on their iPhone and do those types of visitors convert more readily than computer based browsers? What type of search terms result in the majority of my sales? How many times does a visitor review the app before they buy?

Does anyone else reading this do any iPhone (native) development and have any other ideas for how the AppStore can be improved?