Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Beginning iOS5 Development.pdf
Скачиваний:
7
Добавлен:
09.05.2015
Размер:
15.6 Mб
Скачать

556

CHAPTER 15: Grand Central Dispatch, Background Processing, and You

self.segmentedControl = nil;

}

Now, move to the end of the viewDidLoad method, where we’ll create the segmented control and add it to the view:

.

.

.

self.smileyView.image = self.smiley;

self.segmentedControl = [[UISegmentedControl alloc] initWithItems: [NSArray arrayWithObjects:

@"One", @"Two", @"Three", @"Four", nil]] ; self.segmentedControl.frame = CGRectMake(bounds.origin.x + 20,

CGRectGetMaxY(bounds) - 50, bounds.size.width - 40, 30);

[self.view addSubview:segmentedControl];

[self.view addSubview:smileyView]; [self.view addSubview:label];

}

Build and run the app. You should see the segmented control and be able to click its segments to select them one at a time.

Here's where we should mention a slight backward-compatibility issue, since something subtle but important has changed between iOS 4 and iOS 5. To see the difference, we're going to blast into the past, to the dark days of iOS 4.3, and see what happens. Don't worry; you don't need to downgrade a device. We'll do it in Xcode and the iOS simulator.

A Brief Journey to Yesteryear

In the project navigator, select the topmost item, the one that represents your project, to examine your project details. You've seen this view before, which shows you various settings for the project and the application target. Select the State Lab item in the TARGETS section, and make sure the Summary tab is selected at the top of the detail view. The uppermost section, iOS Application Target, contains a Deployment Target popup, currently set to the latest version of iOS that your copy of Xcode knows about. Click the control and choose 4.3. This tells Xcode not only that it should build the app with iOS 4.3 in mind, but also run the iOS simulator using iOS 4.3. Next, click the scheme/device popup control near the upper left of the window, and choose iPhone 4.3 Simulator from the popup. You're now ready to take a trip in the way-back machine!

If you build and run your app at this point, you’ll see one glaring problem: the segmented control doesn’t seem to work! You can tap those segments all you like, and nothing will happen. The problem actually lies with the animation. By default, the Core Animation method we used to set up animation actually prevents some amount of user input from being collected while animations are running (presumably this is a performance optimization). The key difference here between iOS 4 and iOS 5 is that

www.it-ebooks.info

CHAPTER 15: Grand Central Dispatch, Background Processing, and You

557

while iOS 5 turns off user interaction for the views that are currently animated, iOS 4 turns off user interaction for the entire application!

Fortunately, there is an optional way to enable user interaction, which requires us to use a longer method name in each of our rotate methods. Modify them as shown here:

- (void)rotateLabelDown {

[UIView animateWithDuration:0.5 delay:0

options:UIViewAnimationOptionAllowUserInteraction

animations:^{

label.transform = CGAffineTransformMakeRotation(M_PI);

}

completion:^(BOOL finished){ [self rotateLabelUp]; }];

}

- (void)rotateLabelUp {

[UIView animateWithDuration:0.5 delay:0

options:UIViewAnimationOptionAllowUserInteraction

animations:^{

label.transform = CGAffineTransformMakeRotation(0);

}

completion:^(BOOL finished){ if (animate) {

[self rotateLabelDown];

}

}];

}

Build and run the app again, and see what happens. That’s more like it, eh? As we said, this difference between iOS 4 and iOS 5 is subtle, but quite important if your apps use Core Animation and you need to support iOS 4. Though you could strip that code back out, since we’re about to return to the iOS 5 simulator, there’s really no harm in leaving it in and allowing the code to work under iOS 4 as well.

Back to the Background

Let’s return to the present. Select iPhone 5.0 Simulator from the popup menu in the upperleft part of the project window. Now, touch any one of the four segments, and then go through the now-familiar sequence of backgrounding your app and bringing it back up. You’ll see that the segment you chose (bet it was Three) is still selected—no surprise there. Background your app again by clicking the home button, bring up the taskbar (by double-clicking the home button) and kill your app, and then relaunch it. You’ll find yourself back at square one, with no segment selected. That’s what we need to fix next.

www.it-ebooks.info

558

CHAPTER 15: Grand Central Dispatch, Background Processing, and You

CAUTION: When you kill your app in the simulator, it’s possible (depending on which version of Xcode you are running) that upon relaunching your app, you’ll find yourself back in Xcode as the result of a SIGKILL signal. This is perfectly normal. If this happens, click the stop button at the top

