iOS进入后台时,何时精确地进行视图快照?

23

当我通过按下退出按钮将我的iPhone应用程序放到后台,然后通过在主屏幕上点击启动图标重新启动时,我遇到了一个问题:应用程序的视图确实返回到了其初始状态,但在此之前,它会短暂地在屏幕上闪现先前错误的视图状态。

背景

我的主视图基本上由一系列互相关联的UIAnimateWithDuration调用组成。无论发生任何中断,我想要的行为都是将动画重置为其初始状态(除非所有动画都已完成并且应用程序已进入静态最终阶段),并在应用程序返回活动和可见状态时从那里重新开始。

经过研究后,我得知需要两种类型的中断处理代码以提供良好的用户体验:“即时”和“平滑”。我有一个名为resetAnimation的方法,可以立即将视图属性重置为初始状态,并且有一个名为pauseAnimation的方法,可以快速地动画到相同状态,并在视图顶部淡入一个额外的标签,显示“暂停”。

双击退出按钮

原因是“双击退出按钮”用例实际上并没有隐藏您的视图或将您置于后台状态,而只是向上滚动一点,以显示底部的多任务菜单。因此,在这种情况下立即重置视图状态看起来非常丑陋。动画转换并告诉用户你已经暂停似乎是一个更好的想法。

通过在我的应用程序委托中实现applicationWillResignActive委托方法并从那里调用pauseAnimation,这种情况可以很好地平稳工作。我通过实现applicationDidBecomeActive委托方法来处理从多任务菜单返回,然后从那里调用我的resumeAnimation方法,如果存在“暂停”标签,则将其淡出,并从初始状态开始我的动画序列。

这一切都很好,没有任何闪烁。

访问翻转面板

我的应用程序基于Xcode的“utility”模板构建,因此它有一个翻转视图来显示信息/设置。我通过在我的主视图控制器中实现这两个委托方法来处理访问翻转面板和返回到主视图:

  • (void)viewDidDisappear:(BOOL)animated

  • (void)viewDidAppear:(BOOL)animated

我在viewDidDisappear方法中调用resetAnimation,在viewDidAppear中调用resumeAnimation。这一切都很好,主视图从可见状态的开始处处于其初始状态——没有任何意外闪烁错误的动画状态等。但:

按退出按钮,然后从我的应用程序图标重新启动(有问题的部分!)

问题从这里开始。当我点击退出按钮一次并且我的应用程序开始转换到后台时,会发生两件事情。首先,applicationWillResignActive也在这里被调用,因此我的pauseAnimation方法也会启动。虽然这里的过渡不需要平滑 - 视图只是静态的,“缩小”以显示主屏幕 - 但是它仍然需要进行操作。好吧,在系统拍摄视图的确切时刻之前,如果我能够调用resetAnimation,那么也不会有任何伤害。

无论如何,其次,AppDelegate中的applicationDidEnterBackground被调用。我尝试从那里调用resetAnimation,以便当应用返回时视图处于正确状态,但是这似乎行不通。似乎已经拍摄了“快照”,所以当我点击我的应用程序启动图标并重新启动时,错误的视图状态会在正确的初始状态显示之前短暂地闪烁。之后,它运行得很好,动画就像它们应该做的那样进行,但是那个重新启动时的难看闪烁无论我尝试什么都无法消失。

基本上,我想知道的是,系统在什么时候拍摄这个快照?因此,为拍摄“纪念照”准备我的视图的正确委托方法或通知处理程序是什么?

附注:然后还有default.png,它似乎不仅在首次启动时显示,而且每当处理器出现困难或返回应用程序稍有延迟时都会显示。如果你正在返回到完全不同于默认视图的反面视图,这有点丑陋。但这是iOS的核心功能,我猜想我甚至不应该尝试弄清楚或控制它 :)


编辑:由于人们要求实际代码,并且在我提出这个问题后我的应用程序已经发布,因此我将在此处发布一些代码。(该应用程序名为Sweetest Kid,如果您想看看它是如何工作的,可以在此处找到:http://itunes.apple.com/app/sweetest-kid/id476637106?mt=8

这是我的pauseAnimation方法-resetAnimation几乎相同,除了其动画调用具有零持续时间和延迟,并且不显示“暂停”标签。我使用UIAnimation重置值而不仅仅分配新值的一个原因是,如果我不使用UIAnimation,则某种原因动画就不会停止。无论如何,这是pauseAnimation方法:

    - (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
    }
}

