The exact moment iOS takes the view snapshot when entering background?
Asked Answered
Z

5

23

I have a problem when putting my iPhone app to background by pushing the exit button, and then relaunching by tapping the launch icon on the home screen: the app's view does return to its initial state like I want it to, but before that it flashes the earlier, wrong view state onscreen briefly.

Background

My main view consists basically of a sequence of interlinked UIAnimateWithDuration calls. The behavior I want whenever any interruption occurs, is to reset the animation to its initial state (unless the animations have all finished and the app has entered the static final phase), and start over from there whenever the app returns to active and visible state.

After studying the subject I learned I need two types of interruption handling code to provide good ux: "instant" and "smooth". I have the method resetAnimation that resets the view properties to the initial state instantly, and the method pauseAnimation that animates quickly to the same state, with an additional label stating "paused" fading in on the top of the view.

Double clicking exit button

The reason for this is the "double clicking exit button" use case, that actually does not hide your view or put you in the background state, it just scrolls up a bit to show the multitasking menu at the bottom. So, resetting the view state instantly in this case just looked very ugly. The animated transition and telling the user you're paused seemed like a better idea.

This case works nice and smootly by implementing the applicationWillResignActive delegate method in my App Delegate and calling pauseAnimation from there. I handle returning from that multitasking menu by implementing the applicationDidBecomeActive delegate method and calling from there my resumeAnimation method, that fades out the "paused" label if its there, and starts my animation sequence from the initial state.

This all works fine, no flickering anywhere.

Visiting flipside

My app's built over the Xcode "utility" template, so it has a flipside view to show info/settings. I handle visiting the flipside and returning back to the main view by implementing these two delegate methods in my main view controller:

  • (void)viewDidDisappear:(BOOL)animated

  • (void)viewDidAppear:(BOOL)animated

I call my resetAnimation in the viewDidDisappear method and resumeAnimation in viewDidAppear. This all works fine, the main view is its initial state from the very beginning of the transition to visible state - no unexpected flashing of wrong animation states of anything. But:

Pushing exit button and relaunching from my app icon (the buggy part!)

This is where the trouble starts. When I push exit button once and my app begins its transition to background, two things happen. First, applicationWillResignActive gets called here too, so my pauseAnimation method launches also. It wouldn't need to, since the transition doesn't need to be smooth here – the view just goes static, and "zooms out" to reveal the home screen – but what can you do? Well, it wouldn't do any harm either if I just could call resetAnimation before the exact moment that the system takes the snapshot of the view.

Anyways, secondly, applicationDidEnterBackground in the App Delegate gets called. I tried to call resetAnimation from there so that the view would be in the right state when the app returns, but this doesn't seem to work. It seems the "snapshot" has been taken already and so, when I tap my app launch icon and relauch, the wrong view state does flash briefly on the screen before the correct, initial state shows. After that, it works fine, the animations go about like they're supposed to, but that ugly flicker at that relaunch moment won't go away, no matter what I try.

Fundamentally, what I'm after is, what exact moment does the system take this snapshot? And consequently, what would be the correct delegate method or notification handler to prepare my view for taking the "souvenir photo"?

PS. Then there's the default.png, which doesn't seem to only show at first launch, but also whenever the processor's having a hard time or returning to the app is delayed briefly for some other reason. It's a bit ugly, especially if you're returning to your flipside view that looks totally different from your default view. But this is such a core iOS feature, I'm guessing I shouldn't even try to figure out or control that one :)


Edit: since people were asking for actual code, and my app has already been released after asking this question, I'll post some here. ( The app's called Sweetest Kid, and if you want to see how it actually works, it's here: http://itunes.apple.com/app/sweetest-kid/id476637106?mt=8 )

