Why does viewWillAppear not get called when an app comes back from the background?
Asked Answered
B

7

319

I'm writing an app and I need to change the view if the user is looking at the app while talking on the phone.

I've implemented the following method:

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

But it's not being called when the app returns to the foreground.

I know that I can implement:

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

but I don't want to do this. I'd much rather put all my layout information in the viewWillAppear: method, and let that handle all possible scenarios.

I've even tried to call viewWillAppear: from applicationWillEnterForeground:, but I can't seem to pinpoint which is the current view controller at that point.

Does anybody know the proper way to deal with this? I'm sure I'm missing an obvious solution.

Bearcat answered 11/3, 2011 at 20:25 Comment(5)
You should be using applicationWillEnterForeground: to determine when your application has re-entered the active state.Wallace
I said I was trying that in my question. Please refer above. Can you offer a way to determine which is the current view controller from within the app delegate?Bearcat
You could use isMemberOfClass or isKindOfClass, depending on your needs.Wallace
@sudo rm -rf How would that work then? What is he going to call isKindOfClass on?Eliathas
@occulus: Goodness knows, I was just trying to answer his question. For sure your way of doing it is the way to go.Wallace
E
217

The method viewWillAppear should be taken in the context of what is going on in your own application, and not in the context of your application being placed in the foreground when you switch back to it from another app.

In other words, if someone looks at another application or takes a phone call, then switches back to your app which was earlier on backgrounded, your UIViewController which was already visible when you left your app 'doesn't care' so to speak -- as far as it is concerned, it's never disappeared and it's still visible -- and so viewWillAppear isn't called.

I recommend against calling the viewWillAppear yourself -- it has a specific meaning which you shouldn't subvert! A refactoring you can do to achieve the same effect might be as follows:

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

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

Then also you trigger doMyLayoutStuff from the appropriate notification:

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

There's no out of the box way to tell which is the 'current' UIViewController by the way. But you can find ways around that, e.g. there are delegate methods of UINavigationController for finding out when a UIViewController is presented therein. You could use such a thing to track the latest UIViewController which has been presented.

Update

If you layout out UIs with the appropriate autoresizing masks on the various bits, sometimes you don't even need to deal with the 'manual' laying out of your UI - it just gets dealt with...

Eliathas answered 11/3, 2011 at 20:43 Comment(10)
Thanks for this solution. I actually add the observer for UIApplicationDidBecomeActiveNotification and it works very good.Warfarin
This is certainly the correct answer. Of note, however, in response to "there's no out of the box way to tell which is the 'current' UIViewController", I believe that self.navigationController.topViewControllereffectively provides it, or at least the one on the top of the stack, which would be the current one if this code is is firing on the main thread in a view contoller. (Could be wrong, haven't played with it a lot, but seems to work.)Charmeuse
appDelegate.rootViewController will work too, but it might return a UINavigationController, and then you'll need .topViewController as @MatthewFrederick says.Blackbeard
It seems that UIApplicationDidChangeStatusBarFrameNotification does not work if the status bar isn't visible.Vaunting
UIApplicationDidBecomeActiveNotification is incorrect (despite all the people upvoting it). On App start (and only on app start!) this notification is called differently - it is called in addition to viewWillAppear, so with this answer you'll get it called twice. Apple made it unnecessarily difficult to get this right - the docs are still missing (as of 2013!).Riggins
The solution I came up with was to use a class with a static variable('static BOOL enteredBackground;' then I add class methods setters and getters. In applicationDidEnterBackground, I set the variable to true. Then in applicationDidBecomeActive, I check the static bool, and if it is true, I "doMyLayoutStuff" and reset the variable to 'NO'. This prevents: viewWillAppear with applicationDidBecomeActive collision, and also makes sure that the application doesn't think it entered from background if terminated due to memory pressure.Westney
this works for me (changing object:self to object:nil) [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(doMyLayoutStuff:) name: UIApplicationDidBecomeActiveNotification object:nil];Haberman
@Riggins UIApplicationDidBecomeActiveNotification works for me every time the application enters the foreground. iOS 9Aragonite
@Aragonite - I think you misunderstood Adam's observation: As you observed, it does get called every time; it "works". He is saying to be aware that there is a situation where it ~and~ ViewWillAppear BOTH get called. So if you have logic that it would be a mistake to run twice (once via UIApplicationDidBecomeActiveNotification, a second time via ViewWillAppear), then you need to protect against that - for example, by Martin's approach. (For some people, this is a non-issue. Depends what your code does.)Belie
Thanks for the explanation. I do think this is clunkyness on Apple's part as the view controller obviously should care that it is being re-displayed coming back from a different context, and at a different time. I feel like you can take any silly or buggy behaviour and try to rationalize it as if it should be "expected behaviour". The solution in this case always felt like a work-around more than anything. I've had to deal with this nonsense for quite some time since view controllers often need to be refreshed when a user returns, regardless if it was background, or a different view controllerLermontov
B
254

Swift

Short answer

Use a NotificationCenter observer rather than 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
}

Long answer

To find out when an app comes back from the background, use a NotificationCenter observer rather than viewWillAppear. Here is a sample project that shows which events happen when. (This is an adaptation of this Objective-C answer.)

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")
    }

}

