View Controller Containment shared parent objects - ios

I am using view controller containment, and as part of my implementation a child view controller needs to disable a bar button item of the navigation controller of the parent view controller. The theory is that being a child view controller, I had access to the same navigation controller (in my context anyway) as the parent view controller's.
From debugging I can see that self.navigationController and self.parentViewController.navigationController were set to the same address.
For example:
NSLog(#"%# - %#", self.navigationController,
self.parentViewController.navigationController);
NSLog(#"%# - %#", self.navigationItem,
self.parentViewController.navigationItem);
NSLog(#"%# - %#", self.navigationController.navigationItem,
self.parentViewController.navigationController.navigationItem);
Resulted in the following console log:
<UINavigationController: 0xc482290> - <UINavigationController: 0xc482290>
<UINavigationItem: 0xa5f3620> - <UINavigationItem: 0xc482490>
<UINavigationItem: 0xa5f36e0> - <UINavigationItem: 0xa5f36e0>
Results
Navigation controllers are the same, as expected (self->navController == self->parent->navController).
Navigation item's are different, this is expected. Each view controller has it's own nav item (self->navItem != self->parent->navItem).
Accessing the navigation controllers nav item from the parent or the child view controller is identical, as expected (self->navController->navItem == self->parent->navController->navItem)
So I now ask: why didn't disabling a bar button item in the child view controller with the following code work:
self.navigationController.navigationItem.rightBarButtonItem.enabled = NO;
This should be synonym to the following (which does work as expected) given their shared address:
self.parentViewController.navigationController.navigationItem.rightBarButtonItem.enabled = NO;
Update
I was reading my logs too fast. Indeed the parentViewController and the navigation controller have 2 different navigation items. I missed this in my example here because the memory addresses were extremely close: 0xa5f3620 != 0xa5f36e0

A UINavigationController has it's very own navigationItem (like any other UIViewController) that will only come into play if you push it onto the stack of another UINavigationController.
Generally you aren't going to push one UINavigationController onto another so manipulating self.navigationController.navigationItem is pointless.
EDIT:
To check ... I've just aded:
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
UINavigationItem *myNavControllersItem = self.navigationController.navigationItem;
UINavigationItem *myItem = self.navigationItem;
NSLog(#"%# - %#", myNavControllersItem, myItem);
}
to a VC that I'm pushing onto a NavController and get:
<UINavigationItem: 0x1ed0c170> - <UINavigationItem: 0x1ed46330>
Additionally, if I add:
self.navigationItem.rightBarButtonItem.enabled = NO; to viewDidAppear the right button gets disabled.
if I add:
self.navigationController.navigationItem.rightBarButtonItem.enabled = NO; it does not.

Well basically the problem is the way the navigation controller updates the bar button items, basically the buttons are update whenever the top-level child is changed.
For the right bar button item, if the new top-level child has it's own bar buttons items then the navigation controller will select the existing one, if not nothing will be displayed.
So in your case the parrentViewController already has a navigationItem.rightBarButonItem which is not disabled, by calling self.navigationController.navigationItem.rightBarButtonItem.enabled = NO; you are disabling the child's nabigationItem.rightBarButtonItem and you can't see this because the updates are made only when the top-level child is changed which is not your case.
So in order to disable the parentViewController right button you should do:
self.parentViewController.navigationItem.rightBarButtonItem.enabled = NO
For more details regarding the nav bar button updates please check apple docs regarding the Updating Navigation Bar Items

Related

Navigation bar shows wrong navigation item in iOS 9.2

A UINavigationController's navigationBar will initially show the correct UINavigationItem, but then will revert to the previous UINavigationItem every time a UIViewController is pushed onto the stack.
Steps to Reproduce:
Push a UIViewController onto a UINavigationController stack
Set navigationBarHidden = YES on the navigation controller
Push another view controller onto the navigation stack.
Begin an interactive pop transition and then cancel it.
Pop back to the previous view controller
Set navigationBarHidden = NO on the navigation controller
Attempt to push a view controller onto the stack
Looks like -[UINavigationBar _cancelInteractiveTransition] is getting called, even on push transitions after getting into this state? I can set a breakpoint on that symbol, and the navigation bar shows the correct navigation item before it and the wrong navigation item afterwards.
But what I want is that the navigation controller's navigation bar should display the current topViewController's navigation item.
#interface UINavigationController (Private)
- (void)_cancelInteractiveTransition:(float)arg1 transitionContext:(id)arg2;
#end
- (void)_cancelInteractiveTransition:(float)arg1 transitionContext:(id)arg2
{
BOOL hidden = self.isNavigationBarHidden;
if (hidden) {
[self setNavigationBarHidden:NO animated:YES];
}
[super _cancelInteractiveTransition:arg1 transitionContext:arg2];
if (hidden) {
[self setNavigationBarHidden:hidden animated:YES];
}
}
I recently ran into this issue on iOS10 and I'm sure it was there on iOS9, assuming we still supported it. It turned out that the issue was that at the start of the interactive transition we were setting navigationController.navigationBarHidden=NO and then when it was cancelled forgetting to set it back to navigationController.navigationBarHidden=YES. It seems like the navigation bar doesn't like to be unhidden twice in a row. I would imagine that its the same for setting it to hidden twice in a row as well. The good news is that this was not an issue with iOS11.