left of the project window, and then rerun your project to bring your project back to life in the

simulator.

Saving the selection is simple enough. We just need to add a few lines to the end of the applicationDidEnterBackground method in BIDViewController.m:

- (void)applicationDidEnterBackground { NSLog(@"VC: %@", NSStringFromSelector(_cmd)); self.smiley = nil;

self.smileyView.image = nil;

NSInteger selectedIndex = self.segmentedControl.selectedSegmentIndex; [[NSUserDefaults standardUserDefaults] setInteger:selectedIndex

forKey:@"selectedIndex"];

}

But where should we restore this selection index and use it to configure the segmented control? The inverse of this method, applicationWillEnterForeground, isn’t what we want. When that method is called, the app has already been running, and the setting is still intact. Instead, we need to access this when things are being set up after a new launch, which brings us back to the viewDidLoad method. Add the bold lines shown here at the end of the method:

.

.

.

[self.view addSubview:label];

NSNumber *indexNumber;

if (indexNumber = [[NSUserDefaults standardUserDefaults] objectForKey:@"selectedIndex"]) {

NSInteger selectedIndex = [indexNumber intValue]; self.segmentedControl.selectedSegmentIndex = selectedIndex;

}

}

We needed to include a little sanity check here to see whether there’s a value stored for the selectedIndex key, to cover cases such as the first app launch, where nothing has been selected.

Now build and run the app, touch a segment, and then do the full background-kill- restart dance. There it is—your selection is intact!

Obviously, what we’ve shown here is pretty minimal, but the concept can be extended to all kinds of application state. It’s up to you to decide how far you want to take it in order to maintain the illusion for the users that your app was always there, just waiting for them to come back!

www.it-ebooks.info

CHAPTER 15: Grand Central Dispatch, Background Processing, and You

559

Requesting More Backgrounding Time

Earlier, we mentioned the possibility of your app being dumped from memory if moving to the Background state takes too much time. For example, your app may be in the middle of doing a file transfer that it would really be a shame not to finish, but trying to hijack the applicationDidEnterBackground method to make it complete the work there, before the application is really backgrounded, isn’t really an option. Instead, you should use applicationDidEnterBackground as a platform for telling the system that you have some extra work you would like to do, and then start up a block to actually do it. Assuming that the system has enough available RAM to keep your app in memory while the user does something else, the system will oblige you and keep your app running for a while.

We’ll demonstrate this not with an actual file transfer, but with a simple sleep call. Once again, we’ll be using our new acquaintances GCD and blocks to make the contents of our applicationDidEnterBackground method run in a separate queue.

In BIDViewController.m, modify the applicationDidEnterBackground method as follows:

- (void)applicationDidEnterBackground { NSLog(@"VC: %@", NSStringFromSelector(_cmd));

UIApplication *app = [UIApplication sharedApplication];

__block UIBackgroundTaskIdentifier taskId;

taskId = [app beginBackgroundTaskWithExpirationHandler:^{ NSLog(@"Background task ran out of time and was terminated."); [app endBackgroundTask:taskId];

}];

if (taskId == UIBackgroundTaskInvalid) { NSLog(@"Failed to start background task!"); return;

}

dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"Starting background task with %f seconds remaining",

app.backgroundTimeRemaining); self.smiley = nil; self.smileyView.image = nil;

NSInteger selectedIndex = self.segmentedControl.selectedSegmentIndex; [[NSUserDefaults standardUserDefaults] setInteger:selectedIndex

forKey:@"selectedIndex"];

// simulate a lengthy (25 seconds) procedure [NSThread sleepForTimeInterval:25];

NSLog(@"Finishing background task with %f seconds remaining", app.backgroundTimeRemaining);

[app endBackgroundTask:taskId];

});

}

www.it-ebooks.info

560

CHAPTER 15: Grand Central Dispatch, Background Processing, and You

Let’s look through this code piece by piece. First, we grab the shared UIApplication instance, since we’ll be using it several times in this method. Then comes this:

__block UIBackgroundTaskIdentifier taskId;

taskId = [app beginBackgroundTaskWithExpirationHandler:^{ NSLog(@"Background task ran out of time and was terminated."); [app endBackgroundTask:taskId];

}];

