为什么当一个应用从后台返回时,viewWillAppear不会被调用?

318

我正在写一个应用程序,如果用户在打电话时看着应用程序,我需要改变视图。

我已经实现了以下方法:

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    NSLog(@"viewWillAppear:");
    _sv.frame = CGRectMake(0.0, 0.0, 320.0, self.view.bounds.size.height);
}

但是当应用程序返回前台时,它并没有被调用。

我知道我可以实现:

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(statusBarFrameChanged:) name:UIApplicationDidChangeStatusBarFrameNotification object:nil];

但我不想这样做。我更愿意将所有的布局信息放在viewWillAppear:方法中,并让它处理所有可能的情况。

我甚至尝试从applicationWillEnterForeground:调用viewWillAppear:,但似乎无法确定当前的视图控制器是哪个。

有人知道正确的处理方法吗?我相信我错过了一个明显的解决方案。


1
你应该使用applicationWillEnterForeground:来确定你的应用程序何时重新进入活动状态。 - sudo rm -rf
我在我的问题中说过我正在尝试那个。请参考上面的内容。你能提供一种方法来确定当前视图控制器吗? - Philip Walton
根据您的需求,您可以使用 isMemberOfClassisKindOfClass - sudo rm -rf
@sudo rm -rf 这样会怎么样呢?他要在哪里调用isKindOfClass方法? - occulus
@occulus:天晓得,我只是在试图回答他的问题。毫无疑问,你的方法才是正确的。 - sudo rm -rf
7个回答

254

Swift

简短回答

使用NotificationCenter观察者而不是viewWillAppear

override func viewDidLoad() {
    super.viewDidLoad()

    // set observer for UIApplication.willEnterForegroundNotification
    NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)

}

// my selector that was defined above
@objc func willEnterForeground() {
    // do stuff
}

长回答

要找出应用程序何时从后台返回,请使用 NotificationCenter 观察者,而不是 viewWillAppear。这是一个演示事件发生时间的示例项目。(这是此 Objective-C 回答的改编版本。)

import UIKit
class ViewController: UIViewController {

    // MARK: - Overrides

    override func viewDidLoad() {
        super.viewDidLoad()
        print("view did load")

        // add notification observers
        NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)

    }

    override func viewWillAppear(_ animated: Bool) {
        print("view will appear")
    }

    override func viewDidAppear(_ animated: Bool) {
        print("view did appear")
    }

    // MARK: - Notification oberserver methods

    @objc func didBecomeActive() {
        print("did become active")
    }

    @objc func willEnterForeground() {
        print("will enter foreground")
    }

}

第一次启动应用程序时,输出顺序为:

view did load
view will appear
did become active
view did appear
在按下主页按钮然后将应用程序带回到前台后,输出顺序为:

在按下主页按钮然后将应用程序带回到前台后,输出顺序为:

will enter foreground
did become active 

如果你最初尝试使用 viewWillAppear ,那么 UIApplication.willEnterForegroundNotification 可能是你想要的。

注意

从 iOS 9 开始,你不需要删除观察者。 文档 表明:

如果你的应用程序针对 iOS 9.0 及更高版本或 macOS 10.11 及更高版本,则无需在其 dealloc 方法中注销观察者。


7
在Swift 4.2中,通知名称现在是UIApplication.willEnterForegroundNotification和UIApplication.didBecomeActiveNotification。 - hordurh

217

方法viewWillAppear应该在您自己的应用程序中进行,而不是在从另一个应用程序切换回来时将您的应用程序放在前台的情况下进行。

换句话说,如果有人查看另一个应用程序或接听电话,然后切换回到您的应用程序,而您的UIViewController在您离开应用程序时已经可见,“它”就不会关心--就其而言,它从未消失过并且仍然可见--因此不会调用viewWillAppear

我建议不要自己调用viewWillAppear--它具有特定的含义,您不应该篡改它!为了实现相同的效果,可以进行以下重构:

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    [self doMyLayoutStuff:self];
}

- (void)doMyLayoutStuff:(id)sender {
    // stuff
}

然后你还可以从适当的通知中触发doMyLayoutStuff

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(doMyLayoutStuff:) name:UIApplicationDidChangeStatusBarFrameNotification object:self];

没有现成的方法可以告诉你哪个UIViewController是“当前”的。但你可以找到绕开这个问题的方法,例如UINavigationController有一些委托方法,可以找出在其中呈现的UIViewController。你可以使用这样的方法来跟踪最新显示的UIViewController。

更新

如果您使用适当的自动调整大小掩码布局UI元素,有时您甚至不需要处理“手动”布局UI - 它会自动处理...


