web-dev-qa-db-ja.com

Navigation Controllerの戻るボタンのアクションを設定する

Navigation Controllerの戻るボタンのデフォルトアクションを上書きしようとしています。ターゲットにカスタムボタンのアクションを提供しました。奇妙なことは、backbutton属性に割り当てたときに注意を払わず、現在のビューをポップしてルートに戻ることです。

UIBarButtonItem *backButton = [[UIBarButtonItem alloc] 
                                  initWithTitle: @"Servers" 
                                  style:UIBarButtonItemStylePlain 
                                  target:self 
                                  action:@selector(home)];
self.navigationItem.backBarButtonItem = backButton;

leftBarButtonItemnavigationItemで設定するとすぐにアクションが呼び出されますが、ボタンは矢印の付いたものではなく、丸い丸いボタンのように見えます。

self.navigationItem.leftBarButtonItem = backButton;

ルートビューに戻る前にカスタムアクションを呼び出すにはどうすればよいですか?デフォルトのバックアクションを上書きする方法はありますか、またはビューを離れるときに常に呼び出されるメソッドがありますか(viewDidUnloadはそれを行いません)?

175
Parrots

プレスを検出したいView Controllerにこれを入れてみてください:

-(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];
}
363

IViewController-BackButtonHandler 拡張機能を実装しました。サブクラス化する必要はありません。プロジェクトに追加して、navigationShouldPopOnBackButtonクラスのUIViewControllerメソッドをオーバーライドするだけです。

-(BOOL) navigationShouldPopOnBackButton {
    if(needsShowConfirmation) {
        // Show confirmation alert
        // ...
        return NO; // Ignore 'Back' button this time
    }
    return YES; // Process 'Back' button click and Pop view controler
}

サンプルアプリのダウンロード

173
onegray

アマグラマーが言ったのとは異なり、それは可能です。 navigationControllerをサブクラス化する必要があります。すべてを説明しました ここ (コード例を含む)。

42
HansPinckaers

迅速なバージョン:

https://stackoverflow.com/a/19132881/826435

View Controllerでは、プロトコルに準拠し、必要なアクションを実行するだけです。

extension MyViewController: NavigationControllerBackButtonDelegate {
    func shouldPopOnBackButtonPress() -> Bool {
        performSomeActionOnThePressOfABackButton()
        return false
    }
}

次に、NavigationController+BackButtonと言うクラスを作成し、次のコードをコピーして貼り付けます。

protocol NavigationControllerBackButtonDelegate {
    func shouldPopOnBackButtonPress() -> Bool
}

extension UINavigationController {
    public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
        // Prevents from a synchronization issue of popping too many navigation items
        // and not enough view controllers or viceversa from unusual tapping
        if viewControllers.count < navigationBar.items!.count {
            return true
        }

        // Check if we have a view controller that wants to respond to being popped
        var shouldPop = true
        if let viewController = topViewController as? NavigationControllerBackButtonDelegate {
            shouldPop = viewController.shouldPopOnBackButtonPress()
        }

        if (shouldPop) {
            DispatchQueue.main.async {
                self.popViewController(animated: true)
            }
        } else {
            // Prevent the back button from staying in an disabled state
            for view in navigationBar.subviews {
                if view.alpha < 1.0 {
                    UIView.animate(withDuration: 0.25, animations: {
                        view.alpha = 1.0
                    })
                }
            }

        }

        return false
    }
}
13
kgaidis

いくつかのスレッド化の理由から、@ HansPinckaersが言及した解決策は私には適切ではありませんでしたが、戻るボタンをタッチする非常に簡単な方法を見つけました。他の誰か。トリックは非常に簡単です。UINavigationBarのサブビューとして透明なUIButtonを追加し、実際のボタンであるかのようにセレクターを彼に設定します。以下にMonotouchとC#を使用した例を示しますが、objective-cへの翻訳は見つけにくいものではありません。

public class Test : UIViewController {
    public override void ViewDidLoad() {
        UIButton b = new UIButton(new RectangleF(0, 0, 60, 44)); //width must be adapted to label contained in button
        b.BackgroundColor = UIColor.Clear; //making the background invisible
        b.Title = string.Empty; // and no need to write anything
        b.TouchDown += delegate {
            Console.WriteLine("caught!");
            if (true) // check what you want here
                NavigationController.PopViewControllerAnimated(true); // and then we pop if we want
        };
        NavigationController.NavigationBar.AddSubview(button); // insert the button to the nav bar
    }
}

