181

我正在尝试覆盖导航控制器中后退按钮的默认操作。我在自定义按钮上提供了一个目标操作。奇怪的是,当通过 backbutton 属性分配它时,它不会注意它们,它只是弹出当前视图并返回到根:

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

一旦我通过leftBarButtonItemon 设置它,navigationItem它就会调用我的操作,但是按钮看起来像一个普通的圆形按钮,而不是带箭头的返回按钮:

self.navigationItem.leftBarButtonItem = backButton;

在返回根视图之前,如何让它调用我的自定义操作?有没有办法覆盖默认的后退操作,或者有没有在离开视图时总是调用的方法(viewDidUnload不这样做)?

4

29 回答 29

363

尝试将其放入要检测压力的视图控制器中:

-(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];
}
于 2010-08-10T04:20:03.527 回答
181

我已经实现了 UIViewController-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
}

下载示例应用程序

于 2013-10-02T08:28:48.237 回答
40

与 Amagrammer 所说的不同,这是可能的。你必须继承你的navigationController. 我在这里解释了一切(包括示例代码)。

于 2009-11-28T13:43:25.860 回答
17

斯威夫特版本:

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

在您的视图控制器中,您只需遵守协议并执行您需要的任何操作:

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
    }
}
于 2015-12-17T20:12:12.000 回答
5

直接做是不行的。有几种选择:

  1. 创建您自己的自定义UIBarButtonItem,在测试通过时验证点击并弹出
  2. 使用UITextField委托方法验证表单字段内容,例如,在按下键盘上的or按钮-textFieldShouldReturn:后调用ReturnDone

第一个选项的缺点是无法从自定义栏按钮访问后退按钮的左箭头样式。因此,您必须使用图像或使用常规样式按钮。

第二个选项很好,因为您可以在委托方法中返回文本字段,因此您可以将验证逻辑定位到发送到委托回调方法的特定文本字段。

于 2010-05-25T19:58:01.357 回答
5

由于某些线程原因,@HansPinckaers 提到的解决方案不适合我,但我找到了一种更简单的方法来触摸后退按钮,我想把它固定在这里,以防这可以避免数小时的欺骗其他人。诀窍非常简单:只需将一个透明的 UIButton 作为子视图添加到您的 UINavigationBar,并为他设置您的选择器,就好像它是真正的按钮一样!这是一个使用 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
    }
}

有趣的事实:为了测试目的并为我的假按钮找到合适的尺寸,我将它的背景颜色设置为蓝色......它显示后退按钮的后面!无论如何,它仍然会捕捉到针对原始按钮的任何触摸。

于 2011-07-06T23:46:33.380 回答
4

覆盖 navigationBar(_ navigationBar:shouldPop):这不是一个好主意,即使它有效。对我来说,它在返回时会产生随机崩溃。我建议您通过从 navigationItem 中删除默认的 backButton 并创建一个自定义的后退按钮来覆盖后退按钮,如下所示:

override func viewDidLoad(){
   super.viewDidLoad()
   
   navigationItem.leftBarButton = .init(title: "Go Back", ... , action: #selector(myCutsomBackAction) 

   ...
 
}

=========================================

以异步方式在Swift5中使用UIAlert构建先前的响应


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

}
于 2019-07-25T10:49:34.517 回答
3

此技术允许您更改“后退”按钮的文本,而不会影响任何视图控制器的标题或在动画期间看到后退按钮文本的更改。

将此添加到调用视图控制器的 init 方法中:

UIBarButtonItem *temporaryBarButtonItem = [[UIBarButtonItem alloc] init];   
temporaryBarButtonItem.title = @"Back";
self.navigationItem.backBarButtonItem = temporaryBarButtonItem;
[temporaryBarButtonItem release];
于 2010-07-23T01:36:58.270 回答
3

最简单的方法

您可以使用 UINavigationController 的委托方法。按下 VC 的后退按钮时调用该方法willShowViewController。按下后退按钮时执行任何您想要的操作

- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated;
于 2014-09-18T11:31:08.130 回答
3

这是我的 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
    }

}
于 2015-03-14T22:12:36.200 回答
3

找到了一个保留后退按钮样式的解决方案。将以下方法添加到您的视图控制器。

-(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
}

它所做的只是用透明按钮覆盖后退按钮;)

于 2015-08-21T11:13:33.957 回答
2

通过子类化 的委托方法UINavigationBar覆盖该方法,有一种更简单的ShouldPopItem方法

于 2013-08-27T18:49:29.137 回答
2

这种方法对我有用(但“返回”按钮没有“<”符号):

- (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];
}
于 2015-05-01T21:18:56.020 回答
2

我不相信这是可能的,很容易。我相信解决这个问题的唯一方法是制作你自己的后退按钮箭头图像放置在那里。一开始我很沮丧,但我明白为什么为了一致性起见,它被遗漏了。

