¿Por qué no se llama a viewWillAppear cuando una aplicación vuelve del fondo?

280

Estoy escribiendo una aplicación y necesito cambiar la vista si el usuario está mirando la aplicación mientras habla por teléfono.

He implementado el siguiente método:

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

Pero no se llama cuando la aplicación vuelve al primer plano.

Sé que puedo implementar:

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

Pero no quiero hacer esto. Prefiero poner toda mi información de diseño en el método viewWillAppear: y dejar que maneje todos los escenarios posibles.

Incluso he intentado llamar a viewWillAppear: desde applicationWillEnterForeground :, pero parece que no puedo determinar cuál es el controlador de vista actual en ese punto.

¿Alguien sabe la forma correcta de lidiar con esto? Estoy seguro de que me falta una solución obvia.

Philip Walton
fuente
1
Debe usar applicationWillEnterForeground:para determinar cuándo su aplicación ha vuelto a ingresar al estado activo.
sudo rm -rf
Dije que estaba intentando eso en mi pregunta. Por favor, consulte más arriba. ¿Puede ofrecer una manera de determinar cuál es el controlador de vista actual desde el delegado de la aplicación?
Philip Walton
Puede usar isMemberOfClasso isKindOfClass, según sus necesidades.
sudo rm -rf
@sudo rm -rf ¿Cómo funcionaría eso entonces? ¿A qué va a llamar isKindOfClass?
oculto
@occulus: Dios sabe, solo estaba tratando de responder a su pregunta. Seguro que tu forma de hacerlo es el camino a seguir.
sudo rm -rf

Respuestas:

202

El método viewWillAppeardebe tomarse en el contexto de lo que está sucediendo en su propia aplicación, y no en el contexto de que su aplicación se coloque en primer plano cuando vuelva a cambiarla desde otra aplicación.

En otras palabras, si alguien mira otra aplicación o atiende una llamada telefónica, luego vuelve a su aplicación que estaba en segundo plano, su UIViewController que ya estaba visible cuando dejó su aplicación 'no le importa', por así decirlo: en lo que a él respecta, nunca ha desaparecido y sigue siendo visible, y por eso viewWillAppearno se llama.

Recomiendo no llamar a viewWillAppearti mismo, ¡tiene un significado específico que no debes subvertir! Una refactorización que puede hacer para lograr el mismo efecto podría ser la siguiente:

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

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

Entonces también se activa doMyLayoutStuffdesde la notificación correspondiente:

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

Por cierto, no hay una forma innovadora de saber cuál es el UIViewController 'actual'. Pero puede encontrar formas de evitarlo, por ejemplo, existen métodos delegados de UINavigationController para averiguar cuándo se presenta un UIViewController. Podría usar tal cosa para rastrear el último UIViewController que se ha presentado.

Actualizar

Si diseña interfaces de usuario con las máscaras de tamaño automático adecuadas en los diversos bits, a veces ni siquiera necesita lidiar con el diseño 'manual' de su interfaz de usuario; simplemente se trata ...

oculto
fuente
101
Gracias por esta solución De hecho, agrego el observador para UIApplicationDidBecomeActiveNotification y funciona muy bien.
Wayne Liu el
2
Esta es ciertamente la respuesta correcta. Sin embargo, cabe destacar que en respuesta a "no hay una forma inmediata de saber cuál es el" UIViewController "actual", creo que self.navigationController.topViewControllerefectivamente lo proporciona, o al menos el que está en la parte superior de la pila, que sería el uno actual si este código se está activando en el hilo principal en un controlador de vista. (Podría estar equivocado, no he jugado mucho con él, pero parece funcionar.)
Matthew Frederick
appDelegate.rootViewControllerfuncionará también, pero podría devolver un UINavigationController, y luego necesitará .topViewControllercomo dice @MatthewFrederick.
samson
77
UIApplicationDidBecomeActiveNotification es incorrecto (a pesar de todas las personas que lo votaron). En el inicio de la aplicación (¡y solo en el inicio de la aplicación!), Esta notificación se llama de manera diferente : se llama además de viewWillAppear, por lo que con esta respuesta la llamarán dos veces. Apple hizo innecesariamente difícil hacer esto bien: todavía faltan documentos (¡a partir de 2013!).
Adam
1
La solución que se me ocurrió fue utilizar una clase con una variable estática ('static BOOL enterBackground;' y luego agrego setters y getters de métodos de clase. En applicationDidEnterBackground, configuro la variable en true. Luego, en applicationDidBecomeActive, verifico el bool estático , y si es cierto, "doMyLayoutStuff" y restablezco la variable a 'NO'. Esto evita: viewWillAppear with applicationDidBecomeActive collision, y también se asegura de que la aplicación no piense que ingresó desde el fondo si finaliza debido a la presión de la memoria.
vejmartin
197

Rápido

Respuesta corta

Use un NotificationCenterobservador en lugar de 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
}

