I've got a tab bar application and I need to know when and what button a user taps on the tab bar as to display the appropriate notifications and such.
In short: How would I go about detecting the index of a tapped UITabBarItem on a UITabBar?
Thanks in advance!
The answer depends on whether or not the UITabBar is managed by a UITabBarController or not.
Case 1 - UITabBar is already handled by a UITabBarController
Implement the UITabBarControllerDelegate protocol. Specifically the tabBarContoller:didSelectViewController: method. Set an instance of your class that implements the protocol as the delegate of the UITabBarController.
- (void)tabBarController:(UITabBarController *)theTabBarController didSelectViewController:(UIViewController *)viewController {
NSUInteger indexOfTab = [theTabBarController.viewControllers indexOfObject:viewController];
NSLog(#"Tab index = %u (%u)", (int)indexOfTab);
}
In this case you have to be aware of the special situation where you have enough controllers in the tab controller to cause the "More" tab to be displayed. In that case you'll receive a call to the tabBarController:didSelectViewController: with a view controller that isn't in the list (it's an instance of an internal UIKit class UIMoreNavigationController). In that case the indexOfTab in my sample will be NSNotFound.
Case 2 - UITabBar is NOT already handled by a UITabBarController
Implement the UITabBarDelegate protocol. Specifically the tabBar:didSelectItem: method. Set an instance of your class that implements the protocol as the delegate of the UITabBar.
- (void)tabBar:(UITabBar *)theTabBar didSelectItem:(UITabBarItem *)item {
NSUInteger indexOfTab = [[theTabBar items] indexOfObject:item];
NSLog(#"Tab index = %u", (int)indexOfTab);
}
EDIT: Modified the method parameter variables to eliminate the OP's compilation warning about tabBarController being hidden.
SWIFT:
// somewhere inside your TabBarViewController
//...
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
let indexOfTab = tabBar.items?.index(of: item)
print("pressed tabBar: \(String(describing: indexOfTab))")
}
SWIFT 4:
I prefer
// somewhere inside your TabBarViewController
//...
func tabBarController(_ tabBarController: UITabBarController,
shouldSelect viewController: UIViewController) -> Bool{
let index = tabBarController.viewControllers?.index(of: viewController)
return true// you decide
}
There's a method defined in the UITabBarDelegate protocol called tabBar:didSelectItem:, which will notify you of which and when a UITabBarItem is selected (tapped).
I did it like this :
This is in a custom class which extends UITabBarController
.h
#interface CustomTabBarController : UITabBarController<UITabBarDelegate>
.m
-(void)tabBar:(UITabBar *)theTabBar didSelectItem:(UIViewController *)viewController
{
NSLog(#"Tab index = %# ", theTabBar.selectedItem);
for(int i = 0; i < theTabBar.items.count; i++)
{
if(theTabBar.selectedItem == theTabBar.items[i])
{
NSLog(#"%d",i);// this will give the selected tab
}
}
//NSlog(#"Items = %#", theTabBar.items[0]);
}
Simple extension for Swift 4:
extension UITabBarController {
func getSelectedTabIndex() -> Int? {
if let selectedItem = self.tabBar.selectedItem {
return self.tabBar.items?.firstIndex(of: selectedItem)
}
return nil
}
}
And usage:
let index = getSelectedTabIndex()
Related
Ive been searching and i cant seem to find out how to disable all tab bar items EXCEPT the home button from being used until a user logs in or creates and account
You can do something like below, create a custom class(TabBarController), extend it from UITabBarController, and write code inside TabBarController class.
Assign TabBarController class to your UITabBarController
extension TabBarController: UITabBarControllerDelegate{
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
// allow your desired controller to be tapped
if tabBarController.selectedIndex == indexOfHomeControllerInTabBar {
return true
}
return false
}
}
Note: Apple doesn't recommend blocking tabbars, for more info check this link https://developer.apple.com/design/human-interface-guidelines/ios/bars/tab-bars/
here is my approach
note that it is kinda a hack way so you might modify as per your needs
you will conform to UITabBarControllerDelegate and make your VC the delegate for it in the viewDidLoad
then in the "didSelect viewController" delegate method callback you will do your logic and override the selected index as below code
class ViewController: UIViewController, UITabBarControllerDelegate {
// MARK: Lifecycle Methods
override func viewDidLoad() {
super.viewDidLoad()
self.tabBarController?.delegate = self
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
if /* your logged in logic */ {
self.tabBarController?.selectedIndex = 0 /* assuming that the home is at index 0 */
}
}
}
note if you did this step in base VC it will be much better and code saving for you
You can loop for all items in your Tabbar and disable items you want
for i in 0..<tabbarController.tabBar.items!.count {
let item = tabbarController.tabBar.items![i]
item.isEnabled = i == indexOfHomeTab
}
Put this somewhere in viewDidLoad()
if let viewControllers = self.tabBarController?.viewControllers {
for viewController in viewControllers {
if viewController != viewControllers[0] { // assuming your homeViewController index is 0
tabBarController?.tabBarItem.isEnabled = false
}
}
}
The short answer is probably "Don't do that." Tabs in a tab bar are supposed to let the user switch between always-available screen at the top level of your UI. If you read Apple's HIG I suspect you will find that what you are trying to do is not recommended.
Better to have each of the other screens show some sort of disabled/inactive state.
I am trying to pass data BACK TO previous viewController.
Does anyone know how to pass data back from ViewController B to ViewController A? So I want a string to go 'from' BIDAddTypeOfDealViewController to BIDDCCreateViewController. A user edits viewController B and I want that edited data back in ViewController A where I then use it.
I am using the 'passing data back' section of this answer. How mine differs: Point 3 and 6 just mentions when views are popped so I have put that code in viewWillDisappear. I think that is correct?
Also on Point 6 I did not initialise with nib as that is old. I'm using storyboards. And I did not add that last line as I do not believe I would have to push it. Pressing a button on my storyboard already takes me forward.
I think the problem may arise in BIDDCCreateViewController, I have the method but I cannot run it. To run a method it should go [self method]. I am unable to do that. Well that is just what I am guessing.
It compiles and runs fine just nothing is logged, so I don't know if it works.
UPDATE: I am unable to get the 'sendDataToA' method to execute.
#import <UIKit/UIKit.h>
#import "BIDAddTypeOfDealViewController.h"
#interface BIDDCCreateViewController : UIViewController
#property (strong, nonatomic) NSString *placeId;
- (IBAction)gotoBViewController:(id)sender;
#end
#import "BIDDCCreateViewController.h"
#import "BIDAddTypeOfDealViewController.h"
#implementation BIDDCCreateViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
NSLog(#"SUCCESSFULLY PASSED PLACE ID: %#", self.placeId);
}
-(void)sendDataToA:(NSString *)myStringData
{
NSLog(#"Inside sendDataToA");
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Your string Data Showing" message:myStringData delegate:self cancelButtonTitle:#"Ok " otherButtonTitles:nil];
[alert show];
}
- (IBAction)gotoBViewController:(id)sender {
NSLog(#"pressed");
BIDAddTypeOfDealViewController *bidAddType = [[BIDAddTypeOfDealViewController alloc]init];
bidAddType.delegate = self;
}
#end
#protocol senddataProtocol <NSObject>
-(void)sendDataToA:(NSString *)myStringData;
#end
#import <UIKit/UIKit.h>
#interface BIDAddTypeOfDealViewController : UIViewController <UITextFieldDelegate>//Using this delegate for data a user inputs
#property(nonatomic,assign)id delegate;
//other textfield outlets not relevant
- (IBAction)chooseDiscountDeal:(id)sender;
#end
#import "BIDAddTypeOfDealViewController.h"
#interface BIDAddTypeOfDealViewController ()
#end
#implementation BIDAddTypeOfDealViewController
#synthesize delegate;
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
}
-(void)viewWillDisappear:(BOOL)animated
{
[delegate sendDataToA:#"Apple"];
}
#end
You can use a delegate. So in your ViewController B you need to create a protocol that sends data back to your ViewController A. Your ViewController A would become a delegate of ViewController B.
If you are new to objective C, please look at What is Delegate.
Create protocol in ViewControllerB.h :
#import <UIKit/UIKit.h>
#protocol senddataProtocol <NSObject>
-(void)sendDataToA:(NSArray *)array; //I am thinking my data is NSArray, you can use another object for store your information.
#end
#interface ViewControllerB : UIViewController
#property(nonatomic,assign)id delegate;
ViewControllerB.m
#synthesize delegate;
-(void)viewWillDisappear:(BOOL)animated
{
[delegate sendDataToA:yourdata];
}
in your ViewControllerA : when you go to ViewControllerB
ViewControllerA *acontollerobject=[[ViewControllerA alloc] initWithNibName:#"ViewControllerA" bundle:nil];
acontollerobject.delegate=self; // protocol listener
[self.navigationController pushViewController:acontollerobject animated:YES];
and define your function:
-(void)sendDataToA:(NSArray *)array
{
// data will come here inside of ViewControllerA
}
Edited :
You can See this example : How you can Pass data back to previous viewcontroller: Tutorial link
A shorter and simpler method than protocol/delegate is to create a closure:
For sending a String back in my case.
In ViewControllerA:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let viewControllerB = segue.destination as? ViewControllerB {
viewControllerB.callback = { message in
//Do what you want in here!
}
}
}
In ViewControllerB:
var callback : ((String) -> Void)?
#IBAction func done(sender: AnyObject) {
callback?("Hi")
self.dismiss(animated: true, completion: nil)
}
Swift: Sending data back using the delegate pattern
My full answer that covers passing data both ways is here. My answer explaining the delegate pattern is here.
To pass data back from the second view controller to the first view controller, you use a protocol and a delegate. This video is a very clear walk though of that process:
YouTube tutorial: iOS Swift Basics Tutorial: Protocols and Delegates. But also read this post to make sure you don't get into a strong reference cycle.
The following is an example based on the video (with a few modifications).
Create the storyboard layout in the Interface Builder. Again, to make the segue, you just Control drag from the button to the Second View Controller. Set the segue identifier to showSecondViewController. Also, don't forget to hook up the outlets and actions using the names in the following code.
First View Controller
The code for the First View Controller is
import UIKit
class FirstViewController: UIViewController, DataEnteredDelegate {
#IBOutlet weak var label: UILabel!
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showSecondViewController" {
let secondViewController = segue.destinationViewController as! SecondViewController
secondViewController.delegate = self
}
}
func userDidEnterInformation(info: String) {
label.text = info
}
}
Note the use of our custom DataEnteredDelegate protocol.
Second View Controller and Protocol
The code for the second view controller is
import UIKit
// protocol used for sending data back
protocol DataEnteredDelegate: class {
func userDidEnterInformation(info: String)
}
class SecondViewController: UIViewController {
// making this a weak variable so that it won't create a strong reference cycle
weak var delegate: DataEnteredDelegate? = nil
#IBOutlet weak var textField: UITextField!
#IBAction func sendTextBackButton(sender: UIButton) {
// call this method on whichever class implements our delegate protocol
delegate?.userDidEnterInformation(textField.text!)
// go back to the previous view controller
self.navigationController?.popViewControllerAnimated(true)
}
}
Note that the protocol is outside of the View Controller class.
That's it. Running the app now you should be able to send data back from the second view controller to the first.
As Erhan Demirci answered, you can use delegates. Delegates are helpful when you want to pass data to a single view controller.
NSNotificationCenter is another convenient way to transfer data between viewcontrollers/objects. This is very helpful in broadcasting data within the application.
read documentation here.
Edit:
Use #Erhan's solution above. Not this one. This is not a good solution.
This will help. Write this in your ViewControllerB.
// Get array of current navigation stack
NSArray *arrayViewControllers = [self.navigationController viewControllers];
// Get previous viewController object from it
YOUR_VIEW_CONTROLLER_NAME *objViewController = (YOUR_VIEW_CONTROLLER_NAME *)[arrayViewControllers objectAtIndex:arrayViewControllers.count-2];
// For safety this check is needed. whether it the class that you want or not.
if ([objViewController isKindOfClass:[YOUR_VIEW_CONTROLLER_NAME class]])
{
// Access properties of YOUR_VIEW_CONTROLLER_NAME here
objViewController.yourProperty = YOUR_VALUE;
}
Custom delegate is the best option to move data but you can try this also.
You can use NSUserDefaults for Moving the data any where you want.
Swift 3 Code
UserDefaults.standard.set(<Value>, forKey: <Key>)
// To set data
UserDefaults.standard.object(forKey: <Key>)
// To get data
You can also use NSNotification for move data.
NotificationCenter.default.post(name: Notification.Name(rawValue: "refresh"), object: myDict)
NotificationCenter.default.addObserver(self, selector: #selector(refreshList(_:)), name: NSNotification.Name(rawValue: "refresh"), object: nil)
There is protocol there is closure. With closure, we need to avoid memory leaks by using weak self (or unowned self). With protocol, there would be one per viewController that you want to "monitor", end up with dozens of delegate to implement. Here I've another simple solutions in Swift:
Inside a new file or existing one (for example: UIViewController+Extensions.swift), create this protocol:
protocol ViewControllerBackDelegate: class {
func back(from viewController: UIViewController)
}
Inside LEVEL-2 viewController, where you want a callback when Back is pressed from:
class LevelTwoViewController: UIViewController {
// making this a weak variable so that it won't create a strong reference cycle
weak var delegate: ViewControllerBackDelegate? = nil
override func willMove(toParentViewController parent: UIViewController?) {
super.willMove(toParentViewController: parent)
if (parent == nil) {
delegate?.back(from: self)
}
}
}
Since delegate is optional, you may add this code to a base class of your view controllers. I would add to where it needs to be.
In your LEVEL-1 viewController, assume you calling LEVEL-2 via a segue in Storyboard:
class LevelOneViewController: UIViewController {
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if (segue.identifier == "Go to Level 2") {
if let vc = segue.destination as? LevelTwoViewController {
vc.selectedItems = self.selectedItems // passing data-in
vc.delegate = self
}
}
// repeat `if` for another sub-level view controller
}
}
extension LevelOneViewController: ViewControllerBackDelegate {
func back(from viewController: UIViewController) {
if let vc = viewController as? LevelTwoViewController {
self.selectedItems = vc.selectedItems
// call update if necessary
}
// repeat `if` for another sub-level view controller
}
}
only one protocol is required.
only one extension per first-level viewController.
no modifications to sub-level viewController if more/less data need to return
handle data-out just like data-in in prepare(for:sender:)
//FirstViewController
import UIKit
class FirstViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func viewWillAppear(_ animated: Bool) {
}
#IBAction func pushToSecond(_ sender: Any) {
if let vc = storyboard?.instantiateViewController(withIdentifier: "SecondViewController")as? SecondViewController {
vc.callBack = { (id: String,name: String,age: Int) in
print(id,name,age)
}
self.navigationController?.pushViewController(vc, animated: true)
}
}
}
// //SecondViewController
import UIKit
class SecondViewController: UIViewController {
var callBack: ((_ id: String, _ name: String, _ age: Int)-> Void)?
#IBAction func BackToFirstWitData(_ sender: Any) {
callBack?("1","Test",22)
self.navigationController?.popViewController(animated: true)
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
Here is how I would do it.
#interface ViewControllerA:UIViewController
#property(strong, nonatomic) ViewControllerB * recieverB;
#end
#implementation ViewControllerA
//implement class
- (void)prepareForSegue:(UIStoryboardSegue *) sender:(id)sender
{
segue.destinationViewController.recieverA = self;
}
-(void)viewDidLoad
{
//stop strong refrence cycle
self.viewControllerB = nil;
}
#end
Class B
#interface ViewControllerB:UIViewController
#property(strong, nonatomic, getter = parentClass) ViewControllerB * recieverA;
#end
#implementation ViewControllerB
//implement class
- (void)viewWillDisappear:(BOOL)animated
{
parentClass.recieverB = self;
//now class A will have an instance on class b
}
#end
I didn't put the #import
I want to get the current view controller at present in page view controller. how it can be done. Does it have some delegate to call or what.
I just had the same problem. Looks like the current controller is last in the list after you did change the page. This worked for me, but I don't know if it's always true.
- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed
{
UIViewController *vc = [pageViewController.viewControllers lastObject];
}
Try get the first view controller in pageViewController.viewControllers
if let vc = pageViewController.viewControllers?[0] {
... vc is current controller...
}
If you want to handle page changes, do it in a delegate method:
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if let vc = pageViewController.viewControllers?[0] {
... vc is current controller...
}
}
if you are in any UIView class (as UIButton, UITable or any other subclass) that is a subView of the UIViewController.view (or of any subView.subView.subView... of it)
you can check if there's a UIViewController in the superVew(s) chain, and stop when you find a UIViewController
something like this:
UIViewController* controllerFound = nil;
for (UIView* next = [self superview]; next; next = next.superview) {
UIResponder* nextResponder = [next nextResponder];
if ([nextResponder isKindOfClass:[UIViewController class]]) {
controllerFound = (UIViewController*)nextResponder;
}
}
I have a tabbar controller with three tabs . In first tab I have a navigation controller . Now User navigates in the first tab to do some payment so I have disabled the default back buttons cause I dont want user to use back button in between transaction. But when user presses the tab again he/she navigates to the root view . How can I detect the tabbar selection or how do I avoid loading the tab again ?
Please help me on this !! Thank You !!
Note: I am not sure if my question has been already answered on stackoverflow in some other post but I did search and did not get any answer . If so , please feel free to redirect me to that answer and delete this post . Thanx !
Check the UITabBarControllerDelegate Protocol Reference.
The basic idea is that the tabBarController:shouldSelectViewController: selector in your UITabBarController delegate is called whenever the user clicks on tab item.
Thus, by appropriately defining that method, you get a chance to do your own processing before the current view controller is replaced by the one the user selected by clicking in the tab bar.
So, simply return NO from this selector in case you wish to prevent the current view controller to be replaced, i.e. when a transaction is ongoing.
In this way you can open any controller or perform any action on the selection of specific index.
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
let selectedIndex = tabBarController.viewControllers?.firstIndex(of: viewController)!
if selectedIndex == 1{
//Do any thing.
return false
}
return true
}
You have to do it this way..
- (BOOL)tabBarController:(UITabBarController *)tbc shouldSelectViewController:(UIViewController *)vc
{
UIViewController *tbSelectedController = tbc.selectedViewController;
if ([tbSelectedController isEqual:vc])
{
return NO;
}
return YES;
}
In Swift 5:
Continuing Talha Rasool's answer, don't forget to delegate self in viewDidLoad function. This will let current MainTabBarController to handle all the delegate methods
import UIKit
class MainTabBarController: UITabBarController, UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self // Delegate self to handle delegate methods.
}
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
let selectedIndex = tabBarController.viewControllers?.firstIndex(of: viewController)!
if selectedIndex == 1{
//Do anything.
return false
}
return true
}
}
When user navigate to payment controller , you can hide the tabbar.
Use this code in your code , when you navigate to another view
yourcontroller.hidebottombarwhenpushed=YES;
I need to know when my view controller is about to get popped from a nav stack so I can perform an action.
I can't use -viewWillDisappear, because that gets called when the view controller is moved off screen for ANY reason (like a new view controller being pushed on top).
I specifically need to know when the controller is about to be popped itself.
Any ideas would be awesome, thanks in advance.
Override the viewWillDisappear method in the presented VC, then check the isMovingFromParentViewController flag within the override and do specific logic. In my case I'm hiding the navigation controllers toolbar. Still requires that your presented VC understand that it was pushed though so not perfect.
Fortunately, by the time the viewWillDisappear method is called, the viewController has already been removed from the stack, so we know the viewController is popping because it's no longer in the self.navigationController.viewControllers
Swift 4
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if let nav = self.navigationController {
let isPopping = !nav.viewControllers.contains(self)
if isPopping {
// popping off nav
} else {
// on nav, not popping off (pushing past, being presented over, etc.)
}
} else {
// not on nav at all
}
}
Original Code
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if ((self.navigationController) &&
(![self.navigationController.viewControllers containsObject:self])) {
NSLog(#"I've been popped!");
}
}
Try overriding willMoveToParentViewController: (instead of viewWillDisappear:) in your custom subclass of UIViewController.
Called just before the view controller is added or removed from a container view controller.
- (void)willMoveToParentViewController:(UIViewController *)parent
{
[super willMoveToParentViewController:parent];
if (!parent) {
// `self` is about to get popped.
}
}
I don't think there is an explicit message for this, but you could subclass the UINavigationController and override - popViewControllerAnimated (although I haven't tried this before myself).
Alternatively, if there are no other references to the view controller, could you add to its - dealloc?
This is working for me.
- (void)viewDidDisappear:(BOOL)animated
{
if (self.parentViewController == nil) {
NSLog(#"viewDidDisappear doesn't have parent so it's been popped");
//release stuff here
} else {
NSLog(#"PersonViewController view just hidden");
}
}
You can catch it here.
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
if (viewController == YourAboutToAppearController) {
// do something
}
}
This will fire just before the display of the new View. Nobody's moved yet. I use all the time to do magic in front of the asinine NavigationController. You can set titles and button titles and do whatever there.
I have the same problem. I tried with viewDisDisappear, but I don't have the function get called :( (don't know why, maybe because all my VC is UITableViewController).
The suggestion of Alex works fine but it fails if your Navigation controller is displayed under the More tab. In this case, all VCs of your nav controllers have the navigationController as UIMoreNavigationController, not the navigation controller you have subclassed, so you will not be notified by the nav when a VC is about to popped.
Finaly, I solved the problem with a category of UINavigationController, just rewrite - (UIViewController *)popViewControllerAnimated:(BOOL)animated
- (UIViewController *)popViewControllerAnimated:(BOOL)animated{
NSLog(#"UINavigationController(Magic)");
UIViewController *vc = self.topViewController;
if ([vc respondsToSelector:#selector(viewControllerWillBePopped)]) {
[vc performSelector:#selector(viewControllerWillBePopped)];
}
NSArray *vcs = self.viewControllers;
UIViewController *vcc = [vcs objectAtIndex:[vcs count] - 2];
[self popToViewController:vcc animated:YES];
return vcc;}
It works well for me :D
I tried this:
- (void) viewWillDisappear:(BOOL)animated {
// If we are disappearing because we were removed from navigation stack
if (self.navigationController == nil) {
// YOUR CODE HERE
}
[super viewWillDisappear:animated];
}
The idea is that at popping, the view controller's navigationController is set to nil.
So if the view was to disappear, and it longer has a navigationController, I concluded it was popped. (might not work in other scenarios).
Can't vouch that viewWillDisappear will be called upon popping, as it is not mentioned in the docs. I tried it when the view was top view, and below top view - and it worked in both.
Good luck,
Oded.
Subclass UINavigationController and override popViewController:
Swift 3
protocol CanPreventPopProtocol {
func shouldBePopped() -> Bool
}
class MyNavigationController: UINavigationController {
override func popViewController(animated: Bool) -> UIViewController? {
let viewController = self.topViewController
if let canPreventPop = viewController as? CanPreventPopProtocol {
if !canPreventPop.shouldBePopped() {
return nil
}
}
return super.popViewController(animated: animated)
}
//important to prevent UI thread from freezing
//
//if popViewController is called by gesture recognizer and prevented by returning nil
//UI will freeze after calling super.popViewController
//so that, in order to solve the problem we should not return nil from popViewController
//we interrupt the call made by gesture recognizer to popViewController through
//returning false on gestureRecognizerShouldBegin
//
//tested on iOS 9.3.2 not others
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
let viewController = self.topViewController
if let canPreventPop = viewController as? CanPreventPopProtocol {
if !canPreventPop.shouldBePopped() {
return false
}
}
return true
}
}
You can use this one:
if(self.isMovingToParentViewController)
{
NSLog(#"Pushed");
}
else
{
NSLog(#"Popped");
}
You can observe the notification:
- (void)viewDidLoad{
[super viewDidLoad];
[NSNotificationCenter.defaultCenter addObserver:self selector:#selector(navigationControllerWillShowViewController:) name:#"UINavigationControllerWillShowViewControllerNotification" object:nil];
}
- (void)navigationControllerDidShowViewController:(NSNotification *)notification{
UIViewController *lastVisible = notification.userInfo[#"UINavigationControllerLastVisibleViewController"];
if(lastVisible == self){
// we are being popped
}
}
I needed to also prevent from popping sometimes so the best answer for me was written by Orkhan Alikhanov. But it did not work because the delegate was not set, so I made the final version:
import UIKit
class CustomActionsNavigationController: UINavigationController,
UIGestureRecognizerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = self
}
override func popViewController(animated: Bool) -> UIViewController? {
if let delegate = topViewController as? CustomActionsNavigationControllerDelegate {
guard delegate.shouldPop() else { return nil }
}
return super.popViewController(animated: animated)
}
// important to prevent UI thread from freezing
//
// if popViewController is called by gesture recognizer and prevented by returning nil
// UI will freeze after calling super.popViewController
// so that, in order to solve the problem we should not return nil from popViewController
// we interrupt the call made by gesture recognizer to popViewController through
// returning false on gestureRecognizerShouldBegin
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if let delegate = topViewController as? CustomActionsNavigationControllerDelegate {
if !delegate.shouldPop() {
return false
}
}
// This if statement prevents navigation controller to pop when there is only one view controller
if viewControllers.count == 1 {
return false
}
return true
}
}
protocol CustomActionsNavigationControllerDelegate {
func shouldPop() -> Bool
}
UPDATE
I have added viewControllers.count == 1 case, because if there is one controller in the stack and user makes the gesture, it will freeze the UI of your application.
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
const BOOL removingFromParent = ![self.navigationController.viewControllers containsObject:self.parentViewController];
if ( removingFromParent ) {
// cleanup
}
}
Maybe you could use UINavigationBarDelegate's navigationBar:shouldPopItem protocol method.
Try making this check in viewwilldisappear
if ([self.navigationController.viewControllers indexOfObject:self] == NSNotFound) {
//popping of this view has happend.
}