おもしろい事実:テスト目的と、偽のボタンに適した寸法を見つけるために、背景色を青に設定します...そして、behindbackボタン!とにかく、元のボタンをターゲットとするタッチをキャッチします。

5
psycho

直接行うことはできません。いくつかの選択肢があります。

  1. テストに合格したらタップとポップで検証する独自のカスタムUIBarButtonItemを作成します
  2. -textFieldShouldReturn: などのUITextFieldデリゲートメソッドを使用して、フォームフィールドの内容を検証します。これは、ReturnまたはDoneボタンがキーボードで押された後に呼び出されます

最初のオプションの欠点は、戻るボタンの左向き矢印スタイルにカスタムバーボタンからアクセスできないことです。そのため、画像を使用するか、通常のスタイルボタンを使用する必要があります。

2番目のオプションは、デリゲートメソッドでテキストフィールドを取得するため、ニースです。そのため、デリゲートコールバックメソッドに送信される特定のテキストフィールドを検証ロジックの対象にすることができます。

5
Alex Reynolds

この手法により、View Controllerのタイトルに影響を与えたり、アニメーション中に戻るボタンのテキストが変化したりすることなく、「戻る」ボタンのテキストを変更できます。

これをcalling View Controllerのinitメソッドに追加します。

UIBarButtonItem *temporaryBarButtonItem = [[UIBarButtonItem alloc] init];   
temporaryBarButtonItem.title = @"Back";
self.navigationItem.backBarButtonItem = temporaryBarButtonItem;
[temporaryBarButtonItem release];
3
Jason Moore

戻るボタンのスタイルも保持するソリューションを見つけました。 View Controllerに次のメソッドを追加します。

-(void) overrideBack{

    UIButton *transparentButton = [[UIButton alloc] init];
    [transparentButton setFrame:CGRectMake(0,0, 50, 40)];
    [transparentButton setBackgroundColor:[UIColor clearColor]];
    [transparentButton addTarget:self action:@selector(backAction:) forControlEvents:UIControlEventTouchUpInside];
    [self.navigationController.navigationBar addSubview:transparentButton];


}

次のメソッドで必要に応じて機能を提供します。

-(void)backAction:(UIBarButtonItem *)sender {
    //Your functionality
}

それは、透明なボタンで戻るボタンを覆うだけです;)

3
Sarasranglt

最も簡単な方法

UINavigationControllerのデリゲートメソッドを使用できます。メソッドwillShowViewControllerは、VCの戻るボタンが押されたときに呼び出されます。

- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated;
3
Xar E Ahmer

UINavigationBarデリゲートメソッドと、オーバーライドShouldPopItem-メソッドをサブクラス化するだけの簡単な方法があります。

2
jazzyjef2002

これは、ナビゲーションバーの戻るボタンイベントが発生する前にキャッチするための@ onewayのSwift 3バージョンです。 UINavigationBarDelegateUIViewControllerに使用できないため、navigationBarshouldPopが呼び出されたときにトリガーされるデリゲートを作成する必要があります。

@objc public protocol BackButtonDelegate {
      @objc optional func navigationShouldPopOnBackButton() -> Bool 
}

extension UINavigationController: UINavigationBarDelegate  {

    public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {

        if viewControllers.count < (navigationBar.items?.count)! {                
            return true
        }

        var shouldPop = true
        let vc = self.topViewController

        if vc.responds(to: #selector(vc.navigationShouldPopOnBackButton)) {
            shouldPop = vc.navigationShouldPopOnBackButton()
        }

        if shouldPop {
            DispatchQueue.main.async {
                self.popViewController(animated: true)
            }
        } else {
            for subView in navigationBar.subviews {
                if(0 < subView.alpha && subView.alpha < 1) {
                    UIView.animate(withDuration: 0.25, animations: {
                        subView.alpha = 1
                    })
                }
            }
        }

        return false
    }
}

次に、View Controllerでデリゲート関数を追加します。

class BaseVC: UIViewController, BackButtonDelegate {
    func navigationShouldPopOnBackButton() -> Bool {
        if ... {
            return true
        } else {
            return false
        }        
    }
}

