web-dev-qa-db-ja.com

ナビゲーションバーで「戻る」ボタンが押されたことを検出する

Navbarで[戻る]ボタン(前の画面に戻る、親ビューに戻る)ボタンが押されたときにいくつかのアクションを実行する必要があります。

画面が消える前にイベントをキャッチし、データを一時停止して保存するアクションを起動するために実装できる方法はありますか?

120
user440096

UPDATE:一部のコメントによると、元の回答のソリューションは、iOS 8以降の特定のシナリオでは機能しないようです。詳細がなければ、実際にそうであることを確認することはできません。

ただし、そのような状況では、代替手段があります。 willMove(toParentViewController:)をオーバーライドすることにより、View Controllerがポップされていることを検出できます。基本的な考え方は、parentnilのときにView Controllerがポップされるということです。

詳細については、 "Container View Controllerの実装" をご覧ください。


IOS 5以降、この状況に対処する最も簡単な方法は、新しいメソッド- (BOOL)isMovingFromParentViewControllerを使用することです。

- (void)viewWillDisappear:(BOOL)animated {
  [super viewWillDisappear:animated];

  if (self.isMovingFromParentViewController) {
    // Do your stuff here
  }
}

- (BOOL)isMovingFromParentViewControllerは、ナビゲーションスタックでコントローラーをプッシュおよびポップするときに意味があります。

ただし、モーダルビューコントローラを提示する場合は、代わりに- (BOOL)isBeingDismissedを使用する必要があります。

- (void)viewWillDisappear:(BOOL)animated {
  [super viewWillDisappear:animated];

  if (self.isBeingDismissed) {
    // Do your stuff here
  }
}

この質問 で述べたように、両方のプロパティを組み合わせることができます。

- (void)viewWillDisappear:(BOOL)animated {
  [super viewWillDisappear:animated];

  if (self.isMovingFromParentViewController || self.isBeingDismissed) {
    // Do your stuff here
  }
}

他のソリューションは、UINavigationBarの存在に依存しています。その代わり、イベントをトリガーしたアクションから実行するために必要なタスクを切り離す、つまり戻るボタンを押すため、私のアプローチがより好きです。

301
elitalon

viewWillAppear()viewDidDisappear()戻るボタンがタップされたときに呼び出されますが、他のときにも呼び出されます。詳細については、回答の終わりを参照してください。

UIViewController.parentを使用する

willMoveToParentViewController(_:) VC didMoveToParentViewController()の助けを借りて、ORがその親(NavigationController)から削除されると、戻るボタンの検出がより適切に行われます。

親がnilの場合、View ControllerはNavigation Stackからポップされて閉じられます。 parentがnilでない場合、スタックに追加されて表示されます。

// Objective-C
-(void)willMoveToParentViewController:(UIViewController *)parent {
     [super willMoveToParentViewController:parent];
    if (!parent){
       // The back button was pressed or interactive gesture used
    }
}


// Swift
override func willMove(toParentViewController parent: UIViewController?) {
    super.willMove(toParentViewController:parent)
    if parent == nil {
        // The back button was pressed or interactive gesture used
    }
}

willMovedidMoveに交換し、self.parentをチェックして、作業を行うafterView Controllerが閉じられます。

解雇の停止

何らかの非同期保存を行う必要がある場合、親をチェックしても遷移を「一時停止」することはできません。これを行うには、次を実装できます。ここでの欠点は、おしゃれなiOSスタイル/アニメーションの戻るボタンを失うことだけです。また、インタラクティブなスワイプジェスチャーにも注意してください。このケースを処理するには、次を使用します。

var backButton : UIBarButtonItem!

override func viewDidLoad() {
    super.viewDidLoad()

     // Disable the swipe to make sure you get your chance to save
     self.navigationController?.interactivePopGestureRecognizer.enabled = false

     // Replace the default back button
    self.navigationItem.setHidesBackButton(true, animated: false)
    self.backButton = UIBarButtonItem(title: "Back", style: UIBarButtonItemStyle.Plain, target: self, action: "goBack")
    self.navigationItem.leftBarButtonItem = backButton
}

// Then handle the button selection
func goBack() {
    // Here we just remove the back button, you could also disabled it or better yet show an activityIndicator
    self.navigationItem.leftBarButtonItem = nil
    someData.saveInBackground { (success, error) -> Void in
        if success {
            self.navigationController?.popViewControllerAnimated(true)
            // Don't forget to re-enable the interactive gesture
            self.navigationController?.interactivePopGestureRecognizer.enabled = true
        }
        else {
            self.navigationItem.leftBarButtonItem = self.backButton
            // Handle the error
        }
    }
}


