I am hoping I can describe this situation correctly. It is easy to see on the screen when it is happening, but hard to explain in words, but I will do my best.
I have a UISplitViewController with a fixed Master view (UITableView) and one of two Detail views, depending on what kind of cell is touched in the Master. Each of the Detail views is also a UITableView. I have used Todd Bates conversion of Apple's MultipleDetailViews sample to using storyboards as the basis for my code (Adventures in UISplitViewController).
I am using storyboards, so this is where the explaining part gets tricky. The Master and the first displayed Detail view Navigation Controllers are wired up in the storyboard as Relationships to the UISplitViewController. The second Detail Navigation Controller is just sitting in the storyboard with no connections.
Here's the problem. Detail #1 is active in landscape mode and Detail #2 is set as active by tapping a row in the Master. The iPad is rotated to Portrait mode which causes Detail #2 to resize properly for portrait mode. Detail #1 is set as active by tapping a row in the Master. Detail #1 is the same size as it was when it was last displayed in landscape mode, it doesn't fill the screen like it should.
My question is, can I tell the Detail controller that is not currently displayed to resize itself when the orientation is changed? Or maybe a better question is how can I resize the Detail view that was just displayed to fill the Detail portion of the UISplitView?
I hope I described this with enough detail for someone to help.
The UISplitViewControllerDelegate methods are implemented in a Manager class that's supposed to forward those calls to the Detail view controllers. Here's the implementation for that method:
#pragma mark - UISplitViewControllerDelegate
/* forward the message to the current detail view
* all detail views must implement UISplitViewControllerDelegate
*/
-(void)splitViewController:(UISplitViewController *)svc willHideViewController:(UIViewController *)aViewController withBarButtonItem:(UIBarButtonItem *)barButtonItem forPopoverController:(UIPopoverController *)pc
{
self.masterBarButtonItem = barButtonItem;
self.masterPopoverController = pc;
barButtonItem.title = NSLocalizedString(#"Games", #"Games");
[self.currentDetailController.navigationItem setLeftBarButtonItem:self.masterBarButtonItem animated:YES];
}
/* forward the message to the current detail view
* all detail views must implement UISplitViewControllerDelegate
*/
-(void)splitViewController:(UISplitViewController *)svc willShowViewController:(UIViewController *)aViewController invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem
{
self.masterBarButtonItem = nil;
self.masterPopoverController = nil;
[self.currentDetailController.navigationItem setLeftBarButtonItem:nil animated:YES];
}
And I have implemented the following in each one of the Detail view controllers:
#pragma mark - Split view
- (void)splitViewController:(UISplitViewController *)splitController willHideViewController:(UIViewController *)viewController withBarButtonItem:(UIBarButtonItem *)barButtonItem forPopoverController:(UIPopoverController *)popoverController
{
barButtonItem.title = NSLocalizedString(#"Games", #"Games");
[self.navigationItem setLeftBarButtonItem:barButtonItem animated:YES];
self.masterPopoverController = popoverController;
}
- (void)splitViewController:(UISplitViewController *)splitController willShowViewController:(UIViewController *)viewController invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem
{
// Called when the view is shown again in the split view, invalidating the button and popover controller.
[self.navigationItem setLeftBarButtonItem:nil animated:YES];
self.masterPopoverController = nil;
}
Problem is that neither of the Detail view controllers are receiving these messages. I can't see how they's get triggered anyway...
I don't see how forwarding the didRotateFromInterfaceOrientation: will cause the Detail controller that is not displayed from resizing itself. From Apple's documentation:
When the orientation of an iOS–based device changes, the system sends out a UIDeviceOrientationDidChangeNotification notification to let any interested parties know that the change occurred. By default, the UIKit framework intercepts this notification and uses it to update your interface orientation automatically. This means that, with only a few exceptions, you should not need to handle this notification at all. Instead, all you need to do is implement the appropriate methods in your view controller classes to respond to orientation changes.
The window object does much of the work associated with changing the current orientation. However, it works in conjunction with the root view controller to determine whether an orientation change should occur at all and, if so, what additional methods should be called to respond to the change. If this controller is a container, it may rely on a child to decide whether the orientation should occur.
When a rotation occurs, methods of the view controller are called at various stages of the rotation to give the view controller a chance to perform additional tasks. You might use these methods to hide or show views, reposition or resize views, or notify other parts of your app about the orientation change. Because your custom methods are called during the rotation operation, you should avoid performing any time-consuming operations there. You should also avoid replacing your entire view hierarchy with a new set of views. There are better ways to provide unique views for different orientations, such as presenting a new view controller (as described in “Creating an Alternate Landscape Interface”).
Similar to the splitViewController methods, you should forward the following call:
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
[super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
// Forward to detail view controller here:
}
to the detail view controller that is NOT visible (i.e. NOT self.currentDetailController, but the other one).
I ended up solving my own problem. In the code that swaps the Detail views, I set the view about to be displayed to the same size as the view that was just displayed. I still have further testing to do, but this solution looks like a winner.
Related
-(BOOL)shouldAutorotate {
return NO;
}
Above method works for one controller but when there are multiple viewControllers pushed on a stack.
I want a particular controller that should be displayed in portrait mode only.
- (void) viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
}
I have used above method suggested on stack overflow for iOS 8 but it does not give desired result.
First, use -supportedInterfaceOrientations instead of -shouldAutorotate. -shouldAutorotate should only be used when you must disallow autorotation based on factors determined at runtime. You know your view controller will always only support portrait mode, there is no runtime decision here.
Next, your navigation controller's delegate must implement the -navigationControllerSupportedInterfaceOrientations: method to return the result of calling -supportedInterfaceOrientations on the view controller at the top of the navigation stack.
-(NSUInteger)navigationControllerSupportedInterfaceOrientations:(UINavigationController *)navigationController {
return navigationController.topViewController.supportedInterfaceOrientations;
}
An important caveat: A view controller pushed onto a navigation stack has no control over its initial interface orientation; that will always be the current interface orientation. What the above technique will do is prevent the interface from rotating to any orientation other than portrait while that view controller is displayed.
I don't know how to ask this more precisely. I have a master/detail and am creating the whole thing programmatically. I subclasses UISplitViewController and populated it with the two controllers, and everything looks as it should until I set splitViewController:shouldHideViewController:inOrientation such that it returns YES in portrait modes.
When I have the master hiding in portrait and portrait upside-down, as expected, it hides. However, I can't add a "Master" button to the nav bar at the top of the detail view in splitViewController:willHideViewController:withBarButtonItem:forPopoverController. This is probably because I have a fundamental misunderstanding of how I'm supposed to accomplish that task.
I followed the Apple examples and did:
barButtonItem.title = NSLocalizedString(#"Master", #"Master");
[detailController.navigationItem setLeftBarButtonItem:barButtonItem animated:YES];
I'm not getting any errors, but no button either. I speculate that perhaps it's because what I'm saving as detailController in my subclass is a UINavigationController and not a UIViewController.
Any guidance on this is much appreciated!
Having written this, I realized that there were several errors in wiring this whole thing up:
splitViewController:willHideViewController:withBarButtonItem:forPopoverController really wants you to not only to set the barButtonItem title, but also to add it to the nav bar of the detail controller.
If you ever want to programmatically dismiss the popover, you have to store the popover supplied in splitViewController:willHideViewController:withBarButtonItem:forPopoverController someplace in the master view.
So, the answer to the first part of the question was:
barButtonItem.title = NSLocalizedString(#"Master", #"Master");
[[detailController.topViewController navigationItem] setLeftBarButtonItem:barButtonItem animated:YES];
That got me to the UIViewController that can set a UIBarButtonItem on the navigation bar. I'm sure I could have done this directly on the UINavigationController but didn't immediately see how.
The second, unasked part of this question, deals with what to do with the popover once it's visible. Again, I needed the detail controller to know what the actual popover was so it can be dismissed, so in splitViewController:willHideViewController:withBarButtonItem:forPopoverController, I added code like:
[masterController.navigationItem topViewController].popoverController = pc;
where pc is the value of the argument passed into the delegate method. Then, in my master controller, I have a UITableView and on the didSelectRowAtIndexPath, I simply did this:
if(popoverController)
[popoverController dismissPopoverAnimated:YES];
And that's what I learned in iOS school today :)
I have nice helper functions that allow me to show/hide the master popover view controller. However, I can not for the life of me figure out how to hide it on initialization, so that it is hidden when the app first starts.
I've tried a couple of things (such as trying to dismiss is from the viewLoaded or viewDidAppear) but these throw strange errors (e.g. too made slider counts...etc).
Now I'm starting to believe there must be a simpler, and the right way, to do this.
Are you trying to hide the splitviewController Master Popover? Which orientation would make this question more specific. I will assume you are in fact trying to hide the SplitView MasterPopover in landscape (since it should already be hidden in portrait).
Do this:
Your detailViewController should have UISplitViewControllerDelegate. And simply just drop this code in:
- (BOOL)splitViewController:(UISplitViewController *)svc shouldHideViewController:(UIViewController *)vc inOrientation:(UIInterfaceOrientation)orientation{
return YES;
}
This will make popoverView hidden when the app starts up. Let me know, if it works for you.
I am trying to hide the master view controller when a selection has been made in the table view. I've looked all over stackoverflow and can only find solutions that used to work prior to iOS 5.1
I've tried to dismissPopoverAnimated or dismissModalViewControllerAnimated, but none of those seem to work.
There has got to be a way to hide the masterview controller once a section has been made.
Any help would be really appreciated. Thanks.
you have to dismiss your popovercontroller. see this answer, there is a code example:
https://stackoverflow.com/a/5829368/558150
Actually I figured it out after a lot of playing around What I did was save 'pc' in a
UIPopoverController * popOver in: - (void) splitViewController:(UISplitViewController *)svc willHideViewController:(UIViewController *)aViewController withBarButtonItem:(UIBarButtonItem *)barButtonItem forPopoverController:(UIPopoverController *)pc;
then used:
[[[self.splitViewController.viewControllers lastObject] popOver] dismissPopoverAnimated:YES];
There might be a better way to do this, but it works as expected
The IOS 6.0 SplitView template makes this easy, simply set the detailItem and the popover disappears if appropriate. There is even a check if you are using the same detaiItem so no page setup and refresh work gets done.
self.detailViewController.detailItem = self.detailViewController.detailItem;
I am using a UISplitViewController inside a UITabBarController with a plain UIViewController in the master pane of the split view and a UINavigationController in the detail pane, which itself contains a vanilla UIViewController.
I am aware that Apple advise to use split views at the root level only, however I have seen other applications (eg, Amazon- 'Wish List' tab) that use split views in tabs so I'm sure it's possible.
My problem is that the delegate methods of the split view, ie. those in UISplitViewControllerDelegate do not get called, which prevents me from creating my pop-over menu when switching into Portrait mode.
The methods in question are the following -
// Called when a button should be added to a toolbar for a hidden view controller
- (void)splitViewController: (UISplitViewController*)svc willHideViewController:(UIViewController *)aViewController withBarButtonItem:(UIBarButtonItem*)barButtonItem forPopoverController: (UIPopoverController*)pc;
// Called when the view is shown again in the split view, invalidating the button and popover controller
- (void)splitViewController: (UISplitViewController*)svc willShowViewController:(UIViewController *)aViewController invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem;
// Called when the view controller is shown in a popover so the delegate can take action like hiding other popovers.
- (void)splitViewController: (UISplitViewController*)svc popoverController: (UIPopoverController*)pc willPresentViewController:(UIViewController *)aViewController;
The UISplitViewController does receive the rotation notifications.
I can get the willShowViewController method to be called if I force the status bar orientation to landscape right (or left) at the beginning of the app launch, using
[[UIApplication sharedApplication] setStatusBarOrientation:UIInterfaceOrientationLandscapeRight];
However, the willHideViewController doesn't get called. And I don't want to force the app to start in landscape. If I do the same thing but force it to portrait, I don't receive the callbacks.
I don't understand why the split view controller is not calling it's delegate methods when it is otherwise behaving correctly. These methods should be called from its method-
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
internally, and when I breakpoint inside this, I can check that the delegate is set and that it is still alive.
Been stuck on this all day! Everything else is working great and I'm very pleased that the splitview / tabbar / navbar combination is working well. I just need these notifications.
Should I perhaps just call them manually when I rotate? Seems very wrong when the `UISplitViewController' should be doing this.
Solved, it has to be at either root level or a direct subview of a tabBar which also must be at root level. Annoying!
First, try to see if you are setting the correct delegates.
e.g., lets say you created three controllers,
UISplitViewController* splitView;
UIViewController* masterView;
UIViewController* detailView;
You implemented the delegate protocol at the detail view, so that when orientation changes, detail view should be able to put a button in the toolbar.
Now in order for splitView to call this function from delegate, you need to set the delegate itself.
So somewhere, if you are missing the following call,
splitView.delegate = detailView;
detailView's will never get notified of the orientation changes etc. At least this is where I was stuck.
I like the following method of sending a message from the master UIViewController to the detail UIViewController. Somewhere inside the master's implementation:
id detailViewController = [[self.splitViewController viewControllers] lastObject];
[detailViewController setSomeProperty:…];
This is from Paul Hegarty's Fall 2011 Stanford iTunesU iPad and iPhone Application Development course.