Here's my pauseAnimation method – resetAnimation is almost identical, except its animation call has zero duration and delay, and it doesn't show the 'Paused' label. One reason I'm using UIAnimation to reset the values instead of just assigning the new values is that for some reason, the animations just didn't stop if I didn't use UIAnimation. Anyway, here's the pauseAnimation method:

    - (void)pauseAnimation {
    if (currentAnimationPhase < 6 || currentAnimationPhase == 255) { 
            // 6 means finished, 255 is a short initial animation only showing at first launch
        self.paused = YES;
        [UIView animateWithDuration:0.3
                              delay:0 
                            options:UIViewAnimationOptionAllowUserInteraction |
         UIViewAnimationOptionBeginFromCurrentState |
         UIViewAnimationOptionCurveEaseInOut |
         UIViewAnimationOptionOverrideInheritedCurve |
         UIViewAnimationOptionOverrideInheritedDuration
                         animations:^{
                             pausedView.alpha = 1.0;
                             cameraImageView.alpha = 0;
                             mirrorGlowView.alpha = 0;
                             infoButton.alpha = 1.0;
                             chantView.alpha = 0; 
                             verseOneLabel.alpha = 1.0;
                             verseTwoLabel.alpha = 0; 
                             verseThreeLabel.alpha = 0;
                             shine1View.alpha = stars1View.alpha = stars2View.alpha = 0;
                             shine1View.transform = CGAffineTransformIdentity;
                             stars1View.transform = CGAffineTransformIdentity;
                             stars2View.transform = CGAffineTransformIdentity;
                             finishedMenuView.alpha = 0;
                             preparingMagicView.alpha = 0;}
                         completion:^(BOOL finished){
                             pausedView.alpha = 1.0;
                             cameraImageView.alpha = 0;
                             mirrorGlowView.alpha = 0;
                             infoButton.alpha = 1.0;
                             chantView.alpha = 0; 
                             verseOneLabel.alpha = 1.0;
                             verseTwoLabel.alpha = 0; 
                             verseThreeLabel.alpha = 0;
                             shine1View.alpha = stars1View.alpha = stars2View.alpha = 0;
                             shine1View.transform = CGAffineTransformIdentity;
                             stars1View.transform = CGAffineTransformIdentity;
                             stars2View.transform = CGAffineTransformIdentity;
                             finishedMenuView.alpha = 0;
                             preparingMagicView.alpha = 0;
                         }];
        askTheMirrorButton.enabled = YES; 
        againButton.enabled = NO;
        shareOnFacebookButton.enabled = NO;
        emailButton.enabled = NO;
        saveButton.enabled = NO;
        currentAnimationPhase = 0;
        [[cameraImageView subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)]; // To remove the video preview layer
    }
}
Zaslow answered 29/10, 2011 at 9:20 Comment(4)
Unfortunately, I'd bet Apple doesn't want you to know/depend on when the picture is taken. They might change that in the future. +1 on this one.They do have options in some cases to allow a certain default image to load (like when receiving a push notification), but it would be nicer if you could have more control.Chlorothiazide
I have had this kind of trouble, too. #4659894 Would love to see a solution.Cammi
Why can't you use the pause/resume mechanic you already have working for the multitasking tray (two clicks of the "exit" button as you call it), when the user exits out to the Springboard (one click of the button)?Vomiturition
@JonGrant: Because pauseAnimation is an animation with a duration of 0.3 seconds, and it will never be able to finish, since clicking the exit button once just stops the app's foreground processes immediately. Additionally, pauseAnimation needs to be called from applicationWillResignActive for the desired effect, and at that point there's no way to find out whether the app will be advancing to applicationDidEnterBackground or not (in other words, if we're actually going to background or just handling a temporary interruption).Zaslow
A
23

The screenshot is taken immediately after this method returns. I guess your -resetAnimation method completes in the next runloop cycle and not immediately. I've not tried this, but you could try to let the runloop run and then return a little bit later:

- (void) applicationDidEnterBackground:(UIApplication *)application {
    // YOUR CODE HERE

    // Let the runloop run for a brief moment
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.01]];
}

I hope this helps, Fabian


Update: -pauseAnimation and -resetAnimation distinction

Approach: Delay the animation happening in -applicationWillResignActive: and cancel the delayed animation in -applicationDidEnterBackground:

- (void) applicationWillResignActive:(UIApplication *)application {
    // Measure the time between -applicationWillResignActive: and -applicationDidEnterBackground first!
    [self performSelector:@selector(pauseAnimation) withObject:nil afterDelay:0.1];

    // OTHER CODE HERE
}

