ปุ่มโทรกลับใน navigationController ใน iOS


103

ฉันได้ผลักมุมมองไปยังตัวควบคุมการนำทางและเมื่อฉันกดปุ่มย้อนกลับมันจะไปที่มุมมองก่อนหน้าโดยอัตโนมัติ ฉันต้องการทำบางสิ่งเมื่อกดปุ่มย้อนกลับก่อนที่จะเปิดมุมมองออกจากสแต็ก ฟังก์ชันเรียกกลับปุ่มย้อนกลับคืออะไร?



ลองดู [วิธีแก้ปัญหา] [1] ซึ่งยังคงรูปแบบปุ่มย้อนกลับไว้เช่นกัน [1]: stackoverflow.com/a/29943156/3839641
Sarasranglt

คำตอบ:


162

คำตอบของ 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];
}

32
รหัสนี้ไม่ได้ถูกเรียกใช้งานเมื่อผู้ใช้แตะปุ่มย้อนกลับเท่านั้น แต่ในทุก ๆ เหตุการณ์มุมมองจะปรากฏขึ้น (เช่นเมื่อมีปุ่มเสร็จสิ้นหรือปุ่มบันทึกที่ด้านขวา)
เรื่องความหมาย

7
หรือเมื่อก้าวไปสู่มุมมองใหม่
GuybrushThreepwood

สิ่งนี้เรียกอีกอย่างว่าเมื่อผู้ใช้เลื่อนจากขอบด้านซ้าย (interactivePopGestureRecognizer) ในกรณีของฉันฉันกำลังมองหาโดยเฉพาะเมื่อผู้ใช้กดย้อนกลับในขณะที่ไม่ได้แพนกล้องจากขอบด้านซ้าย
Kyle Clegg

2
ไม่ได้หมายความว่าปุ่มย้อนกลับเป็นสาเหตุ อาจเป็นการผ่อนคลายตัวอย่างเช่น
smileBot

1
ฉันมีข้อสงสัยทำไมเราไม่ควรทำเช่นนี้ใน viewDidDisappear?
JohnVanDijk

85

ในความคิดของฉันเป็นทางออกที่ดีที่สุด

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

แต่ใช้ได้กับ iOS5 + เท่านั้น


3
เทคนิคนี้ไม่สามารถแยกความแตกต่างระหว่างการแตะปุ่มย้อนกลับและการคลายตัว
smileBot

วิธี willMoveToParentViewController และ viewWillDisappear ไม่ได้อธิบายว่าคอนโทรลเลอร์ต้องถูกทำลาย didMoveToParentViewController ถูกต้อง
แฮงค์

27

อาจเป็นการดีกว่าที่จะลบล้างปุ่มย้อนกลับเพื่อให้คุณสามารถจัดการกับเหตุการณ์ก่อนที่มุมมองจะปรากฏขึ้นสำหรับสิ่งต่างๆเช่นการยืนยันผู้ใช้

ใน 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 เพื่อดำเนินการเมื่อกดปุ่มย้อนกลับ


UINavigationControllerDelegateไม่ได้มีวิธีการที่จะเรียกว่าเมื่อกลับไปปุ่มเคาะ
เรื่องความหมาย

เทคนิคนี้ช่วยให้สามารถตรวจสอบความถูกต้องของข้อมูลของตัวควบคุมมุมมองและการส่งคืนตามเงื่อนไขจากปุ่มย้อนกลับของตัวควบคุมการนำทาง
gjpc

โซลูชันนี้ทำลายคุณสมบัติการปัดขอบของ iOS 7+
Liron Yahdav

10

