Por que o viewWillAppear não é chamado quando um aplicativo volta em segundo plano?

279

Estou escrevendo um aplicativo e preciso alterar a exibição se o usuário estiver olhando para o aplicativo enquanto estiver falando ao telefone.

Eu implementei o seguinte 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);
}

Mas não está sendo chamado quando o aplicativo retorna ao primeiro plano.

Eu sei que posso implementar:

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

mas eu não quero fazer isso. Prefiro colocar todas as minhas informações de layout no método viewWillAppear: e deixar que isso lide com todos os cenários possíveis.

Eu até tentei chamar viewWillAppear: from applicationWillEnterForeground :, mas não consigo identificar qual é o controlador de exibição atual naquele momento.

Alguém sabe a maneira correta de lidar com isso? Tenho certeza de que estou perdendo uma solução óbvia.

Philip Walton
fonte
1
Você deve usar applicationWillEnterForeground:para determinar quando seu aplicativo voltou a entrar no estado ativo.
Sudo rm -rf
Eu disse que estava tentando isso na minha pergunta. Consulte acima. Você pode oferecer uma maneira de determinar qual é o controlador de exibição atual de dentro do delegado do aplicativo?
Philip Walton
Você pode usar isMemberOfClassou isKindOfClass, dependendo de suas necessidades.
Sudo rm -rf
@sudo rm -rf Como isso funcionaria então? O que ele vai chamar de isKindOfClass?
Occulus
@ occulus: Deus sabe, eu estava apenas tentando responder à sua pergunta. Com certeza a sua maneira de fazê-lo é o caminho a percorrer.
Sudo rm -rf 14/03

Respostas:

202

O método viewWillAppeardeve ser adotado no contexto do que está acontecendo em seu próprio aplicativo, e não no contexto de seu aplicativo ser colocado em primeiro plano quando você voltar para outro aplicativo.

Em outras palavras, se alguém olhar para outro aplicativo ou receber uma ligação telefônica, voltará para o seu aplicativo que estava em segundo plano, o seu UIViewController que já estava visível quando você saiu do aplicativo 'não se importa', por assim dizer - no que diz respeito, nunca desapareceu e ainda é visível - e assim viewWillAppearnão é chamado.

Eu recomendo não ligar para viewWillAppearvocê mesmo - ele tem um significado específico que você não deve subverter! Uma refatoração que você pode fazer para obter o mesmo efeito pode ser a seguinte:

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

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

Em seguida, você também aciona a doMyLayoutStuffpartir da notificação apropriada:

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

Não existe uma maneira imediata de saber qual é o UIViewController 'atual' a propósito. Mas você pode encontrar maneiras de contornar isso, por exemplo, existem métodos delegados do UINavigationController para descobrir quando um UIViewController é apresentado nele. Você pode usar isso para rastrear o UIViewController mais recente que foi apresentado.

Atualizar

Se você criar layouts de interface do usuário com as máscaras de redimensionamento apropriadas nos vários bits, às vezes nem precisará lidar com o layout 'manual' da sua interface do usuário - ele é tratado apenas ...

occulus
fonte
101
Obrigado por esta solução. Na verdade, eu adiciono o observador para UIApplicationDidBecomeActiveNotification e funciona muito bem.
Wayne Liu
2
Esta é certamente a resposta correta. De notar, no entanto, em resposta a "não existe uma maneira imediata de saber qual é o UIViewController 'atual'", acredito que self.navigationController.topViewControllero fornece efetivamente, ou pelo menos aquele no topo da pilha, que seria o atual se esse código estiver disparando no thread principal em um modo de exibição de contoller. (Poderia ser errado, não tenho jogado com ele um monte, mas parece funcionar.)
Matthew Frederick
appDelegate.rootViewControllerfuncionará também, mas pode retornar um UINavigationControllere, em seguida, você precisará .topViewControllercomo @MatthewFrederick diz.
21413
7
UIApplicationDidBecomeActiveNotification está incorreto (apesar de todas as pessoas que fizeram o voto positivo). No início do aplicativo (e apenas no início do aplicativo!), Essa notificação é chamada de maneira diferente - além de viewWillAppear, assim, com esta resposta, você receberá a chamada duas vezes. A Apple tornou desnecessariamente difícil acertar isso - os documentos ainda estão faltando (a partir de 2013!).
Adam
1
A solução que encontrei foi usar uma classe com uma variável estática ('estático BOOL enterBackground;' e depois adicionar métodos setters e getters. No applicationDidEnterBackground, defino a variável como true. Em applicationDidBecomeActive, verifico o bool estático e, se for verdade, eu "doMyLayoutStuff" e redefino a variável para 'NO'. Isso evita: viewWillAppear com colisão applicationDidBecomeActive e também garante que o aplicativo não pense que ele entrou em segundo plano se tiver terminado devido à pressão da memória.
vejmartin
196

Rápido

Resposta curta

Use um NotificationCenterobservador em vez 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
}