ユーザーが戻って行きたいかどうかを判断するために、アラートコントローラーを追加したいことがよくあることに気付きました。その場合、navigationShouldPopOnBackButton()関数でいつでもreturn falseを実行し、次のような操作を行ってView Controllerを閉じることができます。

func navigationShouldPopOnBackButton() -> Bool {
     let alert = UIAlertController(title: "Warning",
                                          message: "Do you want to quit?",
                                          preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "Yes", style: .default, handler: { UIAlertAction in self.yes()}))
            alert.addAction(UIAlertAction(title: "No", style: .cancel, handler: { UIAlertAction in self.no()}))
            present(alert, animated: true, completion: nil)
      return false
}

func yes() {
     print("yes")
     DispatchQueue.main.async {
            _ = self.navigationController?.popViewController(animated: true)
        }
}

func no() {
    print("no")       
}
2
Lawliet

これが簡単に可能だとは思わない。これを回避する唯一の方法は、自分で戻るボタンの矢印画像を作成してそこに配置することです。最初はイライラしましたが、一貫性のために除外された理由がわかります。

通常のボタンを作成し、デフォルトの戻るボタンを非表示にすることで、(矢印なしで)閉じることができます。

self.navigationItem.leftBarButtonItem = [[[UIBarButtonItem alloc] initWithTitle:@"Servers" style:UIBarButtonItemStyleDone target:nil action:nil] autorelease];
self.navigationItem.hidesBackButton = YES;
2
Meltemi

Swiftの使用:

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

onegrayのソリューションは安全ではありません。Appleの公式ドキュメントによると、 https://developer.Apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html 、weそれを避けるべきです。

「カテゴリで宣言されたメソッドの名前が元のクラスのメソッド、または同じクラス(またはスーパークラス)の別のカテゴリのメソッドと同じ場合、動作はどのメソッド実装が使用されるかについて未定義です独自のクラスでカテゴリを使用している場合、これは問題になる可能性は低くなりますが、カテゴリを使用して標準のCocoaまたはCocoa Touchクラスにメソッドを追加すると問題が発生する可能性があります。

2
user2612791

これが私のSwiftソリューションです。 UIViewControllerのサブクラスで、navigationShouldPopOnBackButtonメソッドをオーバーライドします。

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

extension UINavigationController {

    func navigationBar(navigationBar: UINavigationBar, shouldPopItem item: UINavigationItem) -> Bool {
        if let vc = self.topViewController {
            if vc.navigationShouldPopOnBackButton() {
                self.popViewControllerAnimated(true)
            } else {
                for it in navigationBar.subviews {
                    let view = it as! UIView
                    if view.alpha < 1.0 {
                        [UIView .animateWithDuration(0.25, animations: { () -> Void in
                            view.alpha = 1.0
                        })]
                    }
                }
                return false
            }
        }
        return true
    }

}
2
AutomatonTec

@Williamからの答えは正しいですが、ユーザーがスワイプからゴーバックのジェスチャを開始した場合、viewWillDisappearメソッドが呼び出され、selfでさえナビゲーションスタックに含まれません(つまり、self.navigationController.viewControllersにはselfが含まれません)。スワイプが完了せず、View Controllerが実際にポップされない場合でも。したがって、解決策は次のとおりです。

  1. 以下を使用して、viewDidAppearのスワイプゴーバックジェスチャーを無効にし、戻るボタンの使用のみを許可します。

    if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)])
    {
        self.navigationController.interactivePopGestureRecognizer.enabled = NO;
    }
    
  2. または、次のように、代わりにviewDidDisappearを使用します。

    - (void)viewDidDisappear:(BOOL)animated
    {
        [super viewDidDisappear:animated];
        if (![self.navigationController.viewControllers containsObject:self])
        {
            // back button was pressed or the the swipe-to-go-back gesture was
            // completed. We know this is true because self is no longer
            // in the navigation stack.
        }
    }
    
1
boherna

このアプローチは私にとってはうまくいきました(ただし、「戻る」ボタンには「<」記号はありません)。

- (void)viewDidLoad
{
    [super viewDidLoad];

    UIBarButtonItem* backNavButton = [[UIBarButtonItem alloc] initWithTitle:@"Back"
                                                                      style:UIBarButtonItemStyleBordered
                                                                     target:self
                                                                     action:@selector(backButtonClicked)];
    self.navigationItem.leftBarButtonItem = backNavButton;
}

