Обнаружение, когда кнопка "назад" нажата на панели навигации
Мне нужно выполнить некоторые действия, когда на навигационной панели нажата кнопка "Назад" (возврат к предыдущему экрану, возврат в родительский вид).
Есть ли какой-нибудь метод, который я могу реализовать, чтобы перехватить событие и запустить некоторые действия, чтобы приостановить и сохранить данные до исчезновения экрана?
18 ответов
ОБНОВЛЕНИЕ: Согласно некоторым комментариям, решение в оригинальном ответе, кажется, не работает при определенных сценариях в iOS 8+. Я не могу подтвердить, что это действительно так, без дальнейших подробностей.
Для тех из вас, однако, в этой ситуации есть альтернатива. Обнаружение, когда контроллер представления выталкивается, возможно, переопределяя willMove(toParentViewController:)
, Основная идея заключается в том, что контроллер представления появляется, когда parent
является nil
,
Проверьте "Реализация Контроллера Представления Контейнера" для получения дополнительной информации.
Начиная с iOS 5 я обнаружил, что самый простой способ справиться с этой ситуацией - использовать новый метод - (BOOL)isMovingFromParentViewController
:
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if (self.isMovingFromParentViewController) {
// Do your stuff here
}
}
- (BOOL)isMovingFromParentViewController
имеет смысл, когда вы нажимаете и выталкиваете контроллеры в стеке навигации.
Однако, если вы представляете контроллеры модального представления, вы должны использовать - (BOOL)isBeingDismissed
вместо:
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if (self.isBeingDismissed) {
// Do your stuff here
}
}
Как отмечено в этом вопросе, вы можете объединить оба свойства:
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if (self.isMovingFromParentViewController || self.isBeingDismissed) {
// Do your stuff here
}
}
Другие решения основаны на существовании UINavigationBar
, Мне больше нравится этот подход, потому что он отделяет требуемые задачи от действия, которое вызвало событие, то есть нажатие кнопки возврата.
В то время как viewWillAppear()
а также viewDidDisappear()
Вызваны, когда нажата кнопка "Назад", они также вызываются в другое время. Смотрите конец ответа для более подробной информации.
Использование UIViewController.parent
Обнаружение кнопки возврата лучше сделать, когда VC удален из его родителя (NavigationController) с помощью willMoveToParentViewController(_:)
ИЛИ ЖЕ didMoveToParentViewController()
Если parent равен nil, контроллер представления извлекается из стека навигации и удаляется. Если parent не равен nil, он добавляется в стек и представляется.
// Objective-C
-(void)willMoveToParentViewController:(UIViewController *)parent {
[super willMoveToParentViewController:parent];
if (!parent){
// The back button was pressed or interactive gesture used
}
}
// Swift
override func willMove(toParentViewController parent: UIViewController?) {
super.willMove(toParentViewController:parent)
if parent == nil {
// The back button was pressed or interactive gesture used
}
}
Поменять willMove
за didMove
и проверьте self.parent, чтобы сделать работу после того, как контроллер представления отклонен.
Остановка увольнения
Обратите внимание, что проверка родителя не позволяет вам "приостановить" переход, если вам нужно выполнить какое-то асинхронное сохранение. Для этого вы можете реализовать следующее. Единственный недостаток - вы теряете красивую анимированную кнопку возврата в стиле iOS. Также будьте осторожны с интерактивным жестом смахивания. Используйте следующие для обработки этого случая.
var backButton : UIBarButtonItem!
override func viewDidLoad() {
super.viewDidLoad()
// Disable the swipe to make sure you get your chance to save
self.navigationController?.interactivePopGestureRecognizer.enabled = false
// Replace the default back button
self.navigationItem.setHidesBackButton(true, animated: false)
self.backButton = UIBarButtonItem(title: "Back", style: UIBarButtonItemStyle.Plain, target: self, action: "goBack")
self.navigationItem.leftBarButtonItem = backButton
}
// Then handle the button selection
func goBack() {
// Here we just remove the back button, you could also disabled it or better yet show an activityIndicator
self.navigationItem.leftBarButtonItem = nil
someData.saveInBackground { (success, error) -> Void in
if success {
self.navigationController?.popViewControllerAnimated(true)
// Don't forget to re-enable the interactive gesture
self.navigationController?.interactivePopGestureRecognizer.enabled = true
}
else {
self.navigationItem.leftBarButtonItem = self.backButton
// Handle the error
}
}
}
Больше на вид будет / действительно появится
Если вы не получили viewWillAppear
viewDidDisappear
вопрос, давайте рассмотрим пример. Скажем, у вас есть три контроллера вида:
- ListVC: табличное представление вещей
- DetailVC: подробности о вещи
- Настройки ВК: некоторые варианты вещи
Давайте следить за звонками на detailVC
как вы идете от listVC
в settingsVC
и обратно к listVC
Список> Деталь (push detailVC) Detail.viewDidAppear
<- появляется
Подробно> Настройки (push settingsVC) Detail.viewDidDisappear
<- исчезнуть
И когда мы вернемся...
Настройки> Подробно (всплывающие настройки ВК) Detail.viewDidAppear
<- появляется
Деталь> Список (pop detailVC) Detail.viewDidDisappear
<- исчезнуть
Заметить, что viewDidDisappear
вызывается несколько раз, не только при движении назад, но и при движении вперед. Для быстрой операции, которая может быть желательной, но для более сложной операции, такой как сетевой вызов, для сохранения, это может не сработать.
Те, кто утверждает, что это не работает, ошибаются:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if self.isMovingFromParent {
print("we are being popped")
}
}
Это отлично работает. Так что же вызывает широко распространенный миф о том, что это не так?
Проблема, по-видимому, связана с неправильной реализацией другого метода, а именно с тем, что реализация willMove(toParent:)
забыл позвонить super
,
Если вы реализуете willMove(toParent:)
без звонка super
, затем self.isMovingFromParent
будет false
и использование viewWillDisappear
будет казаться неудачным. Это не подвело; ты сломал это.
Первый метод
- (void)didMoveToParentViewController:(UIViewController *)parent
{
if (![parent isEqual:self.parentViewController]) {
NSLog(@"Back pressed");
}
}
Второй метод
-(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];
}
Я играю (или борюсь) с этой проблемой в течение двух дней. IMO лучший подход - просто создать класс расширения и протокол, например так:
@protocol UINavigationControllerBackButtonDelegate <NSObject>
/**
* Indicates that the back button was pressed.
* If this message is implemented the pop logic must be manually handled.
*/
- (void)backButtonPressed;
@end
@interface UINavigationController(BackButtonHandler)
@end
@implementation UINavigationController(BackButtonHandler)
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
{
UIViewController *topViewController = self.topViewController;
BOOL wasBackButtonClicked = topViewController.navigationItem == item;
SEL backButtonPressedSel = @selector(backButtonPressed);
if (wasBackButtonClicked && [topViewController respondsToSelector:backButtonPressedSel]) {
[topViewController performSelector:backButtonPressedSel];
return NO;
}
else {
[self popViewControllerAnimated:YES];
return YES;
}
}
@end
Это работает, потому что UINavigationController
получит звонок navigationBar:shouldPopItem:
каждый раз, когда контроллер представления выскочил. Там мы определяем, была ли нажата спина или нет (любая другая кнопка). Единственное, что вам нужно сделать, это реализовать протокол в контроллере вида, где нажата кнопка назад.
Не забудьте вручную вставить контроллер представления внутрь backButtonPressedSel
, если все в порядке.
Если у вас уже есть подкласс UINavigationViewController
и реализовано navigationBar:shouldPopItem:
не волнуйтесь, это не помешает этому.
Вы также можете быть заинтересованы в отключении жеста спины.
if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)]) {
self.navigationController.interactivePopGestureRecognizer.enabled = NO;
}
Это работает для меня в iOS 9.3.x с Swift:
override func didMoveToParentViewController(parent: UIViewController?) {
super.didMoveToParentViewController(parent)
if parent == self.navigationController?.parentViewController {
print("Back tapped")
}
}
В отличие от других решений здесь, это, кажется, не срабатывает неожиданно.
Вы можете использовать обратный вызов кнопки "Назад", например:
- (BOOL) navigationShouldPopOnBackButton
{
[self backAction];
return NO;
}
- (void) backAction {
// your code goes here
// show confirmation alert, for example
// ...
}
Для протокола, я думаю, что это больше того, что он искал...
UIBarButtonItem *l_backButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRewind target:self action:@selector(backToRootView:)];
self.navigationItem.leftBarButtonItem = l_backButton;
- (void) backToRootView:(id)sender {
// Perform some custom code
[self.navigationController popToRootViewControllerAnimated:YES];
}
Лучший способ - использовать методы делегата UINavigationController.
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated
Используя это, вы можете узнать, какой контроллер показывает UINavigationController.
if ([viewController isKindOfClass:[HomeController class]]) {
NSLog(@"Show home controller");
}
Я использовал раствор Pedro Magalhães , кроме
navigationBar:shouldPop
не был вызван, когда я использовал его в таком расширении:
extension UINavigationController: UINavigationBarDelegate {
public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
return self.topViewController?.navigationShouldPopOnBackButton() ?? true
}
Но то же самое в
UINavigationController
подкласс работал нормально.
class NavigationController: UINavigationController, UINavigationBarDelegate {
func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
return self.topViewController?.navigationShouldPopOnBackButton() ?? true
}
Я вижу некоторые другие вопросы, в которых сообщается, что этот метод не вызывается (но другие методы делегата вызываются должным образом) из iOS 13?
Для Swift с UINavigationController:
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
if self.navigationController?.topViewController != self {
print("back button tapped")
}
}
Как сказал Coli88, вы должны проверить протокол UINavigationBarDelegate.
В более общем смысле вы также можете использовать - (void)viewWillDisapear:(BOOL)animated
выполнить пользовательскую работу, когда представление, сохраненное текущим видимым контроллером представления, собирается исчезнуть. К сожалению, это покрыло бы беспокоящие толчок и случаи популярности.
Вы должны проверить протокол UINavigationBarDelegate. В этом случае вы можете использовать навигацию Bar:shouldPopItem: метод.
Как purrrminator
говорит, ответ elitalon
не совсем верно, так как your stuff
будет выполняться даже при программном подключении контроллера.
Решение, которое я нашел до сих пор, не очень хорошее, но оно работает для меня. Кроме того, что elitalon
сказал, я также проверяю, высовываю ли я программно или нет:
- (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];
Спасибо за вашу помощь!
7ynk3r был очень близок к тому, что я использовал в конце, но для этого потребовались некоторые изменения:
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item {
UIViewController *topViewController = self.topViewController;
BOOL wasBackButtonClicked = topViewController.navigationItem == item;
if (wasBackButtonClicked) {
if ([topViewController respondsToSelector:@selector(navBackButtonPressed)]) {
// if user did press back on the view controller where you handle the navBackButtonPressed
[topViewController performSelector:@selector(navBackButtonPressed)];
return NO;
} else {
// if user did press back but you are not on the view controller that can handle the navBackButtonPressed
[self popViewControllerAnimated:YES];
return YES;
}
} else {
// when you call popViewController programmatically you do not want to pop it twice
return YES;
}
}
Я решил эту проблему, добавив UIControl к панели навигации с левой стороны.
UIControl *leftBarItemControl = [[UIControl alloc] initWithFrame:CGRectMake(0, 0, 90, 44)];
[leftBarItemControl addTarget:self action:@selector(onLeftItemClick:) forControlEvents:UIControlEventTouchUpInside];
self.leftItemControl = leftBarItemControl;
[self.navigationController.navigationBar addSubview:leftBarItemControl];
[self.navigationController.navigationBar bringSubviewToFront:leftBarItemControl];
И вам нужно помнить, чтобы удалить его, когда вид исчезнет:
- (void) viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
if (self.leftItemControl) {
[self.leftItemControl removeFromSuperview];
}
}
Это все!
(СВИФТ)
окончательно найденное решение.. метод, который мы искали, это "willShowViewController", который является методом делегата UINavigationController
//IMPORT UINavigationControllerDelegate !!
class PushedController: UIViewController, UINavigationControllerDelegate {
override func viewDidLoad() {
//set delegate to current class (self)
navigationController?.delegate = self
}
func navigationController(navigationController: UINavigationController, willShowViewController viewController: UIViewController, animated: Bool) {
//MyViewController shoud be the name of your parent Class
if var myViewController = viewController as? MyViewController {
//YOUR STUFF
}
}
}
self.navigationController.isMovingFromParentViewController больше не работает на iOS8 и 9, которые я использую:
-(void) viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
if (self.navigationController.topViewController != self)
{
// Is Popping
}
}