บางทีมันอาจจะสายไปหน่อย แต่ฉันก็เคยมีพฤติกรรมแบบนี้เหมือนกัน และวิธีแก้ปัญหาที่ฉันใช้ก็ใช้งานได้ดีในหนึ่งในแอพที่อยู่ใน App Store เนื่องจากยังไม่เคยเห็นใครใช้วิธีการคล้าย ๆ กันเลยอยากจะแชร์ไว้ที่นี่ ข้อเสียของวิธีนี้คือการที่จะต้อง UINavigationControllersubclassing แม้ว่าการใช้Method Swizzlingอาจช่วยหลีกเลี่ยงสิ่งนั้นได้ แต่ฉันก็ไม่ได้ไปไกลขนาดนั้น

UINavigationBarดังนั้นปุ่มย้อนกลับเริ่มต้นที่มีการจัดการจริงโดย เมื่อผู้ใช้แตะที่ปุ่มย้อนกลับUINavigationBarให้ถามผู้ร่วมประชุมว่าควรเปิดด้านบนUINavigationItemด้วยการโทรnavigationBar(_:shouldPop:)หรือไม่ UINavigationControllerใช้สิ่งนี้จริง แต่ไม่ได้ประกาศต่อสาธารณะว่าใช้UINavigationBarDelegate(ทำไม!?) เพื่อสกัดกั้นเหตุการณ์นี้สร้าง subclass ของUINavigationControllerประกาศความสอดคล้องในการและดำเนินการUINavigationBarDelegate navigationBar(_:shouldPop:)กลับมาtrueหากรายการบนสุดควรจะโผล่ กลับมาfalseถ้ามันควรอยู่

มีปัญหาสองประการ ประการแรกคือคุณต้องเรียกUINavigationControllerรุ่นของnavigationBar(_:shouldPop:)ในบางจุด แต่UINavigationBarControllerไม่ได้ประกาศต่อสาธารณะว่าสอดคล้องกับการUINavigationBarDelegateพยายามเรียกมันจะส่งผลให้เกิดข้อผิดพลาดเวลาคอมไพล์ วิธีแก้ปัญหาที่ฉันใช้คือการใช้ Objective-C runtime เพื่อรับการนำไปใช้โดยตรงและเรียกมันว่า โปรดแจ้งให้เราทราบหากใครมีวิธีแก้ปัญหาที่ดีกว่านี้

ปัญหาอื่น ๆ ที่navigationBar(_:shouldPop:)ถูกเรียกก่อนตามมาคือpopViewController(animated:)หากผู้ใช้แตะที่ปุ่มย้อนกลับ การสั่งซื้อกลับหากควบคุมมุมมองเป็น popped popViewController(animated:)โดยการเรียก ในกรณีนี้ฉันใช้บูลีนเพื่อตรวจสอบว่าpopViewController(animated:)ถูกเรียกมาก่อนหรือไม่navigationBar(_:shouldPop:)ซึ่งหมายความว่าผู้ใช้แตะปุ่มย้อนกลับ

นอกจากนี้ฉันสร้างส่วนขยายUIViewControllerเพื่อให้ตัวควบคุมการนำทางถามตัวควบคุมมุมมองว่าควรจะโผล่ขึ้นมาหากผู้ใช้แตะที่ปุ่มย้อนกลับ ดูตัวควบคุมสามารถย้อนกลับfalseและดำเนินการที่จำเป็นและโทรกลับในpopViewController(animated:)ภายหลัง

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

shouldBePopped(_:)และในคุณดูควบคุมการดำเนินการ หากคุณไม่ใช้วิธีนี้พฤติกรรมเริ่มต้นจะแสดงตัวควบคุมมุมมองทันทีที่ผู้ใช้แตะที่ปุ่มย้อนกลับเหมือนปกติ

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

คุณสามารถดูการสาธิตของฉันที่นี่

ป้อนคำอธิบายภาพที่นี่


นี่เป็นวิธีแก้ปัญหาที่ยอดเยี่ยมและควรนำไปไว้ในบล็อกโพสต์! ดูเหมือนว่าจะมากเกินไปสำหรับสิ่งที่ฉันกำลังค้นหาในตอนนี้ แต่ในสถานการณ์อื่น ๆ สิ่งนี้ก็คุ้มค่าที่จะลอง
ASSeeger