您可以通过创建常规按钮并隐藏默认后退按钮来关闭(不带箭头):

self.navigationItem.leftBarButtonItem = [[[UIBarButtonItem alloc] initWithTitle:@"Servers" style:UIBarButtonItemStyleDone target:nil action:nil] autorelease];
self.navigationItem.hidesBackButton = YES;
于 2009-07-31T23:08:23.683 回答
2

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 {

    }
}
于 2018-04-30T18:37:47.280 回答
2

这是@oneway的 Swift 3 版本的答案,用于在导航栏后退按钮事件被触发之前捕获它。由于UINavigationBarDelegate不能用于UIViewController,您需要创建一个在navigationBar shouldPop被调用时触发的委托。

@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
    }
}

然后,在您的视图控制器中添加委托功能:

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

我意识到我们经常想为用户添加一个警报控制器来决定他们是否想要返回。return false如果是这样,您可以通过执行以下操作始终navigationShouldPopOnBackButton()运行并关闭视图控制器:

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")       
}
于 2017-02-28T10:21:52.250 回答
2

使用斯威夫特:

override func viewWillDisappear(animated: Bool) {
    super.viewWillDisappear(animated)
    if self.navigationController?.topViewController != self {
        print("back button tapped")
    }
}
于 2016-04-19T02:51:59.000 回答
2

onegray的解决方案并不安全。根据Apple的官方文档,https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html,我们应该避免这样做。

“如果在一个类别中声明的方法的名称与原始类中的方法相同,或者与同一类(甚至是超类)上的另一个类别中的方法相同,则对于使用哪种方法实现的行为未定义“

于 2015-11-02T06:02:52.217 回答
1

至少在 Xcode 5 中,有一个简单且相当不错(不完美)的解决方案。在 IB 中,将 Bar Button Item 从 Utilities 窗格中拖放到导航栏左侧的 Back 按钮所在的位置。将标签设置为“返回”。您将拥有一个功能按钮,您可以将其绑定到您的 IBAction 并关闭您的 viewController。我正在做一些工作,然后触发 unwind segue,它运行良好。

不理想的是这个按钮没有得到<箭头,也没有继承之前的VCs标题,但我认为这是可以管理的。出于我的目的,我将新的后退按钮设置为“完成”按钮,这样它的目的就很明确了。

您最终还会在 IB 导航器中看到两个后退按钮,但为了清晰起见,标记它很容易。

在此处输入图像描述

于 2014-04-03T15:42:41.550 回答
1

通过使用您当前离开“nil”的目标和操作变量,您应该能够连接您的保存对话框,以便在“选择”按钮时调用它们。注意,这可能会在奇怪的时刻触发。

我主要同意 Amagrammer,但我认为定制带有箭头的按钮并不难。我只是重命名后退按钮,拍摄屏幕截图,Photoshop 所需的按钮大小,然后将其作为按钮顶部的图像。

于 2009-07-27T01:06:23.237 回答
1

要拦截后退按钮,只需用透明的 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
于 2013-02-19T09:46:54.200 回答
1

您可以尝试访问 NavigationBars Right Button 项目并设置其选择器属性...这里是一个参考UIBarButtonItem 参考,如果这确实有效,另一件事是,将导航栏的右键项目设置为您的自定义 UIBarButtonItem创建并设置它的选择器...希望这会有所帮助

于 2010-05-25T19:58:01.263 回答
1

迅速

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
}
于 2015-02-28T17:22:36.223 回答
1

对于需要像这样的用户输入的表单,我建议将其作为“模式”调用,而不是导航堆栈的一部分。这样他们就必须处理表单上的业务,然后您可以使用自定义按钮对其进行验证和关闭。您甚至可以设计一个与您的应用程序的其他部分看起来相同但提供更多控制权的导航栏。

于 2012-11-08T15:40:10.283 回答
1

利用isMovingFromParentViewController

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

    if self.isMovingFromParentViewController {
        // current viewController is removed from parent
        // do some work
    }
}
于 2015-05-27T15:49:49.787 回答
1

@onegray 答案的 Swift 版本

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并默认采用此行为。

于 2016-06-13T13:29:25.293 回答
1

@William 的答案是正确的,但是,如果用户开始滑动返回手势,viewWillDisappear则调用该方法,甚至self不会在导航堆栈中(即self.navigationController.viewControllers不包含self),即使滑动未完成,视图控制器实际上并未弹出。因此,解决方案是:

  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.
        }
    }
    
于 2016-01-26T19:33:34.737 回答
0

到目前为止我找到的解决方案不是很好,但它对我有用。接受这个答案,我还检查我是否以编程方式弹出:

- (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];
于 2015-05-11T11:07:42.930 回答
0

找到了新的方法:

Objective-C

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

迅速

override func didMoveToParentViewController(parent: UIViewController?) {
    if parent == nil {
        println("Back Pressed")
    }
}
于 2015-07-14T16:52:47.073 回答