Programing

iOS의 navigationController에서 뒤로 버튼 콜백

lottogame 2020. 8. 22. 11:28
반응형

iOS의 navigationController에서 뒤로 버튼 콜백


내비게이션 컨트롤러에 뷰를 눌렀는데 뒤로 버튼을 누르면 자동으로 이전 뷰로 이동합니다. 스택에서 뷰를 열기 전에 뒤로 버튼을 눌렀을 때 몇 가지 작업을 수행하고 싶습니다. 뒤로 버튼 콜백 기능은 무엇입니까?


William Jockusch의 대답 은 쉬운 트릭 으로이 문제를 해결합니다.

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

제 생각에는 최고의 솔루션입니다.

- (void)didMoveToParentViewController:(UIViewController *)parent
{
    if (![parent isEqual:self.parentViewController]) {
         NSLog(@"Back pressed");
    }
}

하지만 iOS5 +에서만 작동합니다.


사용자 확인과 같은 작업을 위해보기가 팝업 되기 전에 이벤트 처리 할 수 ​​있도록 뒤로 버튼을 재정의하는 것이 좋습니다 .

viewDidLoad에서 UIBarButtonItem을 만들고 self.navigationItem.leftBarButtonItem을 설정하여 sel을 전달합니다.

- (void) viewDidLoad
{
// change the back button to cancel and add an event handler
UIBarButtonItem *backButton = [[UIBarButtonItem alloc] initWithTitle:@”back”
style:UIBarButtonItemStyleBordered
target:self
action:@selector(handleBack:)];

self.navigationItem.leftBarButtonItem = backButton;
[backButton release];

}
- (void) handleBack:(id)sender
{
// pop to root view controller
[self.navigationController popToRootViewControllerAnimated:YES];

}

그런 다음 UIAlertView를 발생시켜 작업을 확인한 다음 뷰 컨트롤러를 팝업하는 등의 작업을 수행 할 수 있습니다.

또는 새 뒤로 버튼을 만드는 대신 뒤로 버튼을 눌렀을 때 작업을 수행하도록 UINavigationController 대리자 메서드를 준수 할 수 있습니다.


이 솔루션으로 끝납니다. 뒤로 버튼을 누르면 viewDidDisappear 메서드가 호출됩니다. true를 반환하는 isMovingFromParentViewController 선택기를 호출하여 확인할 수 있습니다. 데이터를 다시 전달할 수 있습니다 (Delegate 사용). 누군가에게 도움이되기를 바랍니다.

-(void)viewDidDisappear:(BOOL)animated{

    if (self.isMovingToParentViewController) {

    }
    if (self.isMovingFromParentViewController) {
       //moving back
        //pass to viewCollection delegate and update UI
        [self.delegateObject passBackSavedData:self.dataModel];

    }
}

이것이이를 감지하는 올바른 방법입니다.

- (void)willMoveToParentViewController:(UIViewController *)parent{
    if (parent == nil){
        //do stuff

    }
}

이 메서드는 뷰가 푸시 될 때도 호출됩니다. 따라서 parent == nil을 확인하는 것은 스택에서 뷰 컨트롤러를 팝하는 것입니다.


"스택에서 뷰를 팝하기 전에"의 경우 :

- (void)willMoveToParentViewController:(UIViewController *)parent{
    if (parent == nil){
        NSLog(@"do whatever you want here");
    }
}

viewController를 요청하는 것보다 더 적절한 방법이 있습니다. 컨트롤러를 뒤로 버튼이있는 navigationBar의 델리게이트로 만들 수 있습니다. 여기에 예가 있습니다. 뒤로 버튼 누르기를 처리하려는 컨트롤러의 구현에서 UINavigationBarDelegate 프로토콜을 구현할 것임을 알려줍니다.

@interface MyViewController () <UINavigationBarDelegate>

그런 다음 초기화 코드의 어딘가 (아마도 viewDidLoad)에서 컨트롤러를 탐색 모음의 대리자로 만듭니다.

self.navigationController.navigationBar.delegate = self;