On first starting the app, the output order is:

view did load
view will appear
did become active
view did appear

After pushing the home button and then bringing the app back to the foreground, the output order is:

will enter foreground
did become active 

So if you were originally trying to use viewWillAppear then UIApplication.willEnterForegroundNotification is probably what you want.

Note

As of iOS 9 and later, you don't need to remove the observer. The documentation states:

If your app targets iOS 9.0 and later or macOS 10.11 and later, you don't need to unregister an observer in its dealloc method.

Benedetta answered 30/12, 2015 at 12:27 Comment(1)
In swift 4.2 the notification name is now UIApplication.willEnterForegroundNotification and UIApplication.didBecomeActiveNotificationAntiperiodic
E
217

The method viewWillAppear should be taken in the context of what is going on in your own application, and not in the context of your application being placed in the foreground when you switch back to it from another app.

In other words, if someone looks at another application or takes a phone call, then switches back to your app which was earlier on backgrounded, your UIViewController which was already visible when you left your app 'doesn't care' so to speak -- as far as it is concerned, it's never disappeared and it's still visible -- and so viewWillAppear isn't called.

I recommend against calling the viewWillAppear yourself -- it has a specific meaning which you shouldn't subvert! A refactoring you can do to achieve the same effect might be as follows:

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

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

Then also you trigger doMyLayoutStuff from the appropriate notification:

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

There's no out of the box way to tell which is the 'current' UIViewController by the way. But you can find ways around that, e.g. there are delegate methods of UINavigationController for finding out when a UIViewController is presented therein. You could use such a thing to track the latest UIViewController which has been presented.

Update

If you layout out UIs with the appropriate autoresizing masks on the various bits, sometimes you don't even need to deal with the 'manual' laying out of your UI - it just gets dealt with...

