导航控制器中后退按钮的设置操作

我试图覆盖导航控制器中后退按钮的默认操作。我已经在自定义按钮上为目标提供了一个操作。奇怪的是,当通过后退按钮属性赋值时,它并没有注意到这些属性,而是弹出当前视图并返回根目录:

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

一旦我通过 navigationItem上的 leftBarButtonItem设置它,它就会调用我的操作,然而那个按钮看起来就像一个普通的圆按钮,而不是向后箭头的按钮:

self.navigationItem.leftBarButtonItem = backButton;

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

168817 次浏览

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

我大部分同意 Amagrammer,但我不认为这将是很难的按钮与箭头的习惯。我只是重命名后退按钮,采取屏幕截图,Photoshop 的按钮大小需要,并有这是你的按钮顶部的图像。

我觉得这不太可能。我相信解决这个问题的唯一方法是制作自己的后退按钮箭头图像放在那里。一开始我很沮丧,但是我明白为什么,为了保持一致性,它被忽略了。

你可以通过创建一个常规按钮并隐藏默认的后退按钮来靠近(不用箭头) :

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

不像 Amagrammer 说的,这是可能的。你必须子类化你的 navigationController。我解释了所有的 给你(包括示例代码)。

你可以尝试访问 NavigationBars Right Button 项目并设置它的选择器属性... 这里有一个引用 项目引用,如果这个不起作用的话,另外一件事就是,将导航栏的右键项目设置为一个自定义的 UIBarButtonItem,并设置它的选择器... 希望这能有所帮助

这是不可能直接做到的,有两个选择:

  1. 创建您自己的自定义 UIBarButtonItem,如果测试通过,它会立即进行验证并弹出
  2. 使用 UITextField委托方法(如 -textFieldShouldReturn:)验证表单字段内容,-textFieldShouldReturn:是在键盘上按下 ReturnDone按钮后调用的

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

第二个选项非常好,因为您可以将文本字段返回委托方法中,这样您就可以将验证逻辑指向发送给委托回调方法的特定文本字段。

这种技术允许您更改“后退”按钮的文本,而不会影响任何视图控制器的标题,也不会在动画过程中看到后退按钮文本的更改。

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

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

试着把这个放到视图控制器中你想要检测压力的地方:

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

由于一些线程方面的原因,@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
}
}

有趣的事实: 为了测试的目的,并找到好的尺寸为我的假按钮,我设置其背景颜色为蓝色... 和它显示 后面的后退按钮!不管怎样,它还是能捕捉到任何触摸到原始按钮的动作。

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

要拦截 Back 按钮,只需用一个透明的 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

有一个简单的方法,只是子类化的 委托方法UINavigationBar重写ShouldPopItem方法

我已经实现了 BackButtonHandler扩展。它不需要子类化任何东西,只需要把它放到你的项目中,并在 UIViewController类中覆盖 navigationShouldPopOnBackButton方法:

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

下载示例 app

至少在 Xcode 5中,有一个简单且相当不错(不完美)的解决方案。在 IB 中,从“实用程序”窗格中拖动一个 Bar Button Item,并将其放置在导航栏的左侧,后退按钮就位于该处。把标签设置为“返回”您将拥有一个功能按钮,可以将其绑定到 IBAction 并关闭 viewController。我正在做一些工作,然后触发一个放松的转折点,它完美地工作。

不理想的是,这个按钮没有得到 < Arrow 并且没有继承以前的 VC 标题,但是我认为这是可以管理的。出于我的目的,我将新的 Back 按钮设置为“完成”按钮,这样它的目的就清楚了。

在 IB 导航器中还有两个 Back 按钮,但是为了清晰起见,可以很容易地对其进行标记。

enter image description here

最简单的方法

可以使用 UINavigationController 的委托方法。当 VC 的返回按钮被按下时,方法 willShowViewController被调用。当返回按钮被按下时,做任何你想做的事情

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

斯威夫特

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
}

以下是我的 Swift 解决方案: 在 UIViewController 的子类中,覆盖导航 ShouldPopOnBackButton 方法。

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
}


}

这种方法对我很有效(但是“ Back”按钮没有“ <”标志) :

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

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

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

使用 isMovingFromParentViewController

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


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

找到了新的方法:

目标 C

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

斯威夫特

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

找到了一个解决方案,保留了后退按钮的风格以及。 将以下方法添加到视图控制器中。

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

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

Onegray 的解决方案并不安全。根据苹果公司的官方文件, 我们应该避免这样做。

“如果在类别中声明的方法的名称与原始类中的方法相同,或者与同一类(甚至超类)中的另一类中的方法相同,则在运行时使用哪个方法实现的行为是未定义的。如果您在自己的类中使用类别,这不太可能成为一个问题,但是在使用类别向标准 Cocoa 或 Cocoa Touch 类添加方法时可能会导致问题。”

迅速版本:

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

然而,@William 给出的答案是正确的,如果用户开始一个滑动到回退的手势,viewWillDisappear方法被调用,甚至 self也不会在导航栈中(也就是说,self.navigationController.viewControllers不包含 self) ,即使滑动没有完成,视图控制器实际上没有弹出。因此,解决方案将是:

  1. 禁用 viewDidAppear中的滑动回退手势,只允许使用后退按钮,方法是:

    if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)])
    {
    self.navigationController.interactivePopGestureRecognizer.enabled = NO;
    }
    
  2. Or simply use viewDidDisappear instead, as follows:

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

使用 Swift:

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

快速回答@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,这种行为就会默认采用。

这里是迅捷3版本的 @ one way的答案捕捉导航栏后退按钮事件之前,它被激发。由于 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
}
}
}

我意识到我们经常需要添加一个警报控制器,以便用户决定是否要返回。如果是这样,你总是可以在 navigationShouldPopOnBackButton()函数中使用 return false,并通过以下方法关闭视图控制器:

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

Swift 4 iOS 11.3版本:

这是建立在来自 https://stackoverflow.com/a/34343418/4316579的 Kgaidis 的答案之上的

我不确定扩展何时停止工作,但在撰写本文时(Swift 4) ,似乎该扩展将不再执行,除非您像下面描述的那样声明 UINavigationBar珠宝一致性。

希望这可以帮助那些想知道为什么他们的扩展不再工作的人。

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


}
}

覆盖导航栏(_ NavationBar: should dPop) : 这是 没有的一个好主意,即使它可以工作。对我来说,它在返回时会产生随机崩溃。我建议你直接重写后退按钮,删除导航项中的默认后退按钮,创建一个自定义后退按钮,如下所示:

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

}


}