9

นี่เป็นวิธีที่ถูกต้องในการตรวจจับสิ่งนี้

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

    }
}

วิธีนี้เรียกว่าเมื่อมุมมองถูกพุชเช่นกัน ดังนั้นการตรวจสอบ parent == nil จึงใช้สำหรับ popping view controller จาก stack


9

ฉันจบลงด้วยการแก้ปัญหานี้ ในขณะที่เราแตะปุ่มย้อนกลับ viewDidDisappear เรียกเมธอด เราสามารถตรวจสอบได้โดยเรียกตัวเลือก 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];

    }
}

อย่าลืม[super viewDidDisappear:animated]
SamB

6

สำหรับ "ก่อนที่จะเปิดมุมมองจากสแต็ก":

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

5

มีวิธีที่เหมาะสมกว่าการถาม viewControllers คุณสามารถกำหนดให้คอนโทรลเลอร์ของคุณเป็นตัวแทนของ 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;
}

4
ไม่ได้ผลสำหรับฉัน .. pitty เพราะมันลีน "*** การยุติแอปเนื่องจากข้อยกเว้น" NSInternalInconsistencyException "ที่ไม่ถูกจับสาเหตุ:" ไม่สามารถตั้งค่าผู้ร่วมประชุมด้วยตนเองบน UINavigationBar ที่จัดการโดยคอนโทรลเลอร์ได้ ""
DynamicDan

สิ่งนี้จะใช้ไม่ได้กับ UINavigationController แต่คุณต้องมี UIViewController มาตรฐานที่มี UINavigationBar อยู่ในนั้น นี่หมายความว่าคุณไม่สามารถใช้ประโยชน์จากตัวควบคุมมุมมองอัตโนมัติหลายตัวที่ผลักดันและโผล่ขึ้นมาที่ NavigationController มอบให้คุณ ขออภัย!
Carlos Guzman

ฉันเพิ่งใช้ UINavigationBar แทน NavigationBarController จากนั้นก็ใช้งานได้ดี ฉันรู้ว่าคำถามเกี่ยวกับ NavigationBarController แต่วิธีนี้เป็นแบบลีน
แอปที่รวมกัน

3

หากคุณไม่สามารถใช้ "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 ที่กำหนดเองดังนั้นในเมธอด viewDidLoad สำหรับ viewController ปกติของคุณให้ทำสิ่งนี้:

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

3

นี่เป็นอีกวิธีหนึ่งที่ฉันนำไปใช้ (ไม่ได้ทดสอบด้วยการทำต่อแบบคลาย แต่มันอาจจะไม่แตกต่างออกไปอย่างที่คนอื่น ๆ ระบุไว้เกี่ยวกับวิธีแก้ไขปัญหาอื่น ๆ ในหน้านี้) เพื่อให้ตัวควบคุมมุมมองหลักดำเนินการก่อนที่ VC ลูกจะผลักดัน ถูกดึงออกจากมุมมอง (ฉันใช้สิ่งนี้สองสามระดับจาก UINavigationController ดั้งเดิม) นอกจากนี้ยังสามารถใช้เพื่อดำเนินการก่อนที่ childVC จะถูกผลักได้อีกด้วย สิ่งนี้มีข้อดีเพิ่มเติมในการทำงานกับปุ่มย้อนกลับของระบบ iOS แทนที่จะต้องสร้าง UIBarButtonItem หรือ UIButton แบบกำหนดเอง

  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;

    }


1

นี่คือสิ่งที่เหมาะกับฉันใน Swift:

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

    super.viewWillDisappear(animated)
}

0

หากคุณใช้ Storyboard และคุณมาจากการผลักดันคุณสามารถลบล้างshouldPerformSegueWithIdentifier:sender:ได้

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.