-(void)backButtonClicked
{
    // Do something...
    AppDelegate* delegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
    [delegate.navController popViewControllerAnimated:YES];
}
1
Ivan

isMovingFromParentViewControllerを使用

override func viewWillDisappear(animated: Bool) {
    super.viewWillDisappear(true)

    if self.isMovingFromParentViewController {
        // current viewController is removed from parent
        // do some work
    }
}
1
herrk

このようなユーザー入力を必要とするフォームの場合、ナビゲーションスタックの一部ではなく「モーダル」として呼び出すことをお勧めします。そのように、彼らはフォーム上のビジネスの世話をしなければなりません、そしてあなたはそれを検証し、カスタムボタンを使用してそれを却下することができます。アプリの他の部分と同じように見えるが、より制御しやすいナビゲーションバーを設計することもできます。

1
Travis M.

NavigationBars Right Buttonアイテムにアクセスして、そのセレクタープロパティを設定してみてください...ここに参照 IBarButtonItem reference があります。セレクタを作成して設定するカスタムUIBarButtonItemへのバー...

1
Daniel

[戻る]ボタンをインターセプトするには、透明なUIControlで覆い、タッチをインターセプトします。

@interface MyViewController : UIViewController
{
    UIControl   *backCover;
    BOOL        inhibitBackButtonBOOL;
}
@end

@implementation MyViewController
-(void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    // Cover the back button (cannot do this in viewWillAppear -- too soon)
    if ( backCover == nil ) {
        backCover = [[UIControl alloc] initWithFrame:CGRectMake( 0, 0, 80, 44)];
#if TARGET_IPHONE_SIMULATOR
        // show the cover for testing
        backCover.backgroundColor = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.15];
#endif
        [backCover addTarget:self action:@selector(backCoverAction) forControlEvents:UIControlEventTouchDown];
        UINavigationBar *navBar = self.navigationController.navigationBar;
        [navBar addSubview:backCover];
    }
}

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

    [backCover removeFromSuperview];
    backCover = nil;
}

- (void)backCoverAction
{
    if ( inhibitBackButtonBOOL ) {
        NSLog(@"Back button aborted");
        // notify the user why...
    } else {
        [self.navigationController popViewControllerAnimated:YES]; // "Back"
    }
}
@end
1
Jeff

Swift 4 iOS 11.3バージョン:

これは https://stackoverflow.com/a/34343418/4316579 のkgaidisからの回答に基づいています

拡張機能の動作がいつ停止したかはわかりませんが、この記事の執筆時点(Swift 4)では、以下に説明するようにUINavigationBarDelegate準拠を宣言しない限り、拡張機能は実行されないようです。

これが、なぜ拡張機能が機能しなくなったのか疑問に思っている人々の助けになることを願っています。

extension UINavigationController: UINavigationBarDelegate {
    public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {

    }
}
1
Edward L.

少なくともXcode 5には、シンプルでかなり良い(完璧ではない)ソリューションがあります。 IBで、[ユーティリティ]ペインからバーボタンアイテムをドラッグし、[戻る]ボタンがあるナビゲーションバーの左側にドロップします。ラベルを「戻る」に設定します。 IBActionに結び付けてviewControllerを閉じることができる機能ボタンがあります。私はいくつかの作業を行ってから、巻き戻しセグエをトリガーし、完全に機能します。

理想的ではないのは、このボタンが<矢印を取得せず、以前のVCタイトルを引き継がないことですが、これは管理できると思います。私の目的のために、新しい戻るボタンを「完了」ボタンに設定し、その目的が明確になっています。

また、IBナビゲーターには2つの[戻る]ボタンが表示されますが、わかりやすくするためにラベルを付けるのは簡単です。

enter image description here

1
Dan Loughney

現在「nil」のままにしているターゲット変数とアクション変数を使用することで、ボタンが「選択」されたときに呼び出されるように保存ダイアログを配線できるはずです。気をつけて、これは奇妙な瞬間にトリガーされる可能性があります。

私はほとんどAmagrammerに同意しますが、矢印をカスタマイズしたボタンを作るのはそれほど難しいとは思いません。戻るボタンの名前を変更し、スクリーンショットを撮り、必要なボタンのサイズをPhotoshopで取り、それをボタンの上部の画像にします。

