Question
In a UISplitViewController collapsed display, how can I programmatically get back to master view controller?
Detail
I googled it but found no solution. Not sure if I was using the right keyword. This is how I show the detail view controller:
[self showDetailViewController:[[UINavigationController alloc] initWithRootViewController:detail] sender:self];
I also tried these 3 methods respectively, but none of them worked:
if (self.splitViewController.collapsed) {
UIBarButtonItem *backButtonItem = self.navigationItem.leftBarButtonItem;
(1):[backButtonItem.target performSelector:backButtonItem.action];
(2):[[UIApplication sharedApplication] sendAction:backButtonItem.action to:backButtonItem.target from:nil forEvent:nil];
(3):objc_msgSend(backButtonItem.target, backButtonItem.action);
}
navigation items set like thie in detail VC viewDidLoad:
self.navigationItem.leftBarButtonItem = self.splitViewController.displayModeButtonItem;
self.navigationItem.leftItemsSupplementBackButton = YES;
Alright, I have found a solution that seems to work. I have tested it on iPhone 6 and iPhone 6 Plus, but I only just discovered it thirty minutes ago, so It might have some unfortunate side effect which I have not run into yet.
It's in swift. I hope it's clear though. Let me know if you need me to provide it in Objective-C instead.
if let splitViewController = splitViewController {
if splitViewController.collapsed {
let viewControllers = splitViewController.viewControllers
for controller in viewControllers {
// PrimaryNavigationController is the navigation controller I use
// as the split views master view, which is also set as its delegate
// but it could be any UINavigationController that is the
// primary controller of the split view
if controller.isKindOfClass(PrimaryNavigationController) {
controller.popViewControllerAnimated(true)
}
}
}
}
I call this from my detail view when I want to dismiss it.
The code works by checking if the split view controller is collapsed, which is the only state where popping the detail view makes sense (to me anyways). Then it simply looks for the navigation controller currently in play in the split view controller and asks it to pop it's top view controller. This works because when in collapsed mode, the split views master view is the only view controller in the stack. The detail view is collapsed "into" it, and therefore becomes the current top view controller of it, thus is the one that gets popped.
Seems to work. Let me know if it do for you too.
I was looking to do exactly the same, and this code worked for me. I put it in the detail view, hooked up to a button in the navigation bar.
In my application the detail view can segue to itself a number of times and this code gets one back to the master view no matter how deep down the line it gets.
#IBAction func unwindSegueId(sender: AnyObject) {
if (self.splitViewController!.collapsed) {
self.splitViewController!.viewControllers[0].popToRootViewControllerAnimated(true)
}
}
This seems to work (provided you have a navigation controller in your master pane)
if (self.splitViewController.collapsed) {
[(UINavigationController *)self.splitViewController.viewControllers[0]
popToRootViewControllerAnimated:YES];
}
Related
Since iOS8 we're allowed to use UISplitViewController on both compact and regular devices. This is great because I don't have to create two different storyboard for iPhone and iPad, but there's one problem that I'm stuck with.
If the split view controller is on iPad(if the collapsed property is NO), I can simply call this to show MasterVC on the left side.
self.splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModePrimaryOverlay;
[self.splitViewController.displayModeButtonItem action];
But if it's on iPhone(if the collapsed property is YES), the displayMode is ignored, and doesn't do anything.
I cannot pop DetailVC with popToRootViewControllerAnimated because DetailVC has it's own navigation controller.
How does Apple expect us to show MasterVC(dismiss DetailVC) in code in collapsed mode if there isn't any method like dismissViewControllerAnimated:completion: for view controller that was presented with showDetail? Your help will be appreciated. Thanks
On devices which don't support the "split" mode, if
You want to present the master view controller instead of the detail when the UISplitViewController first loads, then returning YES in your delegate class (UISplitViewControllerDelegate) splitViewController:collapseSecondaryViewController:ontoPrimaryViewController: method method should do that:
- (BOOL)splitViewController:(UISplitViewController *)splitViewController collapseSecondaryViewController:(UIViewController *)secondaryViewController ontoPrimaryViewController:(UIViewController *)primaryViewController {
return YES;
}
You want to dismiss the detail view controller back to the master, after a specific event (e.g. a touch on a button). In this case you have to pop the detail view controller navigation controller:
[detailViewController.navigationController.navigationController popToRootViewControllerAnimated:YES]
Had a similar issue today trying to pop back from a detail view in a split view controller.
While I'm sure the accepted answer works fine, another approach I found that works as well and may be a bit cleaner is to use an unwind segue.
I setup an unwind segue on the master view I wanted to return to, then created a segue link to the unwind segue from the view I wanted to pop (note: assumes that you are using storyboards).
Make sure to setup the IBAction on the destination view you are popping back to:
-(IBAction)prepareForUnwind:(UIStoryboardSegue *)segue { }
Connect the exit to the segue in the storyboard for the unwind segue. Sorry, I'm not providing a lot of detail on how to setup the unwind segue, but there are many tutorials available for that.
Then on your controller you want to dismiss, connect a segue to the unwind segue of the controller you are popping back to. Be sure to name the segue.
Then on the button touch in the view controller you want to dismiss, just call
[self performSegueWithIdentifier:#"unwindSegueName" sender:self];
This worked really well and avoids digging backwards into a navigation hierarchy that may change.
Hope this is useful to someone!
Happy Holidays!
Here's what I ended up doing to pop the DetailVC if we are in a collapsed state (iPhone excluding +sizes), and show/hide the MasterVC if we are not in a collapsed state (iPad).
#IBAction func backTouchUp(_ sender: UIButton) {
if let splitViewController = splitViewController,
!splitViewController.isCollapsed {
UIApplication.shared.sendAction(splitViewController.displayModeButtonItem.action!, to: splitViewController.displayModeButtonItem.target, from: nil, for: nil)
} else {
navigationController?.popViewController(animated: true)
}
}
Thanks pNre! Here's code that will handle displaying a custom back button when collapsed and the displayModeButton when not collapsed.
lazy var backButtonItem: UIBarButtonItem = {
UIBarButtonItem(image: UIImage(named: "backImage"), style: .plain, target: self, action: #selector(dismissAnimated))
}()
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
guard let svc = splitViewController else { return }
if svc.isCollapsed {
navigationItem.leftBarButtonItem = backButtonItem
} else {
navigationItem.leftBarButtonItem = svc.displayModeButtonItem
}
}
func dismissAnimated() {
_ = navigationController?.navigationController?.popViewController(animated: true)
}
I've placed this in willLayoutSubviews() instead of viewDidLoad() so that the button will be updated adaptively, e.g., for orientation changes on iPhone 7 Plus and size class changes such as while in split view on iPad.
I have a tabbar controller. In one of the viewcontroller children I do a check and determine if I will show a login view with this code:
if(loggedIn){
}else{
SignupViewController *svc = [self.storyboard instantiateViewControllerWithIdentifier:#"SignupView"];
svc.hidesBottomBarWhenPushed = YES;
[self.navigationController pushViewController:svc animated:YES];
}
From here the user goes through several subsequent views in wizard format. Like from SignupViewController1->SignupViewController2->SignupViewController3->etc. As #Rocky pointed out, you cannot pop SignupViewController1 off the stack while you are in SignupViewController2 or 3 or any subsequent viewController.
I know the iOS docs state the following:
If YES, the bottom bar remains hidden until the view controller is
popped from the stack.
My question is how do I get acccess to the original SignupViewController to pop it off the stack to see my tabbar again once I have moved to subsequent views in its navigation controller?
(The next answer is for SWIFT, but, you can traduce it to Objective-C)
I do not know, but maybe you can try that:
#IBAction func returnToPreviusScene(sender : AnyObject) {
let navController:UINavigationController = self.navigationController!
navController.popViewControllerAnimated(true)
navController.pushViewController(FatherView.singleton, animated: false)
}
You need to know:
singleton is a static variable from the VIEW FatherView
I am pushing a viewController onto the UINavigationController with animation, and the controller being pushed on is basically doing something like:
--- app delegate:
[((UINavigationController *)window.rootViewController) pushViewController:initialController animated:YES];
--- initial controller:
- (void)viewDidLoad {
[super viewDidLoad];
if (self.shouldSkipThisController) {
SomeOtherViewController *someOther = [[SomeOtherViewController alloc] init];
[self.navigationController pushViewController:someOther animated:NO];
}
}
This is causing some CRAZY behavior which I don't understand at all. Basically, it seems like the navigation items set on SomeOtherViewController are being covered up by some strange other button that has the name of the title in a back button. It looks like although SomeOtherViewController is setting it's own left and right navigation items, they are covered up by the "default" back button--- and then if I tap on that back button, then just the navigation bar at the top animates-- and THEN SomeOtherViewController's navigation items are then there.
The only thing I could find that sort of worked was to either 1) not animate the push of the initial view controller in the app delegate, or 2) move the shouldSkipThisController condition into viewDidAppear: method.
However, neither of those options are ideal... Any help could be greatly appreciated.
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'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