Respuesta larga

Para saber cuándo una aplicación vuelve del fondo, use un NotificationCenterobservador en lugar de viewWillAppear. Aquí hay un proyecto de muestra que muestra qué eventos ocurren cuando. (Esta es una adaptación de esta respuesta del Objetivo 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")
    }

}

Al iniciar la aplicación por primera vez, el orden de salida es:

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

Después de presionar el botón de inicio y luego volver a poner la aplicación en primer plano, el orden de salida es:

will enter foreground
did become active 

Entonces, si originalmente estaba tratando de usar, viewWillAppearentonces UIApplication.willEnterForegroundNotificationprobablemente sea lo que desea.

Nota

A partir de iOS 9 y versiones posteriores, no es necesario eliminar el observador. La documentación dice:

Si su aplicación se dirige a iOS 9.0 y posterior o macOS 10.11 y posterior, no necesita cancelar el registro de un observador en su deallocmétodo.

Suragch
fuente
66
En swift 4.2, el nombre de la notificación ahora es UIApplication.willEnterForegroundNotification y UIApplication.didBecomeActiveNotification
hordurh
140

Use el Centro de notificaciones en el viewDidLoad:método de su ViewController para llamar a un método y desde allí haga lo que se suponía que debía hacer en su viewWillAppear:método. Llamar viewWillAppear:directamente no es una buena opción.

- (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");
}
Manju
fuente
9
Podría ser una buena idea eliminar al observador en el deallocmétodo entonces.
AncAinu
2
viewDidLoad no es el mejor método para agregarse a sí mismo como observador, si es así, elimine al observador en viewDidUnload
Injectios
¿Cuál es el mejor método para agregar un observador?
Piotr Wasilewicz
El controlador de vista no puede observar solo una notificación, es decir, UIApplicationWillEnterForegroundNotification. ¿Por qué escuchar a ambos?
zulkarnain shah
34

viewWillAppear:animated:, uno de los métodos más confusos en los SDK de iOS en mi opinión, nunca se invoca en tal situación, es decir, el cambio de aplicación. Ese método solo se invoca de acuerdo con la relación entre la vista del controlador de vista y la ventana de la aplicación , es decir, el mensaje se envía a un controlador de vista solo si su vista aparece en la ventana de la aplicación, no en la pantalla.

Cuando su aplicación pasa a segundo plano, obviamente, las vistas superiores de la ventana de la aplicación ya no son visibles para el usuario. En la perspectiva de la ventana de su aplicación, sin embargo, siguen siendo las vistas más altas y, por lo tanto, no desaparecieron de la ventana. Más bien, esas vistas desaparecieron porque la ventana de la aplicación desapareció. No desaparecieron porque desaparecieron de la ventana.

Por lo tanto, cuando el usuario vuelve a su aplicación, obviamente parece aparecer en la pantalla, porque la ventana aparece nuevamente. Pero desde la perspectiva de la ventana, no han desaparecido en absoluto. Por lo tanto, los controladores de vista nunca reciben el viewWillAppear:animatedmensaje.

MHC
fuente
2
Además, -viewWillDisappear: animated: solía ser un lugar conveniente para guardar el estado ya que se llama al salir de la aplicación. Sin embargo, no se llama cuando la aplicación está en segundo plano, y una aplicación en segundo plano puede ser eliminada sin previo aviso.
tc.
66
Otro método realmente mal nombrado es viewDidUnload. Se podría pensar que era lo contrario de viewDidLoad, pero no; solo se llama cuando hay una situación de poca memoria que hace que la vista se descargue, y no cada vez que la vista se descarga realmente en el momento de la transacción.
occulus
Estoy totalmente de acuerdo con @occulus. viewWillAppear tiene su excusa porque la (clase de) multitarea no estaba allí, pero viewDidUnload definitivamente podría tener un mejor nombre.
MHC
Para mí, se llama a viewDidDisappear cuando la aplicación está en segundo plano en iOS7. ¿Puedo obtener una confirmación?
Mike Kogan
4

Swift 4.2 / 5

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

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

Solo tratando de hacerlo lo más fácil posible, vea el código a continuación:

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


}
Ciervo confundido
fuente