Resposta longa

Para descobrir quando um aplicativo volta dos bastidores, use um NotificationCenterobservador em vez de viewWillAppear. Aqui está um projeto de amostra que mostra quais eventos acontecem quando. (Esta é uma adaptação desta resposta do 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")
    }

}

Ao iniciar o aplicativo, a ordem de saída é:

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

Depois de pressionar o botão home e, em seguida, trazer o aplicativo de volta ao primeiro plano, a ordem de saída é:

will enter foreground
did become active 

Portanto, se você estava originalmente tentando usar viewWillAppear, UIApplication.willEnterForegroundNotificationprovavelmente é o que deseja.

Nota

A partir do iOS 9 e posterior, você não precisa remover o observador. A documentação declara:

Se seu aplicativo atingir o iOS 9.0 e posterior ou o macOS 10.11 e posterior, não será necessário cancelar o registro de um observador no deallocmétodo.

Suragch
fonte
6
No swift 4.2, o nome da notificação agora é UIApplication.willEnterForegroundNotification e UIApplication.didBecomeActiveNotification
hordurh 15/18
140

Use o Notification Center no viewDidLoad:método do seu ViewController para chamar um método e, a partir daí, faça o que você deveria fazer no seu viewWillAppear:método. Ligar viewWillAppear:diretamente não é uma boa opção.

- (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
fonte
9
Poderia ser uma boa idéia remover o observador no deallocmétodo então.
AncAinu
2
viewDidLoad não é o melhor método para adicionar eu como observador, em caso afirmativo, remova observador em viewDidUnload
Injectios
qual é o melhor método para adicionar um auto-observador?
Piotr Wasilewicz 26/09
O viewcontroller não pode observar apenas uma notificação, ou seja, UIApplicationWillEnterForegroundNotification. Por que ouvir os dois?
Zulkarnain shah
34

viewWillAppear:animated:, um dos métodos mais confusos nos SDKs do iOS, na minha opinião, nunca é invocado em tal situação, ou seja, na alternância de aplicativos. Esse método é chamado apenas de acordo com a relação entre a visualização do controlador de visualização e a janela do aplicativo , ou seja, a mensagem é enviada para um controlador de visualização apenas se sua visualização aparecer na janela do aplicativo e não na tela.

Quando seu aplicativo fica em segundo plano, obviamente as vistas mais altas da janela do aplicativo não são mais visíveis para o usuário. Na perspectiva da janela do aplicativo, no entanto, elas ainda são as vistas superiores e, portanto, não desapareceram da janela. Em vez disso, essas visualizações desapareceram porque a janela do aplicativo desapareceu. Eles não desapareceram porque desapareceram da janela.

Portanto, quando o usuário volta para o seu aplicativo, ele obviamente aparece na tela, porque a janela aparece novamente. Mas da perspectiva da janela, eles não desapareceram. Portanto, os controladores de exibição nunca recebem a viewWillAppear:animatedmensagem.

MHC
fonte
2
Além disso, -viewWillDisappear: animated: costumava ser um local conveniente para salvar o estado, pois é chamado na saída do aplicativo. Porém, não é chamado quando o aplicativo está em segundo plano, e um aplicativo em segundo plano pode ser eliminado sem aviso.
tc.
6
Outro método realmente mal nomeado é o viewDidUnload. Você pensaria que era o oposto de viewDidLoad, mas não; só é chamado quando há uma situação de pouca memória que causa o descarregamento da exibição, e nem sempre que a exibição é descarregada na hora do desalocação.
Occulus
Eu concordo absolutamente com @occulus. O viewWillAppear tem sua desculpa porque a (tipo de) multitarefa não estava lá, mas o viewDidUnload definitivamente poderia ter um nome melhor.
MHC
Para mim, o viewDidDisappear É chamado quando o aplicativo está em segundo plano no iOS7. Posso obter uma confirmação?
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
fonte
3

Apenas tentando facilitar o máximo possível, veja o código abaixo:

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


}
ConfusedDeer
fonte