Building Streak or: How I Learned to Stop Worrying and Love the Lock Screen

The conversation that got us started on Streak went something like:

"What about a true or false game that you can play from the lock screen?"

"Sounds AWESOME. We can build that today."

We so, so, did not build it that day. Making Streak, like many crazy ideas, turned out to be shockingly difficult. Two aspects of our development were really tough:

  1. Creating an app that can run every day, ad infinitum, with user interaction 100% on the lock screen.
  2. Allowing everyone to play against each other at once, with "everyone" possibly being hundreds/thousands of people.

We'll write another post about #2, but we learned so much about the workings of iOS notifications to achieve #1 that we thought a write-up might be helpful to fellow developers. This stuff is probably obvious to hardened Cocoaheads, but we learned a heck of a lot from googling for posts like this, so hopefully this will help someone, somewhere.

Also if any of this is wrong, we would be forever grateful if you tell us. We have struggled with these issues mightily.

There are a few things about iOS 8 notifications and notification actions that we learned the hard way. Like, the really hard way:

  • Local notifications are the only way to precisely time any app activity (timers don't work)
  • Local notifications don't run code unless the user interacts with them, while remote notifications do
  • Completion Handlers are your god and if you do not respect them they will smite you
  • The system will probably terminate your app from the background sometimes. When this happens, if the user wakes you up using a notification action, unexpected things will happen.

Local Notifications vs. Timers or Remote Notifications

If you are naive—as we are—when you come up with the idea to have a precisely timed trivia game, you think aha, NSTimer! You are wrong. Why? The system will kill your NSTimer in the background unless you are using specially privileged services like location monitoring (a topic for another post).

What about remote notifications, like those cool silent ones you heard about in the WWDC video? Great for delivering content in the background on a loose schedule, not great for precise timing. If you have to send a bunch, you will almost certainly get rate-limited, and your notifications won't deliver, which pretty much hoses the whole thing, right? "Regular" remote notifications, with alerts, seem to be way more reliable, but if you're looking for second-by-second precision, you won't get it (due to them coming over the Internet and all that).

So, local notifications. They post pretty much on time, you can hand them off to the system and they will fire, even if you're terminated (or deleted), and you can clear them before they fire with

[UIApplication sharedApplication].scheduledLocalNotifications.

But using local notifications brings us to our next problem...

Local Notifications Don't Run Code (Without User Interaction)

So you're sitting pretty, thinking, hey, cool, I can run code on whatever timeline I want, since:

- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification

will run when the notification fires. Sadly, you cannot. That function only fires if your app is in the foreground, which it might not be.

The only way to run code from the lock screen via a local notification is:

- (void)application:(UIApplication *)application handleActionWithIdentifier:(NSString *)identifier forLocalNotification:(UILocalNotification *)notification completionHandler:(void (^)())completionHandler

meaning the user has swiped on it and tapped a button. Which brings us to our next pitfall...

Completion Handlers Must Be Respected at All Costs

Oh man, have we screwed this one up good. There are two cardinal sins for notification actions with completion handlers. The first is not calling them (the app will get angry and start ignoring you), and the second, which is much more insidious, is calling them before whatever you wanted to do is done.

E.g., if you need to go fetch something in the background, this task must be done before you call the handler. Or it will be killed by the system and you will be sad and confused. If you're working with notification actions or background refresh, I recommend you treat completion handlers with the utmost respect.

The Terminator

Your app will probably be terminated by the system or by users from time to time. This is not a big deal if it's mostly run in the foreground, as re-opening the app restores everything. If you're looking to resurrect from termination via notification action, though, a few things to know:

  • NSUserDefaults will be inaccessible. (As will's basic functions, if you're using that for data storage, as we are)
  • The only way to access stored data (as far as we can tell) is to use NSFileManager, and this line is critical:

[[NSFileManager defaultManager] setAttributes:@{NSFileProtectionKey : NSFileProtectionNone}]

This was a huge problem for us, as background data persistence/access is vital to our app, and termination kept mucking it up. Finally we stumbled upon some similar Stack Overflow threads / blog posts that led us to the solution. Hopefully this fix will help you too.

We have no idea if these hard-won learnings will apply to anyone else, but it feels good to put them down on paper. And please try out Streak and tell us what you think!