NOTE:
Before reading this question please note that I have read the previous questions that explain the deficiencies regarding apple's implementation of UISplitViewController and how I should use the open-sourced "MGSplitViewController" because its not too easy to simply hide the master view controller on a split view controller in landscape-mode. Please keep in my mind that I'm limited to using the normal UISplitViewController in iOS 5.1.
Now onto the question:
I have a split view controller with table views on the left side (master view) and a detail view controller on the right. I'm using a navigation controller to control the left side which is a table view that transitions onto another table view ("DataTableViewController"). In order to hide this left side, I have placed a "hide" button on the navigation tool bar of the detail view controller. When the hide button is pressed, I change my "_hideMaster" property:
-(IBAction)hidePressed
{
_hideMaster = !_hideMaster;
// Must manually reset the delegate back to self in order to force call "shouldHideViewController"
self.splitViewController.delegate = nil;
self.spliteViewController.delegate = self;
}
and then automatically this method is called in the SplitViewController delegate:
// This is called when I change the delegate from nil back to self.
- (BOOL)splitViewController: (UISplitViewController*)svc shouldHideViewController: (UIViewController *)vc inOrientation:(UIInterfaceOrientation)orientation
{
return _hideMaster;
}
When I debug it, I can see that everything goes according to plan and the property has the correct value when it enters the method splitViewController:shouldHideViewController:inOrientation:
The only problem is that nothing happens. My left most table view (DataTableViewController) does not disappear. When I look closer, the (UIViewController *)vc parameter in the delegate method is not the table view controller that I want to hide but instead the navigation controller associated with this table view. So essentially it is trying to hide the navigation controller - which is clearly not what I want...
How can I make it so that the UIViewController parameter in the automatically called delegate method (shouldHideViewController:) calls the topmost view controller associated with that navigation controller? (After all, I want to hide DataTableViewController)
Here's how I handle it. Might need more work for making the MasterViewController reappear if it is not instantiated on the way back.
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.30f];
[[self.splitViewController.viewControllers lastObject] view].frame = self.splitViewController.view.frame;
[UIView commitAnimations];
Related
Here is the problem I am having. I am unable to set the UINavigationBar title for the views I have contained within a UIPageViewController.
The basic architecture of the app is as follows.
The root view controller for the app is a UITabBarController, with 5 navigation controllers contained in it.
The first Navigation controller, which is the one I am having issues with, contains a page view controller and this page view controller contains a number of UIViewControllers.
I want that, when I swipe through each of these view controllers, I can set the title in the UINavigationBar.
I have tried the following:
In the UIViewController contained within the page view controller, I have tried [self setTitle:#"Title I want"] - it didn't work.
Within the same UIViewController I have also tried [self.navigationBar.navigationItem setTitle:#"Title I want"] - this also didn't work.
I also tried setting the title for the View controller and attempted to extract that inside the PageViewControllers delegate method transitionCompleted, but this didn't work either.
I am wondering should I go back to the drawing board, and whether I am going down a rabbit hole with this view layout architecture. Has anyone else encountered this issue and if so, how did you solve it?
Edit: I must also add that I am doing this programatically.
Thanks for the help.
So, in the end I came up with a way to get this working, albeit not the cleanest solution that I wanted, but suitable for the purpose nonetheless.
I created a new class called PageLeafViewController and set up its init method as below. Child view controllers of a page view controller inherit from this. Here is the code.
Code sample
- (id)initWithIndex:(NSUInteger)index andTitle:(NSString *)navBarTitle; {
if(self = [super init]) {
self.index = index;
self.navBarTitle = navBarTitle;
}
return self;
}
These can be initialised like so before being added to the UIPageViewController.
Code sample
ChildViewController *aChildViewController = [[ChildViewController alloc] initWithIndex:1 andTitle:#"A Title"];
You will need to add a UIPageViewControllerDelegate to your interface for your page view controller. This is so you can implement the code for the delegate methods for when your view transition has been completed, and you need to set the title.
When the UIPageViewController loads, I grab the first view controller and get its title, setting it to the UINavigationController navigation bar
Code sample
PageLeafViewController *initialViewController = (PageLeafViewController *)[self viewControllerAtIndex:0];
[self.navigationItem setTitle:initialViewController.navBarTitle];
When a transition occurs, we set the title again to that of the new child view controller, when the transitioning into view has completed.
Code sample
- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed {
PageLeafViewController *currentLeaf = (PageLeafViewController *)[self.pageViewController.viewControllers lastObject];
[self.navigationItem setTitle:currentLeaf.navBarTitle];
}
Note: The above gets called automatically when a new child view controller has been displayed.
While this is not the most elegant solution it works for now, and I don't think its possible to call a function from within a child view to update the NavigationBar title, unless someone wants to correct me?
Hope this helps.
I don't think you're supposed to set the title on the navigationBar, have you tried self.navigationController.title = #"Title"; ?
I have an iOS 7 app with a side hamburger menu and a main table view controller where I display content. Whenever the user selects an item in my side menu, I'm hiding the side menu and I want to reload data in the main view controller. My initial thought was to put my data refreshing code in my main view controller's viewWillAppear:
But when I set a breakpoint in viewWillAppear:, I get 2 calls when the view controller initially appears, one from UIViewController itself, and another from [ECSlidingViewController viewWillAppear:] where the following line seems to call my viewWillAppear: again
[self.topViewController beginAppearanceTransition:YES animated:animated];
On the other hand, when I show the left menu and then hide it, my view controller's viewWillAppear: is not called this time, so data is not refreshed in my case.
Did I miss something in my configuration somewhere? Is that a bug or a feature? How should I use it?
PS: I used to use IIViewDeckController and I had the exact same problem, so I switched to ECSlidingViewController because it said that "Your view controllers will receive the appropriate view life cycle and rotation methods at the right time.".
As a matter of fact, I managed to do what I wanted with another library: https://github.com/romaonthego/RESideMenu
I had to implement delegate methods in order to call lifecycle methods on my view controller when menu is shown or hidden:
- (void)sideMenu:(RESideMenu *)sideMenu willShowMenuViewController:(UIViewController *)menuViewController {
[sideMenu.contentViewController viewWillDisappear:YES];
}
- (void)sideMenu:(RESideMenu *)sideMenu didShowMenuViewController:(UIViewController *)menuViewController {
[sideMenu.contentViewController viewDidDisappear:YES];
}
- (void)sideMenu:(RESideMenu *)sideMenu willHideMenuViewController:(UIViewController *)menuViewController {
[sideMenu.contentViewController viewWillAppear:YES];
}
- (void)sideMenu:(RESideMenu *)sideMenu didHideMenuViewController:(UIViewController *)menuViewController {
[sideMenu.contentViewController viewDidAppear:YES];
}
And viewWillAppear is not called twice initially.
I have a view controller in my application where on my screen I have a UIView that the user is required to tap on. When they do that, I want to call another viewController's view, and display it on the screen for the user. Unfortunately, I am having trouble displaying the view.
The name of my viewController that I am making the call from is called "MainViewController", and the ViewController whose view I wish to display is called, "NextViewController"
Here is my code from where I make the call:
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#"I was touched.");
_nextView = [[NextViewController alloc] init]; //this code is not being called
[self.view addSubview:_nextView.view]; //neither is this being called
}
Where _nextView is a property that I declare in the .h file of MainViewController.
This method is being called, but for some reason because I am able to see the log statements print to the output, but for some reason I am unable to call the lines after that. What am I doing wrong?
You shouldn't add the view of another view controller to your view without making that view controller a child view controller. If you just want a view, then set one up in a xib file and add it to your view as a subview. If you want to use a view controller, then you should present it modally, and dismiss it when you're done. This kind of situation where you want to gather some info from the user to use in your app, is an appropriate place to use a modal view controller. MainViewController should set itself as the delegate of NextViewController, and NextViewController should define a delegate protocol to send the data back to MainViewController.
To present it modally, do this:
_nextView = [[NextViewController alloc] initWithNibName:#"your nib name here" bundle:nil];
[self presentViewController:_nextView animated:YES completion:nil];
Are you using a Navigation Controller? Or Storyboards? One way of displaying another view controller would be like this:
[self presentViewController:_nextView animated:YES completion:^{
}];
A couple of things:
- If your NSLog gets called, then so do the other two lines you say do not.
- I assume you mean you want to display the other view controller on screen, not display the other view controller's view on the first view controller. These are two very different things, the second of which you wouldn't want to do.
I'm converting an iPhone app to be a universal app, and it's been mostly straight forward to convert the nested tables into a UISplitViewController arrangement, but I have a remaining issue when running on an iPad that is giving me a headache.
For universal app compatibility, the 'master' view contains a UINavigationController that is used to navigate through a series of TableViews that each displays a menu. This works fine.
Eventually, the user arrives at content that is displayed in the detail view. Each detail view 'chain' is contained in a UINavigationController, as some views can drill down to show maps etc. The idea is that the popover button will live at the root level of the detail view. It's probably important to note that the detail views are created from scratch every time that row is selected.
I've studied Apple's Multiple Detail View Sample Code , and so use the master view as the UISplitViewController delegate, which provides the hide/show popover selectors, and then passes the calls down to whichever substitute detail view is selected.
When working in landscape mode, I can select different rows in the master view, and the detail views switch nicely - everything works great. It's wonderful.
In portrait mode, things don't work quite so well... the popover button displays correctly in the currently selected detail view when rotating to portrait, but then disappears when a row is selected (i.e. it's somehow not being added correctly to the newly selected view's NavBar).
I've added diagnostic code, and it looks like the correct calls (with correct pointers) are being made to show the popover button on the newly selected detail view. Also, I can rotate to landscape and back again, and the popover button then appears so I'm reasonably satisfied that the popover UIBarButtonItem is being hooked up to the new detail NavBar correctly.
As the detail views are not created until the row is selected, I was wondering if this was a case of the UINavigationBar not being instantiated at the time that showRootPopoverButtonItem is called (based on Apple's sample code). This theory is supported by the fact that the popover button appears if I rotate to landscape and back again (as mentioned above) with the same view selected.
I also see this comment in Apple's sample code, in didSelectRowAtIndexPath, and just before switching the detail views, note the use of the word 'after'...
// Configure the new view controller's popover button (after the view has been displayed and its toolbar/navigation bar has been created).
So, I tried calling the showRootPopoverButton method again in viewWillAppear (by which time the UINavigationBar should exist), but that doesn't cause the popover button to appear either.
I'd appreciate any thoughts and suggestions as to how to get the popover button to appear immediately when a new row is selected from the master view when in portrait mode. Thanks.
Thanks for reading this far, the relevant code is below.
From the master view, here are the UISplitViewControllerDelegate selectors,
- (void)splitViewController:(UISplitViewController*)svc willHideViewController:(UIViewController *)aViewController withBarButtonItem:(UIBarButtonItem*)barButtonItem forPopoverController:(UIPopoverController*)pc
{
// Keep references to the popover controller and the popover button, and tell the detail view controller to show the button.
barButtonItem.title = #"Root View Controller";
self.popoverController = pc;
self.rootPopoverButtonItem = barButtonItem;
//UIViewController <SubstitutableDetailViewController> *detailViewController = [self.splitViewController.viewControllers objectAtIndex:1];
// ^ Apple's example, commented out, my equivalent code to obtain
// active detail navigation controller below,
UINavigationController *detailNavController = [self.splitViewController.viewControllers objectAtIndex:1];
UIViewController *detailViewController = detailNavController.visibleViewController;
[detailViewController showRootPopoverButtonItem:rootPopoverButtonItem];
}
- (void)splitViewController:(UISplitViewController*)svc willShowViewController:(UIViewController *)aViewController invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem
{
// Nil out references to the popover controller and the popover button, and tell the detail view controller to hide the button.
UINavigationController *detailNavController = [self.splitViewController.viewControllers objectAtIndex:1];
UIViewController *detailViewController = detailNavController.visibleViewController;
[detailViewController invalidateRootPopoverButtonItem:rootPopoverButtonItem];
self.popoverController = nil;
self.rootPopoverButtonItem = nil;
}
And, very much like Apple's example, here's what happens when a row is selected in the master table,
if (rootPopoverButtonItem != nil)
{
NSLog (#"show popover button");
[newDetailViewController showRootPopoverButtonItem:self.rootPopoverButtonItem];
}
From the detail view,
- (void)showRootPopoverButtonItem:(UIBarButtonItem *)barButtonItem
{
NSLog (#"detailViewController (view: %p, button: %p, nav: %p): showRootPopoverButton", self, barButtonItem, self.navigationItem);
barButtonItem.title = self.navigationItem.title;
[self.navigationItem setLeftBarButtonItem:barButtonItem animated:NO];
popoverButton = barButtonItem;
}
- (void)invalidateRootPopoverButtonItem:(UIBarButtonItem *)barButtonItem
{
NSLog (#"detailViewController (%p): invalidateRootPopoverButton", self);
// Called when the view is shown again in the split view, invalidating the button and popover controller.
[self.navigationItem setLeftBarButtonItem:nil animated:NO];
popoverButton = nil;
}
There are two things that I think could be the problem here. You should include the rest of your code. Specifically the part where you change the detail view controller when the user performs an action in the master.
visibleViewController may be nil if you just instantiated detailNavController. Even if you set it's root, there is no "visible" view controller since it actually hasn't displayed yet. You may want to try using topViewController
I'm not sure if you're creating a new detailNavController every time the user selects something in the master but if you are, you need to pass the rootPopoverButtonItem into the detailViewController again because - (void)splitViewController: willHideViewController: withBarButtonItem: forPopoverController: only gets called automatically when the orientation changes.
I have a tab bar with a navigation controller in one of the tabs. Currently the root view of the navigation controller doesnt have the nav bar showing and animates nicely into the subviews by
- (void)viewDidLoad {
...
[self.navigationController setNavigationBarHidden:YES animated:NO];
...
}
and
- (void)viewWillAppear:(BOOL)animated {
[self.navigationController setNavigationBarHidden:YES animated:YES];
}
- (void)viewWillDisappear:(BOOL)animated {
[self.navigationController setNavigationBarHidden:NO animated:YES];
}
But of course changing tabs initiates the viewWillAppear function and so as I go back to the root view the navigation bar slides away, rather than just not being there.
Is there a way that I can hide the nav bar on the root view without animating it except for when appearing from a subview on the navigation stack?
The (BOOL)animated parameter on viewWillAppear:animated. When changing Tabs, it will come as NO, since the animation is immediate. On the other hand, if it's being pushed or popped from the navigation stack with animated:YES, then it will come as YES.
Although this looks like a hack, it's the correct way: you don't need to figure out who was the caller, instead, focus on the fact that if your view controller will appear animated, you have time to do your own animations, if not, screw it, show (or in this case, hide) everything immediately.
Try showing/hiding the bar in the UINavigationController's delegate's navigationController:willShowViewController:animated: method, depending on whether the view controller being shown is your root view controller.
What if you set a boolean variable in your application delegate and in set that boolean accordingly in subviews as 0 and in other views as 1. And in your viewwillappear, according to your variable's value, you can set the animation.