In my StoryBoard, I have embedded my root view controller inside a Navigation Controller, and this view gets displayed when the app is launched. The user then goes through a series of views, which are basically view controllers presented modally.
I'm trying to implement a function to go back to the root view controller, so I called
-(IBAction)backToMenu{
NSLog(#"Back to menu");
[self.navigationController popToRootViewControllerAnimated:YES];
}
but nothing happens. If I do NSLog(#"%#", self.navigationController"); it prints null, so I guess that's the source of my problem. You can't call popToRootViewControllerAnimated: on a view controller that's been presented modally.
Unless you pass a reference to the root view controller. But is this the right approach? If so, how do you pass a reference to the root view controller? As all my view controllers are instances of a custom subclass of UIViewController, I tried doing inserting this in said class's code :
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
[[segue destinationViewController] navigationController] = [[segue sourceViewController] navigationController];
}
but I get an error saying that navigationController is not assignable.
Any thoughts?
In the documentation for dismissViewControllerAnimated:completion:, it says:
If you present several view controllers in succession, thus building a
stack of presented view controllers, calling this method on a view
controller lower in the stack dismisses its immediate child view
controller and all view controllers above that child on the stack.
This suggests that you should keep a reference to your root view controller (or otherwise notify it) and call this method on it. (There's a similar note on the deprecated dismissModalViewControllerAnimated: in case you're using that.)
Does the class that implements the method -(IBAction)backToMenu inherit from UIViewController? I get thi skind of error when the class where I'm implementing the popToRootViewControllerAnimated inherits from some other class. To hold a reference to the original navigatorController I would:
Declare in the class where backToMenu is implemented, a pointer to a pointer to the navigation controller, something like: UINavigationController *navCon; yo should declare this as a property and then synthesizeit.
So when you instance the ViewController for this class you can do something like:
TheClassViewController *theClassVC = [TheClassViewController alloc] initWithNib:#"TheClassViewController" bundle:nil];
theClassVC.navCon = self.navigationController; // Here is where you pass THE reference
[self.navigationController pushViewController:theClassVC animated:YES];
Just solved it.
The problem was that, in the StoryBoard, the initial view controller was the root view controller and not the Navigation Controller it was embedded in!
Once you set the Navigation Controller as the initial view controller (i.e. drag the arrow so that it points to it), popToRootViewControllerAnimated works like a charm.
Related
I have two View Controllers: LevelSelectViewController and GameViewController.
Neither are the root view controller for the app I am making (the root view controller is called MainViewController).
How can I use the navigation method pushViewController:animated: for this transition for LevelSelectViewController to GameViewController?
In my LevelSelectViewController, you click a button and the following action method performs:
- (IBAction)buttonPressed:(id)sender {
// the pushViewController:animated: method hopefully can be used
// other code
}
How can I use the navigation method pushViewController:animated: for this transition for LevelSelectViewController to GameViewController?
The term "root view controller" can be a little confusing because UIWindow has a rootViewController property, and UINavigationController has a initWithRootViewController parameter. If you're calling -pushViewController:animated:, you must have a navigation controller in your view controller graph since that method belongs to UINavigationController. If your LevelSelectViewController instance is part of a navigation stack, you can do this:
- (IBAction)buttonPressed:(id)sender {
// create a game controller
GameViewController *gameController = [[GameViewController alloc] initWithNibName:nil bundle:nil];
// push it
[self.navigationController pushViewController:gameController animated:YES];
// other code
}
A more typical thing to do these days, though, is to put all the view controllers in a storyboard and simply connect the button to a push transition.
If you're using storyboards just create a segue. If you're not passing any information you can push to it by control dragging from one view controller to another. It doesn't matter if it's the root view controller or not.
If you want to pass data create a segue on storyboards from the view controller, not the index path or button etc. and invoke the prepareForSegueMethod in the view controller with the exact same identifier as the segue on storyboards.
I'm experiencing a memory leak (the UINavigationController and its root View Controller are both being leaked) when presenting and dismissing a UINavigationController in a subview. My method of presentation of the navigation controller seems a bit non-standard, so I was hoping someone in the SO community might be able to help.
1. Presentation
The Navigation Controller is presented as follows:
-(void) presentSubNavigationControllerWithRootViewControllerIdentifier:(NSString *)rootViewControllerIdentifier inStoryboardWithName:(NSString *)storyboardName {
// grab the root view controller from a storyboard
UIStoryboard * storyboard = [UIStoryboard storyboardWithName:storyboardName bundle:nil];
UIViewController * rootViewController = [storyboard instantiateViewControllerWithIdentifier:rootViewControllerIdentifier];
// instantiate the navigation controller
UINavigationController * nc = [[UINavigationController alloc] initWithRootViewController:rootViewController];
// perform some layout configuration that should be inconsequential to memory management (right?)
[nc setNavigationBarHidden:YES];
[nc setEdgesForExtendedLayout:UIRectEdgeLeft | UIRectEdgeRight | UIRectEdgeBottom];
nc.view.frame = _navControllerParentView.bounds;
// install the navigation controller (_navControllerParentView is a persisted IBOutlet)
[_navControllerParentView addSubview:nc.view];
// strong reference for easy access
[self setSubNavigationController:nc];
}
At this point, my expectation is that the only "owner" of the navigation controller is the parent view controller (in this case, self). However, when dismissing the navigation controller as shown below, it is not deallocated (and as a result its rootViewController is also leaked, and so on down the ownership tree).
2. Dismissal
Dismissal is pretty simple, but it seems not to be sufficient for proper memory management:
-(void) dismissSubNavigationController {
// prevent an orphan view from remaining in the view hierarchy
[_subNavigationController.view removeFromSuperview];
// release our reference to the navigation controller
[self setSubNavigationController:nil];
}
Surely something else is "retaining" the navigation controller as it is not deallocated. I don't think it could possibly be the root view controller retaining it, could it?
Some research has suggested that retainCount is meaningless, but FWIW I've determined that it remains at 2 after dismissal, where I would expect it to be zero.
Is there an entirely different / better method of presenting the subNavigationController? Maybe defining the navigation controller in the storyboard would have greater benefit than simply eliminating the need for a few lines of code?
It is best practice when adding a controller's view as a subview of another controller's view, that you make that added view's controller a child view controller; that is, the controller whose view your adding it to, should implement the custom container controller api. An easy way to set this up is to use a container view in the storyboard which gives you an embedded controller automatically (you can select that controller and, in the edit menu, choose embed in Navigation controller to get the UI you're trying to make). Normally, this embedded view controller would be added right after the parent controller's view is loaded, but you can suppress that by implementing shouldPerformSegueWithIdentifier:sender:. I created a simple test app with this storyboard,
The code in ViewController to suppress the initial presentation, and the button methods to subsequently present and dismiss it is below,
#implementation ViewController
-(BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender {
if ([identifier isEqualToString:#"Embed"]) { // The embed segue in IB was given this identifier. This method is not called when calling performSegueWithIdentifier:sender: in code (as in the button method below)
return NO;
}else{
return YES;
}
}
- (IBAction)showEmbed:(UIButton *)sender {
[self performSegueWithIdentifier:#"Embed" sender:self];
}
- (IBAction)dismissEmbed:(UIButton *)sender {
[[self.childViewControllers.firstObject view] removeFromSuperview];
[self.childViewControllers.firstObject willMoveToParentViewController:nil];
[self.childViewControllers.firstObject removeFromParentViewController];
}
#end
The navigation controller and any of its child view controllers are properly deallocated when the Dismiss button is touched.
The navigationController property on a UIViewController is retain/strong, which is presumably the other strong reference.
So try popping all view controllers from the navigation controller and see if that works.
I have a couple of "ViewControllers" and one for Update a picture in an iOS app.
The first one has a button when tapped asks if the user wants to use gallery photo or camera.
Now i am presenting this controller by using presentViewController on self.
But when the second view controller is presented i want to set the UIImagePicker source according to what the user has passed in.
I have made 2 different methods. one with camera source and one with "photoslibrary".
I don't know how to invoke one of these methods based on the use choice from the previous controller.
Am i going the right way with this approach? or should i just have one controller?
Basically there are two ways of passing data to a view controller.
Storyboard
If you are working with storyboard segues (that is, control drag from your "root" view controller to the destination view controller, choose a transition style and define a identifier), you can present the view controller
via
[self performSegueWithIdentifier:#"yourSegueIdentifier" sender:self];
Most of the times your destination view controller will be a custom class, so define a public property to hold the data you want to pass through. Then implement the following in your "root" view controller
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
// Setup the location menu delegate
if([segue.identifier isEqualToString:#"yourSegueIdentifier"]) {
// The custom class of your destination view controller,
// don't forget to import the corresponding header
ViewControllerCustomClass *vc = segue.destinationViewController;
// Set custom property
vc.chosenImageId = self.chosenImageId;
// Send message
[vc message];
}
}
Hints:If your destination view controller is the root view controller of a navigationViewController you can access it via [[segue.destinationViewController childViewControllers] objectAtIndex:0]; Additionally, as senderis an id, you can "abuse" it to pass any object through, just as a NSDictionary, for example.Also note, that when I am referring to the root view controller, I am talking of the view controller from which we segue to the destination from.
Programmatically
ViewControllerCustomClass *vc = [[ViewControllerCustomClass alloc] init];
vc.chosenImageId = self.chosenImageId;
// If you want to push it to the navigation controller
[self.navigationController pushViewController:vc animated:YES];
// If you want to open it modally
[self presentViewController:vc animated:YES completion:nil];
You can use inheritance, make your previous controller superClass, and invoke method in presentViewController in viewDidload.
I'm currently working on a project which implements a custom navigation controller, whose code is here:
https://gist.github.com/emilevictor/724a6602fedb8100650c
In one of my controllers, which gets pushed to the navigationController via a push segue, I have an action on a button to return to the main screen:
- (IBAction)returnToMainScreen:(id)sender
{
NSArray *returnedControllers = [self.navigationController popToRootViewControllerAnimated:YES];
NSLog(#"Popped to root view controller.");
}
This will return the current view controller and one preceding it in the returnedControllers array.
However, it doesn't change screens, or call any viewDidDisappear functions. Anyone know why?
Make sure you child viewControllers are being added to the parent viewController with the method [UIViewController addChildViewController:]
viewController's view is not loaded just after that viewController is pushed into navigation controller.
This is my code snippet.
- (void)myMethodInClassA {
// window's root view controller is navigation controller
UINavigationController *naviCtrl = (UINavigationController*)[UIApplication sharedApplication].keyWindow.rootViewController;
MyViewController *myVC = [[MyViewController alloc] initWithNibName:#"MyViewController" bundle:nil];
[naviCtrl pushViewController:myVC animated:NO];
// at this point, myVC's view is NOT loaded
}
When I call myMethodInClassA, myVC's viewDidLoad is called AFTER that method returns. I'd expected that myVC's view is loaded just after navigation controller's pushViewController:animated: is called and before myMethodInClassA returns.
When exactly view controller's view is loaded? Apple's documentation just says it is loaded when it is first accessed. It's a bit ambiguous. why doesn't navigation controller's pushViewController: access view controller's view?
p.s. sorry for initial ambiguous question.
Pushing a view controller (VC) onto a navigation controller's stack makes the VC into a child view controller of the navigation controller (which is a container view controller). Creating such a child-parent relationship is a distinct step which does not cause the child VC's view to be loaded immediately. Rather the container VC loads the view at a later time. I believe there is no explicit specification for what "later" means - usually it will be when the container VC has decided that the time has come to integrate the child VC's view into the container VC's view hierarchy. But basically it simply happens at the discretion of the container VC's implementation.
That being said, anyone can force a VC's view to be loaded by simply accessing the VC's view property. For instance, in your code you could add this line
myVC.view;
which would trigger loadView and then viewDidLoad in MyViewController.
However, in your case if MyViewController needs to react to the event that it has been associated with a container VC, then it would be better to override one (or both?) of the following methods in MyViewController:
- (void) willMoveToParentViewController:(UIViewController*)parent
{
// write your code here
}
- (void) didMoveToParentViewController:(UIViewController*)parent
{
// write your code here
}
You need to be aware, though, that willMoveToParentViewController and didMoveToParentViewController are also invoked when MyViewController is popped from its parent navigation controller's stack. You can detect that this is the case by checking the parent argument for nil.
(Swift 2)
Since this question doesn't have an accepted answer...
What I ended up doing is create a convenience init at the child view controller:
convenience init() {
self.init(nibName: "ChildViewController", bundle: nil)
//initializing the view Controller form specified NIB file
}
and in the parentViewController's viewDidLoad():
let commentsView = CommentsViewController()
self.addChildViewController(commentsView)
self.momentsScrollView.addSubview(commentsView.view)
commentsView.didMoveToParentViewController(self)
As stated above,viewDidLoad gets called once when a view is pushed,you might want to do your stuff in viewWillAppear or viewDidAppear.
Ya if that ViewController will be already pushed in navigationController stack then ViewDidLoad method will not be called again.
First time when you will push that ViewController then viewDidLoad will be called.
So if you need that your some functionality is to be executed every time then implement it in viewWillAppear method because it will be called every-time you push your viewController.
Hope it helps you.
are you pushing the view controller for the first tym?if YES then only viewDidLoad() of the controller will be called and if its already pushed and this is not the first tyn then viewWillAppear () will be called.(or) if you are making a new instance every tym u push it then viewDidLoad() will be called.
I find that I have to call loadViewIfNeeded()
https://developer.apple.com/documentation/uikit/uiviewcontroller/1621446-loadviewifneeded