ビューの詳細が表示されました

viewWillAppearviewDidDisappearの問題を取得できなかった場合、例を見てみましょう。 3つのView Controllerがあるとします。

  1. ListVC:物事のテーブルビュー
  2. DetailVC:モノに関する詳細
  3. SettingsVC:Thingのいくつかのオプション

detailVCからlistVCに戻り、settingsVCに戻るときに、listVCの呼び出しをたどりましょう

リスト>詳細(Push detailVC)Detail.viewDidAppear <-が表示されます
詳細>設定(Push settingsVC)Detail.viewDidDisappear <-消えます

そして戻ると...
設定>詳細(pop settingsVC)Detail.viewDidAppear <-が表示されます
詳細>リスト(pop detailVC)Detail.viewDidDisappear <-消えます

viewDidDisappearは、戻るときだけでなく、進むときにも複数回呼び出されることに注意してください。迅速な操作が必要な場合がありますが、保存するネットワークコールなどのより複雑な操作の場合は、そうでない場合があります。

92
WCByrne

最初の方法

- (void)didMoveToParentViewController:(UIViewController *)parent
{
    if (![parent isEqual:self.parentViewController]) {
         NSLog(@"Back pressed");
    }
}

2番目の方法

-(void) viewWillDisappear:(BOOL)animated {
    if ([self.navigationController.viewControllers indexOfObject:self]==NSNotFound) {
       // back button was pressed.  We know this is true because self is no longer
       // in the navigation stack.  
    }
    [super viewWillDisappear:animated];
}
16
Xar E Ahmer

私はこの問題で2日間プレイ(またはファイティング)しました。 IMOの最良のアプローチは、次のように拡張クラスとプロトコルを作成することです。

@protocol UINavigationControllerBackButtonDelegate <NSObject>
/**
 * Indicates that the back button was pressed.
 * If this message is implemented the pop logic must be manually handled.
 */
- (void)backButtonPressed;
@end

@interface UINavigationController(BackButtonHandler)
@end

@implementation UINavigationController(BackButtonHandler)
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
{
    UIViewController *topViewController = self.topViewController;
    BOOL wasBackButtonClicked = topViewController.navigationItem == item;
    SEL backButtonPressedSel = @selector(backButtonPressed);
    if (wasBackButtonClicked && [topViewController respondsToSelector:backButtonPressedSel]) {
        [topViewController performSelector:backButtonPressedSel];
        return NO;
    }
    else {
        [self popViewControllerAnimated:YES];
        return YES;
    }
}
@end

これは、View ControllerがポップされるたびにUINavigationControllernavigationBar:shouldPopItem:への呼び出しを受け取るためです。そこで、戻るボタンが押されたかどうかを検出します(他のボタン)。あなたがしなければならない唯一のことは、戻るボタンが押されたView Controllerでプロトコルを実装することです。

すべてが問題ない場合は、backButtonPressedSel内にView Controllerを手動でポップすることを忘れないでください。

既にUINavigationViewControllerをサブクラス化してnavigationBar:shouldPopItem:を実装している場合、心配する必要はありません。これは干渉しません。

バックジェスチャを無効にすることもできます。

if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)]) {
    self.navigationController.interactivePopGestureRecognizer.enabled = NO;
}
9
7ynk3r

これは、Swiftを使用したiOS 9.3.xで機能します。

override func didMoveToParentViewController(parent: UIViewController?) {
    super.didMoveToParentViewController(parent)

    if parent == self.navigationController?.parentViewController {
        print("Back tapped")
    }
}

ここの他のソリューションとは異なり、これは予期せずトリガーされるようには見えません。

7
Chris Villa

これが機能しないと主張する人は間違っています:

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    if self.isMovingFromParent {
        print("we are being popped")
    }
}

それはうまくいきます。それでは、広がらないという神話の原因は何ですか?

この問題は、differentメソッドの実装の誤り、つまりwillMove(toParent:)の実装がsuperを呼び出すのを忘れたことに起因するようです。

superを呼び出さずにwillMove(toParent:)を実装すると、self.isMovingFromParentfalseになり、viewWillDisappearの使用は失敗したように見えます。失敗しませんでした。あなたはそれを壊した。

6
matt

記録のために、これは彼が探していたものに近いと思います…

    UIBarButtonItem *l_backButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRewind target:self action:@selector(backToRootView:)];

    self.navigationItem.leftBarButtonItem = l_backButton;


    - (void) backToRootView:(id)sender {

        // Perform some custom code

        [self.navigationController popToRootViewControllerAnimated:YES];
    }
