ฉันได้ผลักมุมมองไปยังตัวควบคุมการนำทางและเมื่อฉันกดปุ่มย้อนกลับมันจะไปที่มุมมองก่อนหน้าโดยอัตโนมัติ ฉันต้องการทำบางสิ่งเมื่อกดปุ่มย้อนกลับก่อนที่จะเปิดมุมมองออกจากสแต็ก ฟังก์ชันเรียกกลับปุ่มย้อนกลับคืออะไร?
ฉันได้ผลักมุมมองไปยังตัวควบคุมการนำทางและเมื่อฉันกดปุ่มย้อนกลับมันจะไปที่มุมมองก่อนหน้าโดยอัตโนมัติ ฉันต้องการทำบางสิ่งเมื่อกดปุ่มย้อนกลับก่อนที่จะเปิดมุมมองออกจากสแต็ก ฟังก์ชันเรียกกลับปุ่มย้อนกลับคืออะไร?
คำตอบ:
คำตอบของ 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 เพื่อดำเนินการเมื่อกดปุ่มย้อนกลับ
UINavigationControllerDelegate
ไม่ได้มีวิธีการที่จะเรียกว่าเมื่อกลับไปปุ่มเคาะ
บางทีมันอาจจะสายไปหน่อย แต่ฉันก็เคยมีพฤติกรรมแบบนี้เหมือนกัน และวิธีแก้ปัญหาที่ฉันใช้ก็ใช้งานได้ดีในหนึ่งในแอพที่อยู่ใน App Store เนื่องจากยังไม่เคยเห็นใครใช้วิธีการคล้าย ๆ กันเลยอยากจะแชร์ไว้ที่นี่ ข้อเสียของวิธีนี้คือการที่จะต้อง UINavigationController
subclassing แม้ว่าการใช้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
}
}
นี่เป็นวิธีที่ถูกต้องในการตรวจจับสิ่งนี้
- (void)willMoveToParentViewController:(UIViewController *)parent{
if (parent == nil){
//do stuff
}
}
วิธีนี้เรียกว่าเมื่อมุมมองถูกพุชเช่นกัน ดังนั้นการตรวจสอบ parent == nil จึงใช้สำหรับ popping view controller จาก stack
ฉันจบลงด้วยการแก้ปัญหานี้ ในขณะที่เราแตะปุ่มย้อนกลับ 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]
สำหรับ "ก่อนที่จะเปิดมุมมองจากสแต็ก":
- (void)willMoveToParentViewController:(UIViewController *)parent{
if (parent == nil){
NSLog(@"do whatever you want here");
}
}
มีวิธีที่เหมาะสมกว่าการถาม 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;
}
หากคุณไม่สามารถใช้ "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;
}
}
นี่เป็นอีกวิธีหนึ่งที่ฉันนำไปใช้ (ไม่ได้ทดสอบด้วยการทำต่อแบบคลาย แต่มันอาจจะไม่แตกต่างออกไปอย่างที่คนอื่น ๆ ระบุไว้เกี่ยวกับวิธีแก้ไขปัญหาอื่น ๆ ในหน้านี้) เพื่อให้ตัวควบคุมมุมมองหลักดำเนินการก่อนที่ VC ลูกจะผลักดัน ถูกดึงออกจากมุมมอง (ฉันใช้สิ่งนี้สองสามระดับจาก UINavigationController ดั้งเดิม) นอกจากนี้ยังสามารถใช้เพื่อดำเนินการก่อนที่ childVC จะถูกผลักได้อีกด้วย สิ่งนี้มีข้อดีเพิ่มเติมในการทำงานกับปุ่มย้อนกลับของระบบ iOS แทนที่จะต้องสร้าง UIBarButtonItem หรือ UIButton แบบกำหนดเอง
ให้ VC หลักของคุณใช้UINavigationControllerDelegate
โปรโตคอลและลงทะเบียนสำหรับข้อความตัวแทน:
MyParentViewController : UIViewController <UINavigationControllerDelegate>
-(void)viewDidLoad {
self.navigationcontroller.delegate = self;
}
ใช้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;
}
หากคุณระบุฟังก์ชันการโทรกลับเฉพาะใน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;
}
นี่คือสิ่งที่เหมาะกับฉันใน Swift:
override func viewWillDisappear(_ animated: Bool) {
if self.navigationController?.viewControllers.index(of: self) == nil {
// back button pressed or back gesture performed
}
super.viewWillDisappear(animated)
}
หากคุณใช้ Storyboard และคุณมาจากการผลักดันคุณสามารถลบล้างshouldPerformSegueWithIdentifier:sender:
ได้