Настройка действия кнопки "Назад" в контроллере навигации

Я пытаюсь переписать действие по умолчанию кнопки "Назад" в контроллере навигации. Я предоставил цели действие на пользовательской кнопке. Странно то, что при назначении его через атрибут backbutton он не обращает на них внимания, а просто выскакивает из текущего представления и возвращается к корню:

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

Как только я установил это через leftBarButtonItem на navigationItem он вызывает мое действие, но тогда кнопка выглядит как круглая, а не как стрелка назад:

self.navigationItem.leftBarButtonItem = backButton;

Как я могу заставить его вызывать свое пользовательское действие, прежде чем вернуться к корневому представлению? Есть ли способ перезаписать обратное действие по умолчанию, или есть метод, который всегда вызывается при выходе из представления (viewDidUnload не делает этого)?

28 ответов

Попробуйте вставить это в контроллер вида, где вы хотите обнаружить нажатие:

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

Я реализовал расширение https://github.com/onegray/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
}

Загрузите пример приложения.

В отличие от Амаграммера, это возможно. Вы должны подкласс navigationController, Я все объяснил здесь (включая пример кода).

Swift версия:

(из /questions/33864183/nastrojka-dejstviya-knopki-nazad-v-kontrollere-navigatsii/33864207#33864207)

В вашем контроллере представления вы просто соблюдаете протокол и выполняете все необходимые действия:

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

По некоторым причинам многопоточность, решение, упомянутое @HansPinckaers, мне не подошло, но я нашел очень простой способ поймать прикосновение к кнопке "Назад", и я хочу закрепить это здесь на случай, если это поможет избежать многочасовых обманов кто-нибудь другой. Трюк действительно прост: просто добавьте прозрачную UIButton в качестве подпредставления к вашему UINavigationBar и установите селекторы для него, как если бы это была настоящая кнопка! Вот пример, использующий Monotouch и C#, но перевод в target-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
    }
}

Интересный факт: в целях тестирования и чтобы найти подходящие размеры для моей фальшивой кнопки, я установил синий цвет ее фона... И он отображается за кнопкой "Назад"! Во всяком случае, он все еще ловит любое прикосновение, нацеленное на оригинальную кнопку.

Это невозможно сделать напрямую. Есть пара альтернатив:

  1. Создайте свой собственный UIBarButtonItem который проверяет при нажатии и появляется, если тест проходит
  2. Проверьте содержимое поля формы, используя UITextField метод делегата, такой как -textFieldShouldReturn:, который называется после Return или же Done кнопка нажата на клавиатуре

Недостатком первого варианта является то, что стиль стрелки влево на кнопке "Назад" не может быть доступен с помощью пользовательской кнопки панели. Таким образом, вы должны использовать изображение или перейти с обычной кнопки стиля.

Второй вариант удобен тем, что вы возвращаете текстовое поле в методе делегата, поэтому вы можете настроить свою логику проверки на конкретное текстовое поле, отправленное методу обратного вызова делегата.

Переопределение Панель навигации (_ Панель навигации:shouldPop): Это не очень хорошая идея, даже если она работает. для меня это генерировало случайные сбои при возвращении. Я советую вам просто переопределить кнопку возврата, удалив кнопку backButton по умолчанию из navigationItem и создав настраиваемую кнопку возврата, как показано ниже:

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

   ...
 
}

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

Основываясь на предыдущих ответы с UIAlert в Swift5 в асинхронном способе


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

}

Нашел решение, которое сохраняет стиль кнопки "Назад". Добавьте следующий метод к вашему контроллеру представления.

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

Все, что он делает - это закрывает кнопку назад прозрачной кнопкой;)

Этот метод позволяет изменять текст кнопки "назад", не затрагивая заголовок любого из контроллеров представления или не видя изменения текста кнопки "Назад" во время анимации.

Добавьте это к методу init в вызывающем контроллере представления:

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

Самый простой способ

Вы можете использовать методы делегата UINavigationController. Метод willShowViewController Вызывается, когда нажата кнопка "Назад" вашего ВК.

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

Вот мое решение 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
    }

}

Решение onegray небезопасно. Согласно официальным документам Apple, https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html мы не должны этого делать.

"Если имя метода, объявленного в категории, совпадает с именем метода в исходном классе или методом в другой категории в том же классе (или даже в суперклассе), поведение не определено относительно того, какая реализация метода используется во время выполнения. Это менее вероятно, будет проблемой, если вы используете категории со своими собственными классами, но могут вызвать проблемы при использовании категорий для добавления методов в стандартные классы Cocoa или Cocoa Touch."