1
TahoeWolverine

Swift

override func viewWillDisappear(animated: Bool) {
    let viewControllers = self.navigationController?.viewControllers!
    if indexOfArray(viewControllers!, searchObject: self) == nil {
        // do something
    }
    super.viewWillDisappear(animated)
}

func indexOfArray(array:[AnyObject], searchObject: AnyObject)-> Int? {
    for (index, value) in enumerate(array) {
        if value as UIViewController == searchObject as UIViewController {
            return index
        }
    }
    return nil
}
1
zono

新しい方法が見つかりました:

Objective-C

- (void)didMoveToParentViewController:(UIViewController *)parent{
    if (parent == NULL) {
        NSLog(@"Back Pressed");
    }
}

Swift

override func didMoveToParentViewController(parent: UIViewController?) {
    if parent == nil {
        println("Back Pressed")
    }
}
0
Ashish Kakkad

私がこれまでに見つけた解決策はあまりいいものではありませんが、私にとってはうまくいきます。これを取ります answer 、プログラムでポップするかどうかもチェックします:

- (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];
0
Ferran Maylinch

IAlert in Swift5 in Asynchronous wayで以前の応答を構築する


protocol NavigationControllerBackButtonDelegate {
    func shouldPopOnBackButtonPress(_ completion: @escaping (Bool) -> ())
}

extension UINavigationController: UINavigationBarDelegate {
    public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {

        if viewControllers.count < navigationBar.items!.count {
            return true
        }

        // Check if we have a view controller that wants to respond to being popped

        if let viewController = topViewController as? NavigationControllerBackButtonDelegate {

            viewController.shouldPopOnBackButtonPress { shouldPop in
                if (shouldPop) {
                    /// on confirm => pop
                    DispatchQueue.main.async {
                        self.popViewController(animated: true)
                    }
                } else {
                    /// on cancel => do nothing
                }
            }
            /// return false => so navigator will cancel the popBack
            /// until user confirm or cancel
            return false
        }else{
            DispatchQueue.main.async {
                self.popViewController(animated: true)
            }
        }
        return true
    }
}


コントローラー上


extension MyController: NavigationControllerBackButtonDelegate {

    func shouldPopOnBackButtonPress(_ completion: @escaping (Bool) -> ()) {

        let msg = "message"

        /// show UIAlert
        alertAttention(msg: msg, actions: [

            .init(title: "Continuer", style: .destructive, handler: { _ in
                completion(true)
            }),
            .init(title: "Annuler", style: .cancel, handler: { _ in
                completion(false)
            })
            ])

    }

}
0
brahimm

@ onegrayの回答の迅速なバージョン

protocol RequestsNavigationPopVerification {
    var confirmationTitle: String { get }
    var confirmationMessage: String { get }
}

extension RequestsNavigationPopVerification where Self: UIViewController {
    var confirmationTitle: String {
        return "Go back?"
    }

    var confirmationMessage: String {
        return "Are you sure?"
    }
}

final class NavigationController: UINavigationController {

    func navigationBar(navigationBar: UINavigationBar, shouldPopItem item: UINavigationItem) -> Bool {

        guard let requestsPopConfirm = topViewController as? RequestsNavigationPopVerification else {
            popViewControllerAnimated(true)
            return true
        }

        let alertController = UIAlertController(title: requestsPopConfirm.confirmationTitle, message: requestsPopConfirm.confirmationMessage, preferredStyle: .Alert)

        alertController.addAction(UIAlertAction(title: "Cancel", style: .Cancel) { _ in
            dispatch_async(dispatch_get_main_queue(), {
                let dimmed = navigationBar.subviews.flatMap { $0.alpha < 1 ? $0 : nil }
                UIView.animateWithDuration(0.25) {
                    dimmed.forEach { $0.alpha = 1 }
                }
            })
            return
        })

        alertController.addAction(UIAlertAction(title: "Go back", style: .Default) { _ in
            dispatch_async(dispatch_get_main_queue(), {
                self.popViewControllerAnimated(true)
            })
        })

        presentViewController(alertController, animated: true, completion: nil)

        return false
    }
}

これで、どのコントローラーでも、RequestsNavigationPopVerificationに準拠するだけで、この動作がデフォルトで採用されます。

0
Adam Waite