Today we’re going to finish updating the Demine project for the iOS 4.0 environment by adding complete support for multitasking. Even though Demine is not going to be doing any background processing, it turns out that a multitasking environment mandates certain changes to the program; some we saw last week, and the balance we’ll see now.
(We’ll be making changes to the project as we left it last week; you can download the final version here.)
Pseudo-Multitasking
AAPL has always encouraged developers to implement what I call “pseudo-multitasking”: the creation of applications that conceal the fact that (until recently) the device could only execute one application at a time. Applications perform this sleight of hand by recording their exact state when they are shut down — what screen they are displaying, where that screen is scrolled to, what the navigation hierarchy looks like — and later using that state to configure themselves after a restart. For instance, the iPhone Human Interface Guidelines state that:
All iPhone applications should:
- …
- Save the current state when stopping, at the finest level of detail possible. For example, if your application displays scrolling data, save the current scroll position.
and:
When starting, iPhone applications should:
- …
- Restore state from the last time your application ran. People should not have to remember the steps they took to reach their previous location in your application.
I have always disliked these guidelines, and therefore largely ignored them. On an aesthetic level I resent them because they seem to represent the platform’s attempt to push a problem that properly belongs to it onto the shoulders of the application developer. On a practical level I find them impossible to follow consistently. Let me illustrate this latter point, using Demine as an example.
Demine consists of 7 screens:
- The root controller/saved-game list
- The playfield
- The high-scores list
- The new game screen
- The custom puzzle screen
- The store menu
- The store detail view
Most of these are either problematic or uninteresting to return to on a restart. The store screens really can’t be gracefully re-presented, because much of their content is fetched from iTunes and therefore not immediately available. The saved-game and high-score screens hardly seem worth the trouble. The custom puzzle screen is both not terribly interesting and a hassle: it’s got 3 text entry fields, which would have to be cached somewhere, along with a record of which field had focus. The playfield is also problematic; since this is a timed game, dropping the user into the middle of a game isn’t the nicest thing to do. Naively pausing the game isn’t a great solution either, since that would give him time to study the playfield, and subvert the timer.
Some of this stuff could be worked around, but the effort seems disproportionate to the gains realized. Plus — and I admit that this may be influencing my judgement — it seems like boring, fiddly work that would go largely unappreciated. Psuedo-multitasking is nice in theory, and something to consider when designing an app, but it is, in my view, a checkbox feature often best sidestepped or downplayed.
I bring all this up not because I like to complain (although that is a factor) but because the multitasking guidelines imply/suggest/require that apps should support pseudo-multitasking as a fallback behavior. The idea is that if/when an app is terminated from the suspended state (as it almost inevitably will be) it should, upon restart, maintain the pretense that it had never been halted. I find this arrangement even more annoying than traditional pseudo-multitasking: it’s more complicated for the developer, while the benefits to the user are smaller. I’ll continue to ignore it as much as possible.
Now, let’s turn to the second multitasking checklist:
OpenGL
AAPL says:
Do not make any OpenGL ES calls from your code. You must not create an EAGLContext object or issue any OpenGL ES drawing commands of any kind. Using these calls will cause your application to be terminated immediately.
Okay, this one’s easy. Demine doesn’t use OpenGL.
Bonjour
AAPL says:
Cancel any Bonjour-related services before being suspended. When your application moves to the background, and before it is suspended, it should unregister from Bonjour and close listening sockets associated with any network services. A suspended application cannot respond to incoming service requests anyway. Closing out those services prevents them from appearing to be available when they actually are not. If you do not close out Bonjour services yourself, the system closes out those services automatically when your application is suspended.
Demine also doesn’t offer any network services, or use Bonjour. 2 down, 9 to go.
Networking
AAPL says:
Be prepared to handle connection failures in your network-based sockets. The system may tear down socket connections while your application is suspended for any number of reasons. As long as your socket-based code is prepared for other types of network failures, such as a lost signal or network transition, this should not lead to any unusual problems. When your application resumes, if it encounters a failure upon using a socket, simply reestablish the connection.
In Demine, only the store interacts with the network. The store is already set up to handle network failures, and, as we saw last week, its reachability support code gracefully handles network failures during program suspension. So we should be fine on this score.
State
AAPL says:
Save your application state before moving to the background. During low-memory conditions, background applications are purged from memory to free up space. Suspended applications are purged first, and no notice is given to the application before it is purged. As a result, before moving to the background, an application should always save enough state information to reconstitute itself later if necessary. Restoring your application to its previous state also provides consistency for the user, who will see a snapshot of your application’s main window briefly when it is relaunched.
Although we’re not interested in pseudo-multitasking (as discussed at the beginning of this article), we do want to save any in-progress games, and stop any game timers, when the app is moved to the background. We’d previously performed this work only in the Playfield's
viewWillDisappear:
method, but, ho-ho-ho, that method is not called on multitasking platforms when the application is moved into the background. Our earlier approach also left something to be desired in terms of handling SMS alerts and the like, so it was due for an update anyway.
Before we get to anything else, however, we must address a nasty little surprise: when a user brings up the “task switcher” UI, the active app receives a UIApplicationWillResignActiveNotification
, but the user can still interact with it until he selects another app. This is a problem for Demine, since we can’t know, upon receiving such a notification, whether it’s about to enter task switcher (in which case, as matters now stand, we really oughtn’t to stop the gameplay timer) or to enter a “real” inactive state (in which case we should stop the timer). To resolve this, we need to add a proper “pause” mode driven by the “(in)active” notifications.
Our pause mode should do three things:
- Obscure the playfield (so the player can’t study it for “free”, subverting the timer)
- Clearly indicate that the game is paused
- Stop the gameplay timer
We can implement such a pause mode by adding an additional UIView
to Playfield.xib
, and covering the main playfield with this view upon entering an inactive state. Here is the heart of the new code:
- (void)pause
{
[self stopTimer];
[self.view addSubview:self.pauseScreen];
self.pauseLabel.text = self.titleLabel.text;
self.pauseScreen.alpha = 0.0;
[UIView beginAnimations:nil context:nil];
self.pauseScreen.alpha = 1.0;
[UIView commitAnimations];
}
- (void)unpause
{
[UIView beginAnimations:nil context:nil];
self.pauseScreen.alpha = 0.0;
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:@selector(finishUnpause:finished:context:)];
[UIView commitAnimations];
}
- (void)finishUnpause:(NSString*)animationID finished:(NSNumber*)finished context:(void*)context
{
[self startTimer];
[self.pauseScreen removeFromSuperview];
}
Playfields
fire their pause
methods on receipt of UIApplicationWillResignActiveNotifications
, and their unpause
methods on receipt of UIApplicationDidBecomeActiveNotifications
. This last is a little dodgy; if a non-visible Playfield
were hanging around in memory when the app received this notification, this logic would start running the timer on its puzzle. That situation should never arise, however.
Our task-switching problems still aren’t entirely over: A user might enter the task switcher while Demine was displaying its saved puzzle list, and then launch a puzzle while inside the task switcher UI. This would produce an un-paused puzzle inside the task switcher UI, which is not what we want. The simplest fix for this appears to be recoding Playfield's
viewDidAppear:
method along these lines (the respondsToSelector:
stuff is there so that we can run on iOS 3.0 platforms):
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
UIApplication* app = [UIApplication sharedApplication];
if ([app respondsToSelector:@selector(applicationState)] && (app.applicationState != UIApplicationStateActive))
{
[self pause];
}
else
{
[self startTimer];
}
}
Now, finally, we can implement our new approach to state management:
- When a
Playfield
becomes visible (viewDidAppear:
) it will start the timer, if necessary, or pause the game, if necessary - When a
Playfield
disappears (viewWillDisappear:
) it will stop the timer, if necessary, and save the puzzle - When a
UIApplicationDidBecomeActiveNotification
is received by aPlayfield
, it will unpause the game, if necessary - When a
UIApplicationWillResignActiveNotification
is received by aPlayfield
, it will pause the game, if necessary - When a
UIApplicationDidEnterBackgroundNotification
is received by aPlayfield
, it will save the puzzle
Much of this is already done; really, we need only create a shared save
method, and invoke it from viewWillDisappear:
and in response to UIApplicationDidEnterBackgroundNotifications
. Something like this:
- (void)save
{
[puzzle saveToManagedPuzzle:managedPuzzle];
NSError* error;
if (![managedPuzzle.managedObjectContext save:&error])
{
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
}
}
There is one last bit of iOS 3.0 business to deal with: UIApplicationDidEnterBackgroundNotification
isn’t available on iOS 3.0, and attempts to refer to it throw a run-time error. Based upon these two examples, I resolved this by weak-linking the symbol:
UIKIT_EXTERN NSString* const UIApplicationDidEnterBackgroundNotification __attribute__((weak_import));
…
- (void)viewDidLoad
{
…
if (&UIApplicationDidEnterBackgroundNotification != NULL)
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(save)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
}
…
}
Finally, in a slight nod to pseudo-multitasking (though it’s really just a UI refinement), I want to store the Playfield's
scroll position when saving a puzzle; this will let a user resume play exactly where he left off, instead of being returned to the UL corner of the puzzle. The only weird thing here is how the stored value is assigned to contentOffset
:
- (void)viewDidLoad
{
…
CGPoint co = CGPointMake([managedPuzzle.scrollX floatValue], [managedPuzzle.scrollY floatValue]);
if (CGPointEqualToPoint(co, self.scrollView.contentOffset))
{
[self scrollViewDidScroll:self.scrollView];
}
else
{
self.scrollView.contentOffset = co;
}
…
}
We test for equality because we need to trigger a scrollViewDidScroll:
message to lay out the Playfield's
initial tiles; changing the contentOffset
will do this, but an assignment that does not change the value will not. Hence, this little hack.
Memory
AAPL says:
Release any unneeded memory when moving to the background. Background applications must maintain enough state information to be able to resume operation quickly when they move back to the foreground. But if there are objects or relatively large memory blocks that you no longer need (such as unused images), you should consider releasing that memory when moving to the background.
This type of memory management has never been my strong suit; this is due partly to long experience with VM environments, in which a program’s total memory footprint (as distinct from its working set) just isn’t terribly relevant, and partly to the view that, on the pre-multitasking iPhone, my footprint was whatever it was, and as long as the program didn’t crash (and didn’t leak), I didn’t care. Still, let’s see if there’s anything worth releasing in Demine.
Accoring to Instruments, Demine’s footprint is about 0.25MB. So, er, no, there’s nothing worth releasing. A quarter-meg represents about 0.1% of the memory on an iPhone 3GS; even if we could reduce this when the app moves into the background, it wouldn’t accomplish anything.
Shared Resources
AAPL says:
Stop using shared system resources before being suspended. Applications that interact with shared system resources such as Address Book need to stop using those resources before being suspended. Priority over such resources always goes to the foreground application. At suspend time, if an application is found to be using a shared resource, it will be terminated instead.
This is another one that doesn’t seem to apply to us, as Demine doesn’t connect to the address book, calendar, etc. (It does access the iTunes store, but I’m assuming that that doesn’t count.) I would note, in passing, just how vague this guideline is: What counts as a “shared system resource”? What counts as “using”? Is my app allowed to hold an ABAddressBookRef
when suspended? Or hold one if it doesn’t access it from the background state? Or hold one if it hasn’t made any unsaved changes?
It’s all rather ill-defined.
Views
AAPL says:
Avoid updating your windows and views. While in the background, your application’s windows and views are not visible, so you should not try to update them. Although creating and manipulating window and view objects in the background will not cause your application to be terminated, this work should be postponed until your application moves to the foreground.
Since we now pause Demine when it resigns the active state (as it always will en route to a background state) we’ve already addressed this; we shutdown our timer and stop nav. bar updates when not in the foreground. (I suppose some weird confluence of factors might cause the Store
controller to update while in the background, but that doesn’t seem worth worrying about.)
Accessories
AAPL says:
Respond to connect and disconnect notifications for external accessories. For applications that communicate with external accessories, the system automatically sends a disconnection notification when the application moves to the background. The application must register for this notification and use it to close out the current accessory session. When the application moves back to the foreground, a matching connection notification is sent giving the application a chance to reconnect. For more information on handling accessory connection and disconnection notifications, see External Accessory Programming Topics.
Demine doesn’t care about external accessories.
Alerts
AAPL says:
Clean up resources for active alerts when moving to the background. In order to preserve context when switching between applications, the system does not automatically dismiss action sheets (UIActionSheet) or alert views (UIAlertView) when your application moves to the background. (For applications linked against iOS 3.x and earlier, action sheets and alerts are still dismissed at quit time so that your application’s cancellation handler has a chance to run.) It is up to you to provide the appropriate cleanup behavior prior to moving to the background. For example, you might want to cancel the action sheet or alert view programmatically or save enough contextual information to restore the view later (in cases where your application is terminated).
We don’t use any UIActionSheets
in Demine, but UIAlertViews
pop up in quite a few places. Fourteen places, actually:
- Upgrade nag for Expert puzzles
- Upgrade nag for Custom puzzles
- Reachability alert in the Store
- Unrecoverable error alert on NSPersistentStoreController setup failure
- Notification for completed purchase
- Notification for failed purchase
- Notification for completed transaction restore
- Notification for failed transaction restore
- Unrecoverable error alert on NSFetchedResultsController setup failure
- Warning on puzzle creation failure
- Out-of-bounds warning for custom puzzle parameter: width
- Out-of-bounds warning for custom puzzle parameter: height
- Out-of-bounds warning for custom puzzle parameter: number of mines
- Warning on disabled in-app purchases
Of these, none are something you’d (theoretically) want to re-create in a pseudo-multitasking application, and only one (the reachability alert) has a non-empty cancel handler. All can reasonably be left in place when moving to the background, so that’s what we’ll do. No changes are required here.
As an aside, it’s convenient that no changes are required, because they’d be annoying to implement. UIActionSheets
and UIAlertViews
are often “fire-and-forget”; if we wanted to dismiss them programmatically on entering the background, we’d have to add logic to keep track of them and handle them appropriately. That doesn’t seem like it would be much fun.
Snapshot
AAPL says:
Remove sensitive information from views before moving to the background. When an application transitions to the background, the system takes a snapshot of the application’s main window, which it then presents briefly when transitioning your application back to the foreground. Before returning from your applicationDidEnterBackground: method, you should hide or obscure passwords and other sensitive personal information that might be captured as part of the snapshot.
Demine doesn’t handle any sensitive information, so this doesn’t apply to us. It is interesting to know about this screenshot business, however; one hopes that these screenshots are not also used when relaunching applications that have been terminated from the background. They don’t appear to be, but there is this worrying remark from earlier in the checklist:
Restoring your application to its previous state also provides consistency for the user, who will see a snapshot of your application’s main window briefly when it is relaunched.
I presume that this is some version of a typo (a thinko?), and that the OS won’t really display a screenshot of your app’s last state (in lieu of the default startup image) on a relaunch. But given’s AAPL’s love affair with pseudo-multitasking, one never knows.
Sleep
AAPL says:
Do minimal work while running in the background. The execution time given to background applications is more constrained than the amount of time given to the foreground application. If your application plays background audio or monitors location changes, you should focus on that task only and defer any nonessential tasks until later. Applications that spend too much time executing in the background can be throttled back further by the system or terminated altogether.
Demine doesn’t do anything in the background.
Conclusion
I hope these posts have demonstrated just how troublesome a multitasking environment can be, even for a simple app like Demine. While it’s certainly possible to just compile a pre-existing app against the 4.0 SDK, thereby getting multitasking “for free”, there are many details to consider if you don’t want your app to develop a bunch of quirks in the process.
Pingback: Things that were not immediately obvious to me » Blog Archive » Multitasking Opt-Out