4
Paul Brady

purrrminatorが言うように、elitalonによる答えは完全には正しくありません。なぜなら、your stuffはコントローラーをプログラムでポップしても実行されるからです。

私がこれまでに見つけた解決策はあまりいいものではありませんが、私にとってはうまくいきます。 elitalonが言ったことに加えて、プログラムでポップしているかどうかも確認します。

- (void)viewWillDisappear:(BOOL)animated {
  [super viewWillDisappear:animated];

  if ((self.isMovingFromParentViewController || self.isBeingDismissed)
      && !self.isPoppingProgrammatically) {
    // Do your stuff here
  }
}

そのプロパティをコントローラーに追加し、プログラムでポップする前にYESに設定する必要があります。

self.isPoppingProgrammatically = YES;
[self.navigationController popViewControllerAnimated:YES];

ご協力いただきありがとうございます!

2
Ferran Maylinch

最良の方法は、UINavigationControllerデリゲートメソッドを使用することです

- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated

これを使用して、UINavigationControllerを表示しているコントローラーを知ることができます。

if ([viewController isKindOfClass:[HomeController class]]) {
    NSLog(@"Show home controller");
}
2
Harald

UINavigationControllerを使用するSwiftの場合:

override func viewWillDisappear(animated: Bool) {
    super.viewWillDisappear(animated)
    if self.navigationController?.topViewController != self {
        print("back button tapped")
    }
}
1
Murray Sagal

Coli88が言ったように、UINavigationBarDelegateプロトコルを確認する必要があります。

より一般的な方法では、- (void)viewWillDisapear:(BOOL)animatedを使用して、現在表示されているView Controllerによって保持されているビューが消えようとしているときにカスタム作業を実行することもできます。残念ながら、これはプッシュとポップの両方のケースをカバーします。

1
ramdam

UIControlを左側のnavigationBarに追加することで、この問題を解決しました。

UIControl *leftBarItemControl = [[UIControl alloc] initWithFrame:CGRectMake(0, 0, 90, 44)];
[leftBarItemControl addTarget:self action:@selector(onLeftItemClick:) forControlEvents:UIControlEventTouchUpInside];
self.leftItemControl = leftBarItemControl;
[self.navigationController.navigationBar addSubview:leftBarItemControl];
[self.navigationController.navigationBar bringSubviewToFront:leftBarItemControl];

また、ビューが消えるときは、忘れずに削除する必要があります。

- (void) viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    if (self.leftItemControl) {
        [self.leftItemControl removeFromSuperview];
    }    
}

それで全部です!

1
Eric

INavigationBarDelegate Protocol を確認する必要があります。この場合、navigationBar:shouldPopItem:メソッドを使用できます。

1
Coli88

7ynk3r の答えは、私が最後に使用したものに本当に近かったが、いくつかの微調整が必​​要だった。

- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item {

    UIViewController *topViewController = self.topViewController;
    BOOL wasBackButtonClicked = topViewController.navigationItem == item;

    if (wasBackButtonClicked) {
        if ([topViewController respondsToSelector:@selector(navBackButtonPressed)]) {
            // if user did press back on the view controller where you handle the navBackButtonPressed
            [topViewController performSelector:@selector(navBackButtonPressed)];
            return NO;
        } else {
            // if user did press back but you are not on the view controller that can handle the navBackButtonPressed
            [self popViewControllerAnimated:YES];
            return YES;
        }
    } else {
        // when you call popViewController programmatically you do not want to pop it twice
        return YES;
    }
}
1
micromanc3r

次のように、戻るボタンコールバックを使用できます。

- (BOOL) navigationShouldPopOnBackButton
{
    [self backAction];
    return NO;
}

- (void) backAction {
    // your code goes here
    // show confirmation alert, for example
    // ...
}

Swiftバージョンの場合は、グローバルスコープのようにできます

extension UIViewController {
     @objc func navigationShouldPopOnBackButton() -> Bool {
     return true
    }
}

extension UINavigationController: UINavigationBarDelegate {
     public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
          return self.topViewController?.navigationShouldPopOnBackButton() ?? true
    }
}

以下では、戻るボタンのアクションを制御したいViewControllerに配置します:

override func navigationShouldPopOnBackButton() -> Bool {
    self.backAction()//Your action you want to perform.

    return true
}
0

self.navigationController.isMovingFromParentViewControllerは、使用しているiOS8および9では動作していません。

-(void) viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    if (self.navigationController.topViewController != self)
    {
        // Is Popping
    }
}
0
Vassily