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

Я пытаюсь переписать действие по умолчанию кнопки "Назад" в контроллере навигации. Я предоставил цели действие на пользовательской кнопке. Странно то, что при назначении его через атрибут 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 этого не делает)?

Ответ 1

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

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

Ответ 2

Я реализовал расширение 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
}

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

Ответ 3

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

Ответ 4

Быстрая версия:

(fooobar.com/questions/25243/...)

В вашем контроллере просмотра вы просто соглашаетесь с протоколом и выполняете любое действие, которое вам нужно:

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

Ответ 5

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

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

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

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

Ответ 6

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

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

Ответ 7

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

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

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

Ответ 8

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

Вы можете использовать методы делегирования UINavigationController. Метод willShowViewController вызывается, когда нажата кнопка "Назад" вашего VC.do, что бы вы ни захотели, когда нажата кнопка назад btn

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

Ответ 9

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

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

Все, что он делает, это закрыть кнопку "Назад" с помощью прозрачной кнопки;)

Ответ 10

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

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

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

Ответ 11

Более простой способ - просто подклассифицировать метод делегата UINavigationBar и переопределить метод ShouldPopItem.

Ответ 12

Здесь мое быстрое решение. В вашем подклассе 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
    }

}

Ответ 13

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

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

Ответ 14

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

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

Ответ 15

Здесь приведена версия Swift 3 из @oneway для того, чтобы уловить событие кнопки панели навигации перед тем, как он будет запущен. Поскольку 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")       
}

Ответ 16

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

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

Ответ 17

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

Ответ 18

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

Ответ 19

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

Ответ 20

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

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

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

enter image description here

Ответ 21

Свифта

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
}

Ответ 22

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

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

Ответ 23

Используйте isMovingFromParentViewController

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

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

Ответ 24

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

  1. Отключите жест viewDidAppear назад для просмотра в viewDidAppear и разрешите использование кнопки "Назад" только с помощью:

    if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)])
    {
        self.navigationController.interactivePopGestureRecognizer.enabled = NO;
    }
    
  2. Или просто используйте вместо этого viewDidDisappear, как 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.
        }
    }
    

Ответ 25

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

Это основано на ответе от kgaidis от fooobar.com/questions/25243/...

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

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

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

    }
}

Ответ 26

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

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

Ответ 27

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

Objective-C

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

Swift

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

Ответ 28

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

Ответ 29

Опираясь на предыдущие ответы с 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)
            })
            ])

    }

}