不幸的是,我敢打赌苹果不希望你知道/依赖于照片拍摄的时间。他们可能会在未来改变这一点。对此表示赞同。在某些情况下,他们确实有选项允许加载某个特定的默认图像(例如在接收推送通知时),但如果你能拥有更多的控制权,那将会更好。 - David Hodge
我也遇到过这种问题。https://dev59.com/hVPTa4cB1Zd3GeqPh0zA 很想看到解决方案。 - Daniel Schneller
为什么当用户点击一次按钮退出到Springboard时,不能使用已经在多任务托盘上工作的暂停/恢复机制(即您所谓的“退出”按钮的两次点击)? - Jon Grant
@JonGrant:因为pauseAnimation是一个持续0.3秒的动画,而且它永远无法完成,因为点击退出按钮一次只会立即停止应用程序的前台进程。此外,需要从applicationWillResignActive调用pauseAnimation以获得所需的效果,在那个时候没有办法找出应用程序是否将进入applicationDidEnterBackground(换句话说,如果我们实际上正在后台运行或仅处理临时中断)。 - Samuli Viitasaari
5个回答

23

此方法返回后立即截取屏幕。我猜想您的-resetAnimation方法会在下一个运行循环周期完成而不是立即完成。我没有尝试过这个方法,但您可以尝试让运行循环运行一段时间,然后稍后再返回:

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

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

希望这可以帮到你, Fabian


更新:-pauseAnimation和-resetAnimation区别

方案:在-applicationWillResignActive:中延迟动画发生,并在-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
}

在-resetAnimation中了解你正在做什么也会很有帮助。 - Fabian Kreiser
谢谢,我会尝试这个方法。你的猜测似乎很有道理,因为在我的resetAnimation方法中,我也使用了UIAnimateWithDuration(没有延迟和持续时间,但仍然是一个动画方法)。我很快就会更新我的问题,并附上一些代码片段。 - Samuli Viitasaari
Keiser: 你的回答可能实际上是找到我原始逻辑错误的关键。在解决双击退出按钮的情况时,我注意到只有在另一个动画调用内重新分配新值给我的可动画属性时,我才能停止正在进行的动画,出现了某些问题。 - Samuli Viitasaari
我只是将相同的风格适应到了背景使用案例中,但现在我想,根本没有理由在那里使用UIAnimation,直接重置值就可以了。这也应该消除次要线程,并可能自动给我正确的快照图像。一旦我回到我的开发计算机,我会立即尝试。 - Samuli Viitasaari
是的,如果您使用UIViewAnimation,则应将其删除,并仅重置视图及其视图层次结构。 - Fabian Kreiser
“-pauseAnimation”和“-resetAnimation”的区别问题仍未解决。当您使用主页按钮离开应用程序时,您可以在“-willResignActive”和“-didEnterBackground”之间测量时间,然后在“-willResignActive”中以该持续时间作为延迟触发动画(它很可能是非常短的时间间隔),并在“-didEnterBackground”中取消延迟触发。我会编辑我的答案。 - Fabian Kreiser

3

我现在已经进行了一些测试,并且通过@Fabian Kreiser的帮助解决了问题。

总之:Kreiser是正确的:iOS会在方法applicationDidEnterBackground:返回后立即截屏——立即意味着在当前runloop结束之前。

这意味着,如果您在didEnterBackground方法中启动任何计划任务,您需要让当前runloop运行尽可能长的时间,以便任务能够完成。

在我的情况下,计划任务是UIAnimateWithDuration方法调用——我被它的延迟和持续时间都为0所困惑——但是,该调用仍然被计划在另一个线程中运行,因此无法在applicationDidEnterBackground方法结束之前完成。结果:截图确实是在显示更新到我想要的状态之前拍摄的——当重新启动时,这个截图会短暂地闪烁在屏幕上,导致不必要的闪烁。

此外,为了提供我在问题中解释的“平滑”与“即时”的过渡行为,Kreiser建议在applicationWillResignActive:中延迟“平滑”过渡调用,并在applicationDidEnterBackground:中取消调用,这很有效。我注意到两个委托方法之间的延迟在我的情况下约为0.005-0.019秒,因此我应用了一个慷慨的余量,并使用了0.05秒的延迟。

我的奖励、正确答案和感谢都归功于Fabian。希望这也能帮助其他处于类似情况的人。


2
运行循环解决方案实际上会导致应用程序出现一些问题。
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.01]];

如果您在应用程序后台打开应用程序,则应用程序会变成黑屏。当您第二次重新打开应用程序时,一切都恢复正常。
更好的方法是使用:
[CATransaction flush]

这将强制立即应用所有当前的事务,而且不会出现导致黑屏的问题。


1
在iOS 7中,有一个[[UIApplication sharedApplication] ignoreSnapshotOnNextApplicationLaunch]的调用,正好可以满足您的需求。

很高兴知道在我离开iOS领域的这段时间里事情有所进展! - Samuli Viitasaari

1

根据您对此过渡顺利实现的重要性有多强烈,您可以使用UIApplicationExitsOnSuspend完全终止应用程序的多任务处理。然后,您将保证获得您的Default.png和清洁的可视状态。

当然,您需要在退出/启动时保存/恢复状态,而且没有关于您的应用程序性质的更多信息,很难说这是否值得麻烦。


谢谢你的想法,我之前不知道这个可能性。但我仍然希望有一种更“简洁”的方法来实现这个目标。 - Samuli Viitasaari

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接