The call to beginBackgroundTaskWithExpirationHandler: returns an identifier that we’ll need to keep track of for later use. We’ve declared the taskId variable it’s stored in with the __block storage qualifier, since we want to be sure the identifier returned by the method is shared among any blocks we create in this method.

With the call to beginBackgroundTaskWithExpirationHandler:, we’re basically telling the system that we need more time to accomplish something, and we promise to let it know when we’re finished. The block we give as a parameter may be called if the system decides that we’ve been going way too long anyway and decides to stop running.

Note that the block we gave ended with a call to endBackgroundTask:, passing along taskId. That tells the system that we’re finished with the work for which we previously requested extra time. It’s important to balance each call to beginBackgroundTaskWithExpirationHandler: with a matching call to endBackgroundTask: so that the system knows when we’ve completed the work.

NOTE: Depending on your computing background, the use of the word task here may evoke

associations with what we usually call a process, consisting of a running program that may contain multiple threads, and so on. In this case, try to put that out of your mind. The use of task in this context really just means “something that needs to get done.” Any task you create here is

running within your still-executing app.

Next, we do this:

if (taskId == UIBackgroundTaskInvalid) { NSLog(@"Failed to start background task!"); return;

}

If our earlier call to beginBackgroundTaskWithExpirationHandler: returned the special value UIBackgroundTaskInvalid, that means the system is refusing to grant us any additional time. In that case, you could try to do the quickest part of whatever needs doing anyway and hope that it completes quickly enough that your app won’t be terminated before it’s finished. This is mostly likely to be an issue when running on older devices, such as the iPhone 3G, that let you run iOS 4 but don’t support multitasking. In this example, however, we’re just letting it slide.

Next comes the interesting part where the work itself is actually done:

dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"Starting background task with %f seconds remaining",

app.backgroundTimeRemaining);

www.it-ebooks.info

CHAPTER 15: Grand Central Dispatch, Background Processing, and You

561

self.smiley = nil; self.smileyView.image = nil;

NSInteger selectedIndex = self.segmentedControl.selectedSegmentIndex; [[NSUserDefaults standardUserDefaults] setInteger:selectedIndex

forKey:@"selectedIndex"]; // simulate a lengthy (25 seconds) procedure

[NSThread sleepForTimeInterval:25];

NSLog(@"Finishing background task with %f seconds remaining", app.backgroundTimeRemaining);

[app endBackgroundTask:taskId];

});

All this does is take the same work our method was doing in the first place and place it in a background queue. At the end of that block, we call endBackgroundTask: to let the system know that we’re finished.

With that in place, build and run the app, and then background your app by pressing the home button. Watch the Xcode console as well as the status bar at the bottom of the Xcode window. You’ll see that this time, your app stays running (you don’t get the “Debugging terminated” message in the status bar), and after 25 seconds, you will see the final log in your output. A complete run of the app up to this point should give you console output along these lines:

2011-10-30 22:35:28.608 State Lab[7449:207] application:didFinishLaunchingWithOptions:

2011-10-30 22:35:28.616 State Lab[7449:207] applicationDidBecomeActive: 2011-10-30 22:35:28.617 State Lab[7449:207] VC: applicationDidBecomeActive 2011-10-30 22:35:31.869 State Lab[7449:207] applicationWillResignActive: 2011-10-30 22:35:31.870 State Lab[7449:207] VC: applicationWillResignActive 2011-10-30 22:35:31.871 State Lab[7449:207] applicationDidEnterBackground: 2011-10-30 22:35:31.873 State Lab[7449:207] VC: applicationDidEnterBackground

2011-10-30 22:35:31.874 State Lab[7449:1903] Starting background task with 599.995069 seconds remaining

2011-10-30 22:35:56.877 State Lab[7449:1903] Finishing background task with 574.993956 seconds remaining

As you can see, the system is much more generous with time when doing things in the background than in the main thread of your app, so following this procedure can really help you out if you have any ongoing tasks to deal with.

Note that we asked for a single background task identifier, but in practice, you can ask for as many as you need. For example, if you have multiple network transfers happening at Background time and you need to complete them, you can ask for an identifier for each and allow them to continue running in a background queue. So, you can easily allow multiple operations to run in parallel during the available time. Also consider that the task identifier you receive is a normal C-language value (not an object), and apart from being stored in a local __block variable, it can just as well be stored as an instance variable if that better suits your class design.

www.it-ebooks.info

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]