I am writing tests to verify that a new view controller of a certain type is pushed on a navigation controller's stack when a certain action is made. However, I'm finding that calling pushViewController does not result in my UIViewControllers list of view controllers being updated nor the top one being the newly pushed controller.
Here's an abbreviated version of my test class:
#import "RRViewController.h"
#import "RestroomDetailsViewController.h"
#implementation RRViewControllerTests
{
RRViewController *viewController;
...
UINavigationController *navigationController;
}
- (void)setUp
{
[super setUp];
viewController = [[RRViewController alloc] init];
...
navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];
}
...
- (void)testSelectingRestroomPushesNewViewController
{
[viewController userDidSelectRestroom];
UIViewController *currentTopViewController = navigationController.topViewController;
XCTAssertFalse([currentTopViewController isEqual:viewController], #"New view controller should be pushed onto Navigation Controller stack.");
XCTAssertTrue([currentTopViewController isKindOfClass:[RestroomDetailsViewController class]], #"New view Controller should be an RestroomDetailsViewController.");
}
And here is the RRViewController class with that userDidSelectRestroomn method called on viewController:
#import "RestroomDetailsViewController"
...
- (void)userDidSelectRestroom
{
RestroomDetailsViewController *nextViewController = [[RestroomDetailsViewController alloc] init];
[[self navigationController] pushViewController:nextViewController animated:YES];
}
The issue is that both of my tests are failing. Taking a look at navigationController.viewControllers in the RRViewControllerTests test class after userDidSelectRestroom is called shows:
<__NSArrayI 0x7ffaf9d37020>(
<RRViewController: 0x7ffaf9eba420>
)
i.e. just the original root RRViewController.
Any thoughts as to why the new RestroomDetailsViewController is not pushed onto navigationController's stack?
Related
I am trying to incorporate State Restoration in my app. I have it working fine for the most part, but presenting a navigation controller for a modal view on top of another navigation controller seems challenging.
For testing, I created a new split-view app on the iPad, with navigation controllers for both sides of the split view, and a Master and Detail view controller for each side, the roots of their respective navcontrollers. In the master view, you can click on a button to push a new TestViewController onto the navController stack programatically. I hook up the splitView in the storyboard, add restorationIDs to everything, opt-in to the delegate, provide a restoration class and adhere to the UIViewControllerRestoration protocol for TestViewController (since it's created programmatically) and everything works fine. If I close the app and retort it, it will start the TestViewController pushed onto the master's navcontroller. So far so good.
I then change the button handler to present the TestViewController inside a new UINavigationController, present it onto the master's navigation controller, to show a modal view (instead of pushing it on the nav stack). Now, when I relaunch the app, there is no modal view there anymore. TestModalViewController's viewControllerWithRestorationIdentifierPath:coder: is actually called correctly as before, but the modal view is never presented for some reason.
Here is the code for what I'm talking about
MasterViewController.h:
- (void)pushButton:(id)sender
{
TestModalViewController *test = [[TestModalViewController alloc] initWithNibName:#"TestViewController" bundle:nil];
test.restorationIdentifier = #"testid";
test.restorationClass = [TestModalViewController class];
UINavigationController *modal = [[UINavigationController alloc] initWithRootViewController:test];
modal.modalPresentationStyle = UIModalPresentationFormSheet;
modal.restorationIdentifier = #"ModalTestID";
[self.navigationController presentViewController:modal animated:YES completion:nil];
return;
}
TestModalViewController.m:
+ (UIViewController *) viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder {
TestModalViewController *test = [[TestModalViewController alloc] initWithNibName:#"TestViewController" bundle:nil];
test.restorationClass = [TestModalViewController class];
test.restorationIdentifier = [identifierComponents lastObject];
return test;
}
Perhaps the UINavigationController that is created to display modally is never preserved? Not sure why, because it does have a restorationIdentifier.
Edit:
After further testing, it turns out if I remove the UINavigationController from the the pushButton: code, and present the TestModalViewController instance directly, it gets restored correctly. So something about the UINavigationController being presented from another UINavigationController?
This works (though not what I really want):
- (void)pushButton:(id)sender
{
TestModalViewController *test = [[TestModalViewController alloc] initWithNibName:#"TestViewController" bundle:nil];
test.restorationIdentifier = #"testid";
test.restorationClass = [TestModalViewController class];
//UINavigationController *modal = [[UINavigationController alloc] initWithRootViewController:test];
//modal.modalPresentationStyle = UIModalPresentationFormSheet;
//modal.restorationIdentifier = #"ModalTestID";
[self.navigationController presentViewController:test animated:YES completion:nil];
return;
}
EDIT:
Attached link to test project: dropbox.com/sh/w8herpy2djjl1kw/vw_ZWqimgt
It's basically the Core Data master-detail template; run it on the iPad simulator. The + button in Master invokes the TestModalVC; if you then press the Home button, then kill debugger and launch again, you see the snapshot contains the TestModalVC but when the app is launched, it doesn't get restored
You can either create your own restoration class to handle this, or add the following to your app delegate:
- (UIViewController *)application:(UIApplication *)application viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents
coder:(NSCoder *)coder
{
NSString *lastIdentifier = [identifierComponents lastObject];
if ([lastIdentifier isEqualToString:#"ModalTestID"])
{
UINavigationController *nc = [[UINavigationController alloc] init];
nc.restorationIdentifier = #"ModalTestID";
return nc;
}
else if(...) //Other navigation controllers
{
}
return nil;
}
More information in the documentation.
to begin with, here is some code:
- (void)viewDidLoad
{
[super viewDidLoad];
FirstViewController *first = [[FirstViewController alloc] init];
SecondViewController *second = [[SecondViewController alloc] init];
MBPullDownController *pullDownController = [[MBPullDownController alloc] initWithFrontController:first backController:second];
[self.navigationController addChildViewController:pullDownController];
}
- (void)pushAnotherViewController:(NSNotification *)notification
{
AnotherViewController *another = [self.storyboard instantiateViewControllerWithIdentifier:#"anotherViewController"];
[self pushScheduleViewController:another];
}
I use the MBPullDownController open source control. Using to seperate view controllers I load into the pull down controller. This code is in a view controller called RootViewController which is embedded in a UINavigationController. Then there's a method for pushing another view controller in the navigation controller. It's when I try to use the method (in AnotherViewController) popToRootViewController: that my app crashes and the EXC_BAD_ACCESS message comes up in the console.
EDIT
This is my code in "AnotherViewController"
- (void)popBack
{
RootScheduleViewController *root = [[RootScheduleViewController alloc] init];
[self.navigationController popToViewController:root animated:YES];
}
You are getting a bad access error when you call popBack because you are creating a new instance of the view controller and then trying to pop to it. For a navigation controller, the view controller must be part of the navigation stack in order to pop to it. So if an instance of this view controller exists, find it in the navigation stack and pop to it.
for(UIViewController * viewController in self.navigationController.viewControllers){
if([viewController isKindOfClass:[RootScheduleViewController class]]){
[self.navigationController popToViewController:viewController animated:NO];
break;
}
}
I have a custom view controller named CKCalendarViewControllerInternal.
CKCalendarViewControllerInternal
This class is the subclass of UIViewController.
CkCalendarViewController
I have a custom view controller named CKCalendarViewController. It's a subclass of UINavigationController as follow:
#interface CKCalendarViewController : UINavigationController <CKCalendarViewDelegate, UINavigationControllerDelegate>
This class is initialize with the CKCalendarViewControllerInternal as follow:
- (id)init
{
CKCalendarViewControllerInternal *calendarViewController = [CKCalendarViewControllerInternal new];
self = [super initWithRootViewController:calendarViewController];
}
Now, In AppDelegate my first view is as follow:
AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.viewController = [[[ViewController alloc] initWithNibName:#"ViewController" bundle:nil] autorelease];
UINavigationController *n1=[[UINavigationController alloc]init];
n1.viewControllers=[[NSArray alloc]initWithObjects:self.viewController, nil];
self.window.rootViewController=n1;
[self.window makeKeyAndVisible];
return YES;
}
CkDemoViewController
This class is the subclass of CkCalendarViewController as follow
#interface CKDemoViewController : CKCalendarViewController
ViewController.m
When i try to push the CKDemoViewController on button clicked.
Error & Question
It shows me error like
Exception: Pushing a navigation controller is not supported
Exception: [NSException]:Pushing a navigation controller is not supported
ex.name:'NSInvalidArgumentException'
ex.reason:'Pushing a navigation controller is not supported'
Reason for error
This is because the CKCalendarViewController is the subclass of UINavigationController.
If i try to open the modal view, it works perfectly.
But How can i initialize the CKCalendarViewController as shown above with the CKCalendarViewControllerInternal class??
Thank you,
Answer will greatly appreciate
If I understand correctly what you are doing, the simplest "hackish" way to make things work would be making CKCalendarViewController derive from CKCalendarViewControllerInternal. I am suggesting this because I see that you are trying to use your CKCalendarViewController as a normal view controller, so there should be no reason to have it be a navigation controller.
Another possibility would be for you to actually use your CKCalendarViewController as a navigation controller by doing this in your app delegate:
UINavigationController *n1 = [[CKCalendarViewController alloc]init];
n1.viewControllers = [[NSArray alloc]initWithObjects:self.viewController, nil];
self.window.rootViewController = n1;
but this depends on what you are trying to achieve.
More generally, if you are interested in "nesting" controllers within controllers, you should learn about controller containment. In controller containment, what you do to add a controller to another one is basically this:
[vc willMoveToParentViewController:self];
[self addChildViewController:vc];
[self.view addSubview:vc.view];
[vc didMoveToParentViewController:self];
I have a navigation based application.On click of a button on the navigation bar in the first screen , I am able to push another view controller as follows :
-(void) buttonClicked:(id)sender
{
UIViewController* mv = [[SecondViewController alloc] init];
[[self navigationController] pushViewController:mv animated:YES];
}
Now i have a UIView(separate .h and .m files) as part of the first screen. On click of a button in the UIView, i want to push the SecondViewController.
I have tried the following :
UIViewController* mv = [[SecondViewController alloc] init];
UIViewController * home=[[FirstViewController alloc]init];
[[home navigationController] pushViewController:mv animated:YES];
It doesnt work!! Kindly help
UIViewController* mv = [[SecondViewController alloc] init];
UIViewController * home=[[FirstViewController alloc]init];
[[home navigationController] pushViewController:mv animated:YES];
The problem here is that home isn't part of the navigation stack, so [home navigationController] is surely nil. I'm not quite clear on what you're trying to do here, but just creating a view controller doesn't mean that it's actually part of the view controller graph.
Why would it work? Randomly creating view controllers whose view is not even visible, is not the solution. You can either keep a reference to the VC in the view like this:
#imlementation ViewController
- (id) init
{
// ...
aView = [[CustomView alloc] init];
aView.viewController = self;
// ...
}
#end
#interface CustomView
#property (assign) ViewController *viewController;
#end
Or you can search the responder chain at runtime:
UIResponder *next = [view nextResponder];
while (next)
{
if ([next isKindOfClass:[ViewController class]])
{
break;
}
next = [next nextResponder];
}
And now "next" will contain the view controller (or nil if it can't be found).
Try using the same navigationController to push view, this keeps the same stack of ViewControllers.
UIViewController* mv = [[SecondViewController alloc] init];
[[self navigationController] pushViewController:mv animated:YES];
[mv release];
I see your problem now! You need to #import your FirstViewController, then #class it. Then do your push.
So:
//.h
#import "FirstViewContoller.h"
#class FirstViewController;
#interface...
//.m
-(void)return {
FirstViewController *firstview = [[FirstViewController alloc]init(withnibname:)];
[firstView.navigationController pushViewController: firstView.navigationController.topViewController animated: TRUE];
}
If I am not wrong, your UIView though is in separate files, is still added to the screen from a UIViewController class.
Simply, post a notification from UIView to your FirstViewController class where you have access to the navigation controller. Then push the SecondViewController from there.
You Can use this. It Works very well for me:-
Firstly Create Object of AppDelegate in UIView Class and initialize it. Then create Navigationcontroller object in Appdelegate.h :-
#property(strong,nonatomic) UINavigationController *navControl;
In your UIView Class implement this code where you want to push :-
ViewController *objview = [[ViewController alloc]init]; [appDelegate.navControl pushViewController:objview animated:YES];
My app allows the user to switch between two different modal view controllers (for two different styles of data entry). The code below used to work (in iOS 4.3 and earlier):
UIViewController * parent = current.parentViewController;
[current dismissModalViewControllerAnimated:NO];
svc.modalPresentationStyle = UIModalPresentationFormSheet;
[parent presentModalViewController:svc animated:NO];
[svc release];
but no longer (in iOS 5) - the "current" view controller dismisses, but "svc" is not presented.
Any idea why it broke (i.e. what did I do wrong)?
Any idea how to do it "right" (so that it works on 5.0 as well as 4.3 and earlier)?
Jeff Hay was totally right in his comment except for one thing. You should do it in the -viewDidAppear: method of the view controller which originally presented the first modal view controller.
Example:
// MyViewController.h
#interface MyViewController : UIViewController {
BOOL _shouldPresentSecondModalViewController;
}
#end
// MyViewController.m
#implementation MyViewController
- (void)viewDidAppear:(BOOL)animated {
if(_shouldPresentSecondModalViewController) {
UINavigationController *myNavCon;
// Code to create second modal navigation controller
[self presentModalViewController:myNavCon animated:YES];
_shouldPresentSecondModalViewController = NO;
}
}
- (void)presentFirstViewController {
UINavigationController *myNavCon;
// Code to create the first navigation controller
_shouldPresentSecondModalViewController = YES;
[self presentModalViewController:myNavCon animated:YES];
}
#end
EDIT:
Now, if you want to pass data between the two modal view controllers, you can use a delegate.
// FirstModalViewControllerDelegate.h
#protocol FirstModalViewControllerDelegate
#optional
- (void)controller:(FirstModalViewControllerDelegate *)vc shouldShowData:(id)anyType;
#end
// MyViewController.h
#interface MyViewController : UIViewController <FirstModalViewControllerDelegate> {
id _dataToDisplay;
}
#end
// MyViewController.m
#implementation MyViewController
- (void)viewDidAppear:(BOOL)animated {
if(_dataToDisplay != nil) {
UINavigationController *myNavCon;
// Code to create second modal navigation controller
[self presentModalViewController:myNavCon animated:YES];
[_dataToDisplay release];
_dataToDisplay = nil;
}
}
- (void)presentFirstViewController {
UINavigationController *myNavCon;
FirstModalViewController *myCon;
// Code to create the first modal view controller
[myCon setDelegate:self];
myNavCon = [[UINavigationController alloc] initWithRootViewController:myCon];
[self presentModalViewController:myNavCon animated:YES];
[myNavCon release];
}
- (void)controller:(FirstModalViewControllerDelegate *)vc shouldShowData:(id)anyType {
/* This method will get called if the first modal view controller wants to display
some data. If the first modal view controller doesn't call this method, the
_dataToDisplay instance variable will stay nil. However, in that case, you'll of
course need to implement other methods to, like a response to a Done button, dismiss
the modal view controller */
[self dismissModalViewController];
_dataToDisplay = [anyType retain];
}
#end