Я не верю, что это возможно, легко. Единственный способ обойти это - создать собственное изображение стрелки назад, чтобы разместить его там. Поначалу это меня расстраивало, но я понимаю, почему, ради последовательности, это не учли.

Вы можете приблизиться (без стрелки), создав обычную кнопку и скрыв кнопку возврата по умолчанию:

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

Используя Swift:

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

Версия Swift 4 для iOS 11.3:

Это основано на ответе от kgaidis от /questions/33864183/nastrojka-dejstviya-knopki-nazad-v-kontrollere-navigatsii/33864203#33864203

Я не уверен, когда расширение перестало работать, но на момент написания этой статьи (Swift 4) кажется, что расширение больше не будет выполняться, пока вы не объявите соответствие UINavigationBarDelegate, как описано ниже.

Надеюсь, что это помогает людям, которые задаются вопросом, почему их расширение больше не работает.

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

    }
}

Есть более простой способ, просто подклассифицируя метод делегата UINavigationBar и переопределить ShouldPopItemметод.

Вот версия Swon 3 ответа @oneway для отлавливания события кнопки возврата на панели навигации до того, как оно будет запущено. Как UINavigationBarDelegate не может быть использован для UIViewController необходимо создать делегат, который будет срабатывать при 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
    }
}

А затем в вашем контроллере представления добавьте функцию делегата:

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

Этот подход работал для меня (но кнопка "Назад" не будет иметь знак "<"):

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

Ответ от @William является правильным, однако, если пользователь запускает жест пролистывания назад, вызывается метод viewWillDisappear, и даже "self" не будет в стеке навигации (то есть, self.navigationController.viewControllers won'содержать' себя '), даже если свайп не завершен и контроллер вида фактически не вытолкнут. Таким образом, решение будет:

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

Использование isMovingFromParentViewController

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

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

Для формы, которая требует пользовательского ввода, подобного этой, я бы рекомендовал вызывать ее как "модальную" вместо части вашего стека навигации. Таким образом, они должны позаботиться о бизнесе в форме, затем вы можете проверить ее и отклонить с помощью пользовательской кнопки. Вы даже можете создать навигационную панель, которая будет выглядеть так же, как и все остальное ваше приложение, но даст вам больше контроля.

Стриж

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
}

По крайней мере, в Xcode 5 есть простое и довольно хорошее (не идеальное) решение. В IB перетащите элемент панели кнопок с панели "Утилиты" и поместите его в левую часть панели навигации, где будет кнопка "Назад". Установите метку "Назад". У вас будет функциональная кнопка, которую вы можете привязать к вашему IBAction и закрыть свой viewController. Я делаю некоторую работу, а затем запускаю сеанс расслабления, и он работает отлично.

Что не идеально, так это то, что эта кнопка не получает стрелку <и не переносит предыдущий заголовок ВК, но я думаю, что этим можно управлять. Для моих целей я установил новую кнопку "Назад" как кнопку "Готово", поэтому ее назначение понятно.

Вы также получите две кнопки "Назад" в навигаторе IB, но для наглядности достаточно легко обозначить их.

Вы можете попытаться получить доступ к элементу правой кнопки NavigationBars и установить его свойство селектора... вот ссылка на ссылку UIBarButtonItem, еще одна вещь, если эта работа, которая будет работать, - это установить правый элемент кнопки панели навигации на пользовательский UIBarButtonItem, который вы создать и установить его селектор... надеюсь, это поможет

Чтобы перехватить кнопку "Назад", просто закройте ее прозрачным 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

Используя переменные target и action, которые вы в настоящий момент оставляете "nil", вы сможете связать свои диалоги сохранения так, чтобы они вызывались, когда кнопка "выбрана". Остерегайтесь, это может быть вызвано в странные моменты.

Я согласен в основном с Amagrammer, но я не думаю, что было бы так сложно сделать кнопку со стрелкой на заказ. Я бы просто переименовал кнопку "Назад", сделал снимок экрана, сделал фотошоп нужного размера кнопки, и это было изображение сверху вашей кнопки.

Быстрая версия ответа @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 и это поведение принято по умолчанию.

Найден новый способ сделать это:

Objective-C

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

стриж

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

Решение, которое я нашел до сих пор, не очень хорошее, но оно работает для меня. Принимая этот ответ, я также проверяю, высовываю ли я программно или нет:

- (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];
Другие вопросы по тегам