Modal Presentation Forcing Navigation Controller to Pop to Root

I am having a strange problem that I can't seem to find the cause for.
When attempting to present a modal view controller on a navigation controller the navigation controller is popping all of my view controllers underneath when the modal is dismissed.
So after pushing a few view controllers and presenting a modal on the topViewController, I end up back at the rootViewController when the modal is dismissed.
Anyone had this happen to them lately, I can't seem to find the reasoning for why this is happening?
This answer is for #rshev:
It was actually a user error. Here's what was happening: I had a view controller with a manually added navigationController on top of it (as a subview/child VC). The nav controller then had 3 VCs in its stack. The third (and visible) VC was presenting an image picker controller. When the image picker was dismissed, I momentarily saw my third VC , then it quickly popped back to the 1st, discarding the other two VC's from memory.
So what went wrong? What I didn't realize is that viewDidAppear (and viewWillAppear) was being called on my content view controller (the one with nav controller for its subview). This content VC was actually setting its navigation controller (and adding it as a subview) on viewDidAppear, thus covering up the original nav controller.
To solve it, I just added a static boolean to determine when the first VC FIRST appears, like so:
- (void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
static BOOL firstAppearance = YES;
if (firstAppearance)
{
firstAppearance = NO;
UINavigationController *navController = [self.storyboard instantiateViewControllerWithIdentifier:#"NavigationController"];
[navController.view setFrame:self.view.bounds];
[self.view addSubview:navController.view];
[self addChildViewController:navController];
[navController didMoveToParentViewController:self];
}
}
Hope that helps.

Going Back and Forth between View Controllers with Back Button Always Going to Root

I have three View Controllers in a Navigation Controller. One is the root, and the others should appear if conditionA or conditionB is met, respectively.
In case both conditions are true, I want the conditionA View Controller to be shown. If things then change, and conditionA is no longer met, but conditionB still is, I want to show the conditionB View Controller. This means that I want to be able to go from the conditionA View Controller to the conditionB View Controller, and vice versa, until neither condition is met. However, I always want the back button to send users to the root.
To implement this while avoiding errors like ‘Finishing up a navigation transition in an unexpected state’, I’ve taken code from Dave DeLong’s answer https://stackoverflow.com/a/11821263/2524427.
(void)somethingChanged:(NSNotification*)notification
{
if(!conditionA && !conditionB){return;}
NSArray *viewControllers = self.navigationController.viewControllers;
NSMutableArray *newViewControllers = [NSMutableArray array];
// preserve the root view controller
[newViewControllers addObject:[viewControllers objectAtIndex:0]];
if(conditionA)
{
// add the new view controller
[newViewControllers addObject:[self.storyboard instantiateViewControllerWithIdentifier:#"conditionAViewController"]];
// animatedly change the navigation stack
[self.navigationController setViewControllers:newViewControllers animated:YES];
return;
}
if(conditionB)
{
// add the new view controller
[newViewControllers addObject:[self.storyboard instantiateViewControllerWithIdentifier:#"conditionBViewController"]];
// animatedly change the navigation stack
[self.navigationController setViewControllers:newViewControllers animated:YES];
}
}
This works almost perfectly. However, if I have the following sequence
somethingChanged triggered while only conditionB is met -> conditionB View Controller appears - as desired
somethingChanged triggered again, but now both conditionA and conditionB are met -> condition A View Controller appears - as desired
somethingChanged triggered for the third time, now back to initial situation where only conditionB is met -> I return to the root View Controller (with Back button) instead of conditionB View Controller…
How can I keep going back and forth between these View Controllers?
Instead of the solution in the question you linked. Try this
init your own back button UIBarButtonItem as navigationBar's leftBarButtonItem in both conditionA View Controller and conditionB view controller
wire up with a action
- (IBAction)dismiss:(id)sender {
[self.navigationController popToRootViewControllerAnimated:YES];
}

UINavigation Controller subclass and push Segues

I've been looking this up for a while now, it might have a simple answer:
According to the Apple docs, past ios6, we can subclass UINavigationController. How do we perform a segue from identifier when it prevents anything that isn't a UINavigationController. Mainly:
uncaught exception 'NSGenericException', reason: 'Could not find a
navigation controllerfor segue 'profileSegue'. Push segues can only
be used when the source controller is managed by an instance of
UINavigationController.
I'm using JaSidePanels and my center panel (navigation) needed to be subclasses for a delegate as there is a menu on the left panel that I want to switch the views when clicked.
#interface CenterViewController : UINavigationController <MenuDelegate>
Basically, since this object is a CenterViewController at runtime, is there a way to cast it to its superclass? I've tried [self superclass] but that didn't work (same error).
I need to call this code in the CenterViewController. Is it possible to move it to UINavigationController?
- (void)viewDidLoad
{
RootViewController *rootViewController = (RootViewController *)[[[[UIApplication sharedApplication] delegate] window] rootViewController];
MenuViewController *leftViewController = (MenuViewController *)rootViewController.leftPanel;
// Store a reference to the center view controller in the left view controller's delegate property
leftViewController.menuDelegate = self;
[super viewDidLoad];
}
- (void) MenuItemSelected: (NSString*) item
{
if ([item isEqualToString:#"Home"]) {
//These would throw the error since we're not technically a "UINavigationController"
//[self performSegueWithIdentifier: #"mapViewController" sender: nil];
} else if ([item isEqualToString:#"Profile"]) {
//[self performSegueWithIdentifier: #"profileSegue" sender: self];
}
}
EDIT:
More information with pictures.
I'm curious as to how navigation controllers should work with side panels. I've looked at many other plugins for sidepanels and here is an example. Even though it works, why does it have 2 nav controllers?
Here is our current setup:
So basically, am I thinking about this wrong in the sense that I want to push a new VC to the existing NavVC? Would it be better to push a new NavVC when a menu button is pressed? What would happen when we go into a subview from the Maps view. Would the menu be accessible via sliding?
If you look carefully at the message you will see that your problem isn't caused by subclassing UINavigationController it is because you are executing the segue against your (subclassed) UINavigationController. Push segues are executed against a UIViewController that is embedded in or managed by a UINavigationController, with the system then finding the managing UINavigationController via the view controller's navigationController property in order to execute the push.
The message you have received says
...Push segues can only
be used when the source controller is managed by an instance of
UINavigationController
In your case the source controller is an instance of UINavigationController, it isn't managed by a UINavigationController.
You haven't said exactly how your app navigation works, but I have a suspicion that a UINavigationController isn't the right tool to use anyway. If you are using the side menus to allow the user to select the central content in a random way (i.e. the user could select the first option then the fifth and then go back to the first) then you should probably just have a central view into which you present the selected view. Pushing views onto a UINavigationController will end up with a large 'stack' of views unless you pop the current view controller before pushing the new one, which is even more complicated and not the visual effect you are looking for.
You can still achieve the 'push' style transition but you will need to use a custom segue. If it were me I would probably push from the left if the user selected a menu item that was closer to the top than the current option and from the right if the new item was closer to the bottom than the current, but again I am making assumptions on how your app navigation works.
UPDATE
Yes, I think you are on the right track with the section of your updated question. Navigation controllers are for navigating a series of related views in a hierarchical manner - think of the Settings app - you select "general" or "wall paper" or whatever - each of these then has a series of views that you can navigate through; up and down a stack.
In your app it looks like home, profile and settings should each be navigation controllers. Your root view would then just be a view. Your menu would select which view controller to present in the root view - this is like a tab bar controller, except your menu takes the place of a tab bar.
You can allocate your home, profile & settings view controllers in your appDelegate and store them to properties of the appDelegate. Then you can use something like this:
- (void) MenuItemSelected: (NSString*) item
{
myappDelegate *app=(myappDelegate *)[UIApplication sharedApplication].delegate;
[delegate.currentViewController removeFromParentViewController];
UIViewController *newController;
if ([item isEqualToString:#"Home"]) {
newController=app.homeViewController;
} else if ([item isEqualToString:#"Profile"]) {
newController=app.profileViewController;
}
if (app.currentViewController != newController)
{
[app.currentViewController removeFromParentViewController];
[app.rootViewController addChildViewController:newController];
app.currentViewController = newController;
}

iOS - Check for Navigation bar

I'm creating a library which will add a view at the bottom of the application (when my library is integrated in application).
I'm using view controller's view's frame parameter to get the size of the view and calculation my library's view frame according and showing it.
The problem is that when navigation bar is there, my view is going still below the actual view visible. So, i want to know whether current view controller is based on navigation controller or not and whether navigation bar is visible in that view or not. how can I find that?
I'm late with the reply, but for other persons who try to do the same thing (like me :D).
This code may solve your problem:
id nav = [UIApplication sharedApplication].keyWindow.rootViewController;
if ([nav isKindOfClass:[UINavigationController class]]) {
UINavigationController *navc = (UINavigationController *) nav;
if(navc.navigationBarHidden) {
NSLog(#"NOOOO NAV BAR");
} else {
NSLog(#"WE HAVE NAV BAR");
}
}
UINavigationBar inherits from and has all the fine properties and behaviors of UIView and one of these properties is hidden.
So for your view, if you can get a handle to your navigation bar, all you need to do is check to see if hidden is YES or NO.
one way to do this would be to have a UINavigationController property or accessor (setter & getter) for your library so whoever makes use of the library can set the navigation controller and/or bar on your library's behalf.
Up to date check from a view controller context:
let navHidden = navigationController?.isNavigationBarHidden ?? true
if needsCloseButton || navHidden
{
// here add an alternative ways to get out since back button is not here, say add a close button somewhere

Resources