마지막으로 shouldPopItem 메서드를 구현합니다. 이 메서드는 뒤로 버튼을 누르면 바로 호출됩니다. 스택에 여러 컨트롤러 또는 탐색 항목이있는 경우 예상 할 때만 사용자 지정 작업을 수행 할 수 있도록 해당 탐색 항목 중 어떤 항목이 팝업되는지 확인하고 싶을 것입니다. 예를 들면 다음과 같습니다.

-(BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
{
    NSLog(@"Back button got pressed!");
    //if you return NO, the back button press is cancelled
    return YES;
}

"viewWillDisappear"또는 유사한 메서드를 사용할 수없는 경우 UINavigationController를 하위 클래스로 지정하십시오. 다음은 헤더 클래스입니다.

#import <Foundation/Foundation.h>
@class MyViewController;

@interface CCNavigationController : UINavigationController

@property (nonatomic, strong) MyViewController *viewController;

@end

구현 클래스 :

#import "CCNavigationController.h"
#import "MyViewController.h"

@implementation CCNavigationController {

}
- (UIViewController *)popViewControllerAnimated:(BOOL)animated {
    @"This is the moment for you to do whatever you want"
    [self.viewController doCustomMethod];
    return [super popViewControllerAnimated:animated];
}

@end

반면에이 viewController를 사용자 지정 NavigationController에 연결해야하므로 일반 viewController에 대한 viewDidLoad 메서드에서 다음을 수행합니다.

@implementation MyViewController {
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        ((CCNavigationController*)self.navigationController).viewController = self;
    }
}

