SplitViewController Woes - replacing detail view controller in portrait mode failure - iphone

I've got a SVC setup which works well. The master and detail views of the SPC are both UINavigationControllers, with separate root view controllers each.
My application works perfectly. I can start it up in either portrait or landscape mode and everything works as it aught to. The 'master' popup button is visible in portrait mode but hidden in landscape mode. Both master and detail planes animate and rotate properly, there is no popping or weird animation artifacts present, and everything stretches properly. 100% satisfaction per spec.
Issues arise when I replace the detail view controller. Since we cannot replace the root node of a uinavi controller, what I do is create a brand new UINavigationController, assigning it the new view I want in the detail view. I do this because the views in the detail view can do 1-2-3 levels deep, but I need to be able to assign unique 'root' views (eg I need to be able to completely replace the root UINavigationController).
The code looks like this:
self.detailViewController = [[SomeNewController1of3 alloc] initWithNibName:#"SomeNewController1of3" bundle:nil];
self.splitViewController.delegate = self.detailViewController;
UINavigationController *newNC = [[UINavigationController alloc] initWithRootViewController:self.detailViewController];
self.splitViewController.viewControllers = #[navigationController, newN];
First, I create the new viewcontroller, whatever it may be. I then set the splitviewcontroller's delegate to = the newly created viewcontroller (so that willHideViewController, etc fires). Then I create the a navicontroller to house the newly created viewcontroller, so that it can push and pop from it. And finally, I replace the second (detail) view of the split view controller with my new navi controller.
Again, this plan works flawlessly when I start in landscape.
When I start in portrait, these are the anomalies I've noticed:
All popups (alert boxes, the master view, etc) have weird rotations and flip out from an awkward spot when they are shown. It's almost as if it rotates these popups right as they're displayed, instead of having them simply 'slide' out of their correct locations.
When the master view is displayed, after doing the weird rotation describes above, it disappears completely for an instant (leaving a black, empty region) then reanimates to it's original location.
Until I complete a device rotation, the 'master' button on the detail view controller which displays the popup for the master view is not shown. After confirming with breakpoints, I've indeed concluded that willHideViewController is Not being called by my above method.
What I don't get is why everything behaves flawlessly when I start in landscape, but all of these errors arise in portrait? All of my views actually have a default orientation set to portrait in IB, and are scale to fit. Also it's not like they're crazy interfaces, they are all just stock tableviews.
Have any of you encountered this issue or found a method around it?

Firstly, the UISplitViewController delegate should be a top level object like your App Delegate, not a view controller the split controller is showing.
Secondly, to answer your question, to replace a showing detail view controller (i.e. pushed on the master nav) in portrait you can make use of a split controller delegate method:
AppDelegate.m
- (BOOL)splitViewController:(UISplitViewController *)splitViewController showDetailViewController:(UIViewController *)vc sender:(id)sender{
if(!splitViewController.isCollapsed){
return NO;
}
UINavigationController *masterNav = splitViewController.viewControllers.firstObject;
if(![masterNav.topViewController isKindOfClass:UINavigationController.class]){
return NO;
}
UINavigationController *existingDetailNav = (UINavigationController *)masterNav.topViewController;
UINavigationController *newDetailNav = (UINavigationController *)vc;
existingDetailNav.viewControllers = #[newDetailNav.viewControllers.firstObject];
return YES;
}
You might be wondering why we don't just replace the master nav stack. Well the problem is the split view controller preserves the detail view controller from the last time showDetail that went through its default behavior. So if we were in portrait, tapped on a cell to show detail, then replaced the detail, then navigate back then upon rotating to landscape the old detail will show on the right and not the new one. By replacing the detail nav stack with the new detail controller as above this solves this because although the split is showing the preserved detail nav it now contains the new detail.

I find it is easier to subclass the SplitViewController. From there, you can easily swap out the the detail adding a navController for those that need it.
Subclass UISplitViewController and set your root splitViewController to that class. Then add this method to your UISplitViewController subclass:
-(void)setDetailControllerTo:(UIViewController *)detailController withNavControllerTitle:(NSString *)title {
[detailController view]; // this line forces the viewDidLoad method to be called
if (title) {
UINavigationController *navController = [[UINavigationController alloc] init];
[navController pushViewController:detailController animated:YES];
detailController.title = title;
NSArray *viewControllers=#[self.mainController.viewControllers[0],navController];
self.mainController.viewControllers = viewControllers;
} else {
NSArray *viewControllers=#[self.mainController.viewControllers[0],detailController];
self.mainController.viewControllers = viewControllers;
}
}
To call this method do something like this from the master view controller in the tableView:didSelectRowAtIndexPath: method
FixedSplitViewController *splitController = (FixedSplitViewController*) self.splitViewController;
CurrentEventViewController *controller = [self.storyboard instantiateViewControllerWithIdentifier:#"CurrentEventViewController"];
// add any setup code here
[splitController setDetailControllerTo:controller withNavControllerTitle:#"Current Event"];
If you wish to keep the master view visible in portrait rotation, add this method to the SplitViewController subclass:
-(BOOL)splitViewController:(UISplitViewController *)svc shouldHideViewController:(UIViewController *)vc inOrientation:(UIInterfaceOrientation)orientation {
return NO;
}

Related

Present UIImagePickerController (or any view controller) from UIView

Ok so I'm trying to present a view controller from a UIView like this:
CSCamera *camera = [CSCamera cameraFromYamlNode:answer forView:view];
[view addSubview:camera.view];
Now my CSCamera view is basically a view controller that does some custom managing of UIImagePickerController (custom overlays and things). In order to do this I have a method showCamera() that is called to modally present the picker after I add camera.view as a subview:
[camera showCamera];
However I am getting this error:
Warning: Attempt to present <UIImagePickerController: 0x1eda8d90> on <CSCamera: 0x2008e4d0> which is already presenting (null)
Is what I'm trying to do possible?
You have a big structural problem.
CSCamera *camera = [CSCamera cameraFromYamlNode:answer forView:view];
[view addSubview:camera.view];
Adding a subview that belongs to a different view controller is unsupported. I think it's the root cause of all your woes.

Autorotating only some of the tabs inside a UITabBar? (ios 5)

I have a UITabBar with 5 tabs. I only wish to enable autorotation for a UIViewController that gets pushed onto the stack deep inside tab #3. So to be clear: tap UITabBar item 3, and you get tabbar item #3's root UIView, which should not autorotate. Tap and get another UIViewController pushed onto the stack (via a UINavigationController). Tap again, and get another UIViewController pushed onto the stack. Only here should this UIView autorotate.
The other 4 tabs should not rotate at all--not the root view of the tabs, nor any of the child views of the tabs.
Can someone tell me what approach I should use? I read that every single tab needs to respond "YES" to willAutorotateToInterfaceOrientation.
In each view's shouldAutorotate..., you could call a method in the root view controller that checks what is currently being displayed. If the deep-level view for tab 3 is on display, it will return YES, otherwise NO, and the views will, in turn, return the same.
Edit -- more detail per user798719's request:
Your root view controller knows which view is on display. You add a method to the root view controller - (BOOL) isDeepLevelTab3Displayed;. The method checks whether the deep-level view for tab 3 is on display and, if so, returns YES, otherwise returns NO.
Each sub view controller’s shouldAutorotate… method will get a ref to the root controller so that it can call isDeepLevelTab3Displayed.
If you’re using a navigation-style controller, you can get the ref like this (self is the sub controller):
NSArray *arrayOfControllers = [self viewControllers];
UIViewController *rootController = [arrayOfControllers objectAtIndex:0]; // per UIViewController class ref, root controller is at index 0
Or you could get anything in your project like this:
YourProjectAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
UIViewController *rootController = appDelegate.rootController; // or appDelegate.intermediateClass1.intermClass2.rootController — however you set up your project
So every sub controller would do this:
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation {
UIViewController *rootController = [[self viewControllers] objectAtIndex:0];
return [rootController isDeepLevelTab3Displayed];
}
Therefore, every subcontroller would return YES when autorotation should happen, fulfilling the requirement you mention at the end of your question.
However, if all your subcontrollers are instances of UINavigationController, you could determine which view is currently on display directly, by calling visibleViewController. Then you just need a way of checking its identity.
You could check the controller’s nibName or title, for example, against a constant, or add an integer property intControllerIdentity to all your controllers and set them in the controller’s initWithNibName…
The integer-property scheme might be best, because it won’t be affected should you later change the nibName or title.
You’d add constants to some class whose h file is imported by all the controllers (or, if all the controllers are instances of the same class, put these constants in that class’s h file):
#define kFooController 1
#define kBarController 2
#define kRotatableController 3
And you’d set it like this:
self.intControllerIdentity = kRotatableController;
And check it like this:
if (self.intControllerIdentity == kRotatableController)
Hope that helps. But evaluate this added detail with a critical eye; I have worked with autorotation but not yet with navigation controllers.

Updating UINavigationBar Title

I have a split view controller and a navigationcontroller on the left side. As views are pushed onto the nav controller I'm attempting to change the Navigation Bar title. The problem seems to be that the text can only be updated in viewWillAppear. If I try to change the text after that initial load, it doesn't update. At first I thought I needed to call setNeedsDisplay on the NavigationItem, but it doesn't have a property. I don't understand because the same code works in viewWillAppear but not once the view is shown.
I'm using:
(void)viewWillAppear:(BOOL)animated{
self.title = #"blah";
...
}
It should be self.navigationItem.title = #"Some Title";
I've set up a simple project where initial controller is embedded in navigation controller, and the second controller is pushed onto the navigation stack. Second controller has a button which calls this bit of code:
- (IBAction)changeTitle:(id)sender {
static unsigned int i = 1;
self.navigationItem.title = [NSString stringWithFormat:#"Number of changes: %ud", i];
i++;
}
It works as intended. Clearly it happens after all appear/layout events. So your problem lies somewhere else. Maybe self that you are referring to is a wrong controller.
self.navigationBar.topItem.title = [NSString stringWithFormat:#"TOP"];

iphone - 3 view controllers how to return to first one

I have a game with menu - level select - game vc
I'd like to dump the user out to the menu after the game, but I'm doing presentModalViewController's and if I keep a reference to level select VC in my game VC I can dismiss it, but then we see the level select screen for a split second before going back to the menu... ( no animations )
Is there another way to do this? All I know is having a top bar ( navigationController? ) which I don't want showing and this presentModal way I'm currently using to switch view controllers.
How I dismiss the game view controller:
- (void)dismissMe {
NSLog(#"dismissMe");
[mParentVC dismissModalViewControllerAnimated:NO];
// [self dismissModalViewControllerAnimated:NO];
}
You can set the rootViewController on the UIWindow object.
[[UIApplication] sharedApplication].keyWindow.rootViewController = theVCYouWantDisplayed
You can use UINavigationController with navigationBarHidden property set to YES

Act on click of a button on the Nav Bar for moreNavigationController -- Can't pushviewcontroller

Okay, here is my issue: My app has a display of categories in the tab bar at the bottom of the iPhoneOS screen. This only allows 5 categories before it presents the MORE button. I have over 25 (please do not answer this by saying: "Rethink your application...etc" -- that was rudely said before. They are food, drink, etc categories and cannot be changed). I want to allow the user to put their favorites on the home page. The Apple moreNavigationController editing system only allows 20 tab bar items to be rearranged due to space constraints on the editing page. This is not enough so i need to implement my own Editing screen. I set the rightBarButtonItem to nil and created my own. Using NSLog, i can see the "click" happens when clicking the EDIT button, but I cannot push using pushViewController. Nothing happens. I think it has something to do with the navigationController I am addressing...but i am not sure. ps: This all happens in my App Delegate which DOES act as both UITabBarControllerDelegate & UINavigationControllerDelegate.
I tried to do the following:
- ( void )navigationController:( UINavigationController * )navigationController_local willShowViewController:( UIViewController * )viewController_local animated:( BOOL )animated
{
UIViewController * currentController = navigationController_local.visibleViewController;
UIViewController * nextController = viewController_local;
// Do whatever here.
NSLog(#"Nav contoller willShowViewController fired\n'%#'\n'%#'\nThere are currently: %d views on the stack\n",currentController,nextController,[self.navigationController.viewControllers count]);
if ( [nextController isKindOfClass:NSClassFromString(#"UIMoreListController")])
{
UINavigationBar *morenavbar = navigationController_local.navigationBar;
UINavigationItem *morenavitem = morenavbar.topItem;
morenavitem.rightBarButtonItem = nil;
NSLog(#"Is a UIMoreListController\n");
UIBarButtonItem *editTabBarButton = [[UIBarButtonItem alloc]
initWithTitle:#"Edit"
style:UIBarButtonItemStylePlain
target:self
action:#selector(editTabBar:)];
morenavitem.rightBarButtonItem = editTabBarButton;
[editTabBarButton release];
}
}
This works to place an EDIT button at the top right of the screen -- mimicking Apple's look and feel... but when that button is clicked, you cannot exit the darn moreNavigationController.
I have tried many things. UIAlerts work, etc...but pushing (or popping -- even popping to root view) a view controller on the stack does not.
- (void) editTabBar:(id)sender {
NSLog(#"clicked edit tabbar\n");
NSLog(#"Total count of controllers: %d\n",[self.navigationController.viewControllers count]);
TabBarViewController *tabBarViewController2 = [[TabBarViewController alloc] initWithNibName:#"TabBarView" bundle:nil];
tabBarViewController2.navigationItem.title=#"Edit Tab Bar";
[self.navigationController pushViewController:tabBarViewController2 animated:YES];
[tabBarViewController2 release];
NSLog(#"finished edit tabbar\n");
}
If you click the edit button on the moreNavigationController's display page, you get the log entries like expected AND (this is strange) the views on the stack climbs -- but no page change occurs. I marked it down to not using the correct navigation controller...but I am lost on how to find which one TO use.
this is a weird one too. In the edit function if i just do this:
- (void) editTabBar:(id)sender {
self.tabBarController.selectedIndex = 0;
}
It DOES take me home (to tabbarcontroller 0)
BUT doing this:
- (void) editTabBar:(id)sender {
[self.navigationController popToRootViewControllerAnimated:YES];
}
does not work.
Does the moreNavigationController have some special quality that screws with the rest of the system?
I would try reimplementing the whole "More" functionality from scratch. In other words, store the four home tabs in your user defaults and add a dummy fifth tab that switches to your own complete reimplementation of the more view controller stack.
You could even write a lightweight subclass of UITabBarController that handled this for you.
UITabBarController is evil, so I wouldn't be at all surprised if MoreController had some special properties, too.
I have had success intercepting the More Controller in shouldSelectViewController to change the data source; you may be able to find some workaround there.
PS I am inclined to agree that you could consider redesigning your app so that you didn't need an unlimited number of viewControllers attached to the tab bar just to select categories; you might have better luck using a tool bar with a single, scrollable, custom view in it. If that's really the best way of picking categories for your app, of course.