Eliathas answered 11/3, 2011 at 20:43 Comment(10)
Thanks for this solution. I actually add the observer for UIApplicationDidBecomeActiveNotification and it works very good.Warfarin
This is certainly the correct answer. Of note, however, in response to "there's no out of the box way to tell which is the 'current' UIViewController", I believe that self.navigationController.topViewControllereffectively provides it, or at least the one on the top of the stack, which would be the current one if this code is is firing on the main thread in a view contoller. (Could be wrong, haven't played with it a lot, but seems to work.)Charmeuse
appDelegate.rootViewController will work too, but it might return a UINavigationController, and then you'll need .topViewController as @MatthewFrederick says.Blackbeard
It seems that UIApplicationDidChangeStatusBarFrameNotification does not work if the status bar isn't visible.Vaunting
UIApplicationDidBecomeActiveNotification is incorrect (despite all the people upvoting it). On App start (and only on app start!) this notification is called differently - it is called in addition to viewWillAppear, so with this answer you'll get it called twice. Apple made it unnecessarily difficult to get this right - the docs are still missing (as of 2013!).Riggins
The solution I came up with was to use a class with a static variable('static BOOL enteredBackground;' then I add class methods setters and getters. In applicationDidEnterBackground, I set the variable to true. Then in applicationDidBecomeActive, I check the static bool, and if it is true, I "doMyLayoutStuff" and reset the variable to 'NO'. This prevents: viewWillAppear with applicationDidBecomeActive collision, and also makes sure that the application doesn't think it entered from background if terminated due to memory pressure.Westney
this works for me (changing object:self to object:nil) [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(doMyLayoutStuff:) name: UIApplicationDidBecomeActiveNotification object:nil];Haberman
@Riggins UIApplicationDidBecomeActiveNotification works for me every time the application enters the foreground. iOS 9Aragonite
@Aragonite - I think you misunderstood Adam's observation: As you observed, it does get called every time; it "works". He is saying to be aware that there is a situation where it ~and~ ViewWillAppear BOTH get called. So if you have logic that it would be a mistake to run twice (once via UIApplicationDidBecomeActiveNotification, a second time via ViewWillAppear), then you need to protect against that - for example, by Martin's approach. (For some people, this is a non-issue. Depends what your code does.)Belie
Thanks for the explanation. I do think this is clunkyness on Apple's part as the view controller obviously should care that it is being re-displayed coming back from a different context, and at a different time. I feel like you can take any silly or buggy behaviour and try to rationalize it as if it should be "expected behaviour". The solution in this case always felt like a work-around more than anything. I've had to deal with this nonsense for quite some time since view controllers often need to be refreshed when a user returns, regardless if it was background, or a different view controllerLermontov
S
142

Use Notification Center in the viewDidLoad: method of your ViewController to call a method and from there do what you were supposed to do in your viewWillAppear: method. Calling viewWillAppear: directly is not a good option.

- (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");
}
Selfrealization answered 28/8, 2013 at 5:21 Comment(5)
Could be a good idea to remove the observer in dealloc method then.Karyotype
viewDidLoad not the best method to add self as observer, if so, remove observer in viewDidUnloadSuburbia
what is the best method to add self a observer?Metry
Can't the viewcontroller observe for just one notification, i.e., UIApplicationWillEnterForegroundNotification. Why listen to both?Lambaste
You can use either one of them, not required to listen both the notification. I have just shown both the options.Selfrealization
M
37

viewWillAppear:animated:, one of the most confusing methods in the iOS SDKs in my opinion, is never be invoked in such a situation, i.e., application switching. That method is only invoked according to the relationship between the view controller's view and the application's window, i.e., the message is sent to a view controller only if its view appears on the application's window, not on the screen.

When your application goes background, obviously the topmost views of the application window are no longer visible to the user. In your application window's perspective, however, they are still the topmost views and therefore they did not disappear from the window. Rather, those views disappeared because the application window disappeared. They did not disappeared because they disappeared from the window.

Therefore, when the user switches back to your application, they obviously seem to appear on the screen, because the window appears again. But from the window's perspective, they haven't disappeared at all. Therefore the view controllers never get the viewWillAppear:animated message.

Mariel answered 11/3, 2011 at 20:54 Comment(4)
Additionally, -viewWillDisappear:animated: used to be a convenient place to save state since it's called at app exit. It's not called when the app is backgrounded, though, and a backgrounded app can be killed without warning.Hydrozoan
Another really badly named method is viewDidUnload. You'd think it was the opposite of viewDidLoad, but no; it's only called when there was a low memory situation which caused the view to unload, and not every time the view is actually unloaded at dealloc time.Eliathas
I absolutely agree with @occulus. viewWillAppear has its excuse because the (sort of) multitasking was not there, but viewDidUnload definitely could have a better name.Mariel
For me viewDidDisappear IS called when the app is backgrounded on iOS7. Can I get a confirmation?Denotative
A
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
}
Allister answered 15/9, 2019 at 10:24 Comment(0)
B
4

Just trying to make it as easy as possible see code below:

- (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");


}
Briefcase answered 17/7, 2015 at 3:55 Comment(0)
B
1

It's even easier with 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!")
    }   
}
Baseball answered 9/7, 2020 at 11:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.