여기에 내가 구현 한 또 다른 방법이 있습니다. 뷰 스택에서 튀어 나옵니다 (원래 UINavigationController에서 몇 수준 아래로 사용했습니다). 이것은 또한 childVC가 푸시되기 전에 작업을 수행하는 데 사용될 수 있습니다. 이는 사용자 정의 UIBarButtonItem 또는 UIButton을 생성하는 대신 iOS 시스템 뒤로 버튼으로 작업 할 수있는 추가 이점이 있습니다.

  1. 부모 VC가 UINavigationControllerDelegate프로토콜을 채택하고 위임 메시지를 등록하도록합니다.

    MyParentViewController : UIViewController <UINavigationControllerDelegate>
    
    -(void)viewDidLoad {
        self.navigationcontroller.delegate = self;
    }
    
  2. UINavigationControllerDelegate인스턴스 메서드를에서 구현하십시오 MyParentViewController.

    - (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC {
        // Test if operation is a pop; can also test for a push (i.e., do something before the ChildVC is pushed
        if (operation == UINavigationControllerOperationPop) {
            // Make sure it's the child class you're looking for
            if ([fromVC isKindOfClass:[ChildViewController class]]) {
                // Can handle logic here or send to another method; can also access all properties of child VC at this time
                return [self didPressBackButtonOnChildViewControllerVC:fromVC];
            }
        }
        // If you don't want to specify a nav controller transition
        return nil;
    }
    
  3. 위의 UINavigationControllerDelegate인스턴스 메서드 에서 특정 콜백 함수를 지정하는 경우

    -(id <UIViewControllerAnimatedTransitioning>)didPressBackButtonOnAddSearchRegionsVC:(UIViewController *)fromVC {
        ChildViewController *childVC = ChildViewController.new;
        childVC = (ChildViewController *)fromVC;
    
        // childVC.propertiesIWantToAccess go here
    
        // If you don't want to specify a nav controller transition
        return nil;
    

    }


Maybe it's a little too late, but I also wanted the same behavior before. And the solution I went with works quite well in one of the apps currently on the App Store. Since I haven't seen anyone goes with similar method, I would like to share it here. The downside of this solution is that it requires subclassing UINavigationController. Though using Method Swizzling might help avoiding that, I didn't go that far.

So, the default back button is actually managed by UINavigationBar. When a user taps on the back button, UINavigationBar ask its delegate if it should pop the top UINavigationItem by calling navigationBar(_:shouldPop:). UINavigationController actually implement this, but it doesn't publicly declare that it adopts UINavigationBarDelegate (why!?). To intercept this event, create a subclass of UINavigationController, declare its conformance to UINavigationBarDelegate and implement navigationBar(_:shouldPop:). Return true if the top item should be popped. Return false if it should stay.

There are two problems. The first is that you must call the UINavigationController version of navigationBar(_:shouldPop:) at some point. But UINavigationBarController doesn't publicly declare it conformance to UINavigationBarDelegate, trying to call it will result in a compile time error. The solution I went with is to use Objective-C runtime to get the implementation directly and call it. Please let me know if anyone has a better solution.

The other problem is that navigationBar(_:shouldPop:) is called first follows by popViewController(animated:) if the user taps on the back button. The order reverses if the view controller is popped by calling popViewController(animated:). In this case, I use a boolean to detect if popViewController(animated:) is called before navigationBar(_:shouldPop:) which mean that the user has tapped on the back button.

Also, I make an extension of UIViewController to let the navigation controller ask the view controller if it should be popped if the user taps on the back button. View controllers can return false and do any necessary actions and call popViewController(animated:) later.

class InterceptableNavigationController: UINavigationController, UINavigationBarDelegate {
    // If a view controller is popped by tapping on the back button, `navigationBar(_:, shouldPop:)` is called first follows by `popViewController(animated:)`.
    // If it is popped by calling to `popViewController(animated:)`, the order reverses and we need this flag to check that.
    private var didCallPopViewController = false

    override func popViewController(animated: Bool) -> UIViewController? {
        didCallPopViewController = true
        return super.popViewController(animated: animated)
    }

    func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
        // If this is a subsequence call after `popViewController(animated:)`, we should just pop the view controller right away.
        if didCallPopViewController {
            return originalImplementationOfNavigationBar(navigationBar, shouldPop: item)
        }

        // The following code is called only when the user taps on the back button.

        guard let vc = topViewController, item == vc.navigationItem else {
            return false
        }

        if vc.shouldBePopped(self) {
            return originalImplementationOfNavigationBar(navigationBar, shouldPop: item)
        } else {
            return false
        }
    }

    func navigationBar(_ navigationBar: UINavigationBar, didPop item: UINavigationItem) {
        didCallPopViewController = false
    }

    /// Since `UINavigationController` doesn't publicly declare its conformance to `UINavigationBarDelegate`,
    /// trying to called `navigationBar(_:shouldPop:)` will result in a compile error.
    /// So, we'll have to use Objective-C runtime to directly get super's implementation of `navigationBar(_:shouldPop:)` and call it.
    private func originalImplementationOfNavigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
        let sel = #selector(UINavigationBarDelegate.navigationBar(_:shouldPop:))
        let imp = class_getMethodImplementation(class_getSuperclass(InterceptableNavigationController.self), sel)
        typealias ShouldPopFunction = @convention(c) (AnyObject, Selector, UINavigationBar, UINavigationItem) -> Bool
        let shouldPop = unsafeBitCast(imp, to: ShouldPopFunction.self)
        return shouldPop(self, sel, navigationBar, item)
    }
}

extension UIViewController {
    @objc func shouldBePopped(_ navigationController: UINavigationController) -> Bool {
        return true
    }
}

And in you view controllers, implement shouldBePopped(_:). If you don't implement this method, the default behavior will be to pop the view controller as soon as the user taps on the back button just like normal.

class MyViewController: UIViewController {
    override func shouldBePopped(_ navigationController: UINavigationController) -> Bool {
        let alert = UIAlertController(title: "Do you want to go back?",
                                      message: "Do you really want to go back? Tap on \"Yes\" to go back. Tap on \"No\" to stay on this screen.",
                                      preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "No", style: .cancel, handler: nil))
        alert.addAction(UIAlertAction(title: "Yes", style: .default, handler: { _ in
            navigationController.popViewController(animated: true)
        }))
        present(alert, animated: true, completion: nil)
        return false
    }
}

You can look at my demo here.

enter image description here


This is what it works for me in Swift:

override func viewWillDisappear(_ animated: Bool) {
    if self.navigationController?.viewControllers.index(of: self) == nil {
        // back button pressed or back gesture performed
    }

    super.viewWillDisappear(animated)
}

If you're using a Storyboard and you're coming from a push segue, you could also just override shouldPerformSegueWithIdentifier:sender:.

참고URL : https://stackoverflow.com/questions/5217992/back-button-callback-in-navigationcontroller-in-ios

반응형