- (void) applicationDidEnterBackground:(UIApplication *)application {
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(pauseAnimation) object:nil];

    // OTHER CODE HERE
}
Ambagious answered 28/11, 2011 at 17:12 Comment(6)
It would also be helpful to know what you're doing in -resetAnimation.Ambagious
Thanks, I'll try this. Your guess seems educated, and this just might be the case, since I'm using UIAnimateWithDuration (with 0 delay and 0 duration, but a animation method nonetheless) in my resetAnimation method too. I'll update my question shortly with some code snippets.Zaslow
Keiser: your answer might in fact be the key to finding the mistake in my original logic. When solving the double-clicking exit button case, I noticed I only got the ongoing animations to stop only when reassigning the new values to my animatable properties inside another animation call, for some reason.Zaslow
And i just adapted the same style to the background use case, but now that I think about it, there should be no reason to use UIAnimation there at all, just reset the values straight away. This should also eliminate the secondary thread, and probably also give me the correct snapshot image automatically. I'll try just that as soon as I get to my dev comp.Zaslow
Yes, if you use UIViewAnimation you should remove that and only reset your views and the view hierarchy.Ambagious
The problem with the -pauseAnimation and -resetAnimation distinction is still unsolved, though. You should maybe measure the time between -willResignActive and -didEnterBackground when you're leaving the app using the home button and then in -willResignActive fire the animation with that duration as delay (it will most probably be a very short time interval) and cancel the delayed fire in -didEnterBackground. I will edit my answer.Ambagious
Z
3

I've now run some tests, and eliminated the problem, thanks to @Fabian Kreiser.

To conclude: Kreiser had it right: iOS takes the screenshot immediately after the method applicationDidEnterBackground: returns -- immediately meaning, before the end of the current runloop.

What this means is, if you launch any scheduled tasks in the didEnterBackground method you want to finish before leaving, you will have to let the current runloop run for as long as the tasks might take to finish.

In my case, the scheduled task was an UIAnimateWithDuration method call -- I let myself be confused by the fact that both its delay and duration was 0 -- the call was nonetheless scheduled to run in another thread, and thus wasn't able to finish before the end of applicationDidEnterBackground method. Result: the screenshot was indeed taken before the display was updated to the state I wanted -- and, when relaunching, this screenshot flashed briefly onscreen, causing the unwanted flickering.

Furthermore, to provide the "smooth" vs. "instant" transition behavior explained in my question, Kreiser's suggestion to delay the "smooth" transition call in applicationWillResignActive: and cancel the call in applicationDidEnterBackground: works fine. I noticed the delay between the two delegate methods was around 0.005-0.019 seconds in my case, so I applied a generous margin and used a delay of 0.05 seconds.

My bounty, the correct answer tick, and my thanks go to Fabian. Hopefully this helps others in similar situation, too.

Zaslow answered 6/12, 2011 at 19:12 Comment(0)
T
2

The runloop solution actually results in some problems with the app.

[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.01]];

If you go to the background and immediately open the app again, the app will turn into a black screen. When you reopen the app for the second time, everything is back to normal.

A better way is to use

[CATransaction flush]

This forces all current transactions to be immediately applied and does not have the problem resulting in a black screen.

Triform answered 27/2, 2015 at 13:50 Comment(0)
L
1

Depending on how hardcore important it is to you to have this transition run smoothly, you could kill off multi-tasking for your app entirely w/ UIApplicationExitsOnSuspend. Then, you would be guaranteed your Default.png and a clean visual state.

Of course, you'd have to save/restore state on exit/startup, and without more info on the nature of your app, it's tough to say whether this would be worth the trouble.

Lyricism answered 28/11, 2011 at 16:59 Comment(1)
Thanks for the idea, I wasn't aware of this possibility. And yet I'm still hoping there's some 'cleaner' way to achieve this goal.Zaslow
E
1

In iOS 7, there is [[UIApplication sharedApplication] ignoreSnapshotOnNextApplicationLaunch] call that does exactly what you needed.

Entrance answered 15/4, 2014 at 7:41 Comment(1)
Great to know things have gone forward while I've been out the iOS scene!Zaslow

© 2022 - 2024 — McMap. All rights reserved.