105
谢谢这个解决方案。我实际上为UIApplicationDidBecomeActiveNotification添加了观察者,它非常有效。 - Wayne Liu
2
这肯定是正确的答案。需要注意的是,在回答“没有现成的方法来确定哪个 UIViewController 是‘当前’的”时,我相信 self.navigationController.topViewController 能有效提供它,或者至少提供堆栈顶部的视图控制器,如果这段代码在主线程中的一个视图控制器上运行,则它会是当前的一个。(可能是错的,没怎么研究过,但似乎可行。) - Matthew Frederick
7
尽管有很多人给它点赞,但UIApplicationDidBecomeActiveNotification名称不正确。只有在应用程序启动时才会(仅仅在应用程序启动时!)调用此通知 - 它与viewWillAppear一起被调用,因此使用此答案后将调用它两次。苹果公司使得正确操作变得毫无必要地困难 - 至少到2013年为止,文档仍然没有涵盖这个问题。 - Adam
1
我想出的解决方案是使用一个带有静态变量的类('static BOOL enteredBackground;'),然后添加类方法setters和getters。在applicationDidEnterBackground中,将变量设置为true。然后在applicationDidBecomeActive中,检查静态布尔值,如果为真,则“doMyLayoutStuff”并将变量重置为'NO'。这可以避免:viewWillAppear与applicationDidBecomeActive冲突,并确保应用程序不会认为它是从后台进入的,如果由于内存压力而终止。 - martin
1
谢谢解释。我认为这是苹果的笨拙之处,因为视图控制器显然应该关心它在从不同上下文和不同时间返回时被重新显示的情况。我觉得你可以将任何愚蠢或有错误的行为都尝试合理化,就好像它应该是“预期的行为”一样。在这种情况下,解决方案总感觉像是一个变通办法,而不是其他什么。自从视图控制器经常需要在用户返回时刷新,无论是后台还是不同的视图控制器,我就不得不处理这些无聊的东西了。 - nenchev
显示剩余5条评论

142

在您的ViewController的viewDidLoad:方法中使用通知中心来调用一个方法,然后从那里执行您原本应该在viewWillAppear:方法中执行的操作。直接调用viewWillAppear:不是一个好选择。

- (void)viewDidLoad
{
    [super viewDidLoad];
    NSLog(@"view did load");

    [[NSNotificationCenter defaultCenter] addObserver:self 
        selector:@selector(applicationIsActive:) 
        name:UIApplicationDidBecomeActiveNotification 
        object:nil];

    [[NSNotificationCenter defaultCenter] addObserver:self 
        selector:@selector(applicationEnteredForeground:) 
        name:UIApplicationWillEnterForegroundNotification
        object:nil];
}

- (void)applicationIsActive:(NSNotification *)notification {
    NSLog(@"Application Did Become Active");
}

- (void)applicationEnteredForeground:(NSNotification *)notification {
    NSLog(@"Application Entered Foreground");
}

9
可以考虑在dealloc方法中移除观察者,这可能是个不错的主意。 - Tancrede Chazallet
2
viewDidLoad 不是添加 self 作为观察者的最佳方法,如果是这样,请在 viewDidUnload 中删除观察者。 - Injectios
什么是添加自我观察者的最佳方法? - Piotr Wasilewicz
视图控制器不能只观察一个通知,即UIApplicationWillEnterForegroundNotification吗?为什么要同时监听两个通知呢? - zulkarnain shah
你可以使用其中任意一个,不需要同时监听两个通知。我只是展示了这两个选项。 - Manju

37

viewWillAppear:animated: 是我认为 iOS SDK 中最令人困惑的方法之一。在应用程序切换时,该方法永远不会被调用。该方法只在视图控制器的视图和 应用程序窗口 之间的关系下被调用,即只有当其视图出现在应用程序窗口上而非屏幕上时,消息才会发送给视图控制器。

当您的应用程序进入后台时,显然应用程序窗口中的最上方视图对用户不再可见。但是从应用程序窗口的角度来看,它们仍然是最上方的视图,因此它们并没有从窗口中消失。相反,这些视图消失了,因为应用程序窗口消失了,而不是因为它们从窗口中消失了。

因此,当用户切换回您的应用程序时,它们显然似乎出现在屏幕上,因为窗口再次出现了。但从窗口的角度来看,它们根本没有消失。因此,视图控制器永远不会收到 viewWillAppear:animated 消息。


3
另外,-viewWillDisappear:animated: 曾经是保存状态的便捷位置,因为它会在应用程序退出时被调用。然而,当应用程序进入后台时,它不会被调用,而且后台应用程序有可能在没有警告的情况下被终止。 - tc.
7
另一个命名不佳的方法是viewDidUnload。你可能会认为它是viewDidLoad的相反,但实际上并不是;它只有在出现低内存情况导致视图卸载时才被调用,并不是每次视图在dealloc时间实际卸载时都调用。 - occulus
1
我完全同意@occulus的观点。viewWillAppear有其存在的理由,因为(某种程度上的)多任务并不存在,但viewDidUnload肯定可以有一个更好的名称。 - MHC
1
对我来说,在iOS7上,当应用程序进入后台时,确实会调用viewDidDisappear方法。我能得到确认吗? - Mike Kogan

8

Swift 4.2 / 5

override func viewDidLoad() {
    super.viewDidLoad()
    NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground),
                                           name: Notification.Name.UIApplication.willEnterForegroundNotification,
                                           object: nil)
}

@objc func willEnterForeground() {
   // do what's needed
}

4

只是想尽可能地简化操作,请参见下面的代码:

- (void)viewDidLoad
{
   [self appWillEnterForeground]; //register For Application Will enterForeground
}


- (id)appWillEnterForeground{ //Application will enter foreground.

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(allFunctions)
                                                 name:UIApplicationWillEnterForegroundNotification
                                               object:nil];
    return self;
}


-(void) allFunctions{ //call any functions that need to be run when application will enter foreground 
    NSLog(@"calling all functions...application just came back from foreground");


}

1

使用SwiftUI更加简单:

var body: some View {     
    Text("Hello World")
    .onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { _ in
        print("Moving to background!")
    }
    .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
        print("Moving back to foreground!")
    }   
}

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