This problem sounds quite basic but I don’t understand what I am overlooking.
I am trying to push a new view controller into a navigation controller, however the topViewController remains unaffected.
#import "TNPViewController.h"
#interface TNCViewController : UIViewController <UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
#implementation TNCViewController
-(void)userDidSelectNewsNotification:(NSNotification*)note
{
TNPViewController *nextViewController = [[TNPViewController alloc] init];
[[self navigationController] pushViewController:nextViewController animated:YES];
UIViewController *test = [[self navigationController] topViewController];
}
The test shows an instance of TNCViewController instead of TNPViewController. How is this possible?
UPDATE
Thanks for everyone's participation. The method name indicating notifications is a red herring. I found the problem, as Stuart had mentioned previously but deleted later on. (As I have high reputation score, I still can see his deleted post).
My initial unit test was this:
-(void)testSelectingNewsPushesNewViewController
{
[viewController userDidSelectNewsNotification:nil];
UIViewController *currentTopVC = navController.topViewController;
XCTAssertFalse([currentTopVC isEqual:viewController], #"New viewcontroller should be pushed onto the stack.");
XCTAssertTrue([currentTopVC isKindOfClass:[TNPViewController class]], #"New vc should be a TNPViewController");
}
And it failed. Then I set a breakpoint and tried the test instance above and it still was showing the wrong topviewcontroller.
At least the unit test works if I change
[[self navigationController] pushViewController:nextViewController animated:YES];
to
[[self navigationController] pushViewController:nextViewController animated:NO];
A better solution is to use an ANIMATED constant for unit tests to disable the animations.
This doesn't really answer your question about why your navigationController is not pushing your VC. But it is a suggestion about another possible approach.
You could instead add a new VC on the Storyboard and simply activate the segue when the userDidSelectNewsNotification method is activated. Then change the information accordingly to the event in the VC, specially since you are initializing it every time anyway.
This is something of a stab in the dark, but the issue is hard to diagnose without more information.
I see you're trying to push the new view controller in response to a notification. Are you sure this notification is being handled on the main thread? UI methods such as pushing new view controllers will fail (or at least behave unpredictably) when not performed on the main thread. This may also go some way to explaining the odd behaviour of topViewController returning an unexpected view controller instance.*
Ideally, you should guarantee these notifications are posted on the main thread, so they will be received on that same thread. If you cannot guarantee this (for example if you're not responsible for posting the notifications elsewhere in your code), then you should dispatch any UI-related code to the main thread:
- (void)userDidSelectNewsNotification:(NSNotification *)note
{
dispatch_async(dispatch_get_main_queue(), ^{
TNPViewController *nextViewController = [[TNPViewController alloc] initWithNibName:#"TNPViewController" bundle:nil];
[self.navigationController pushViewController:nextViewController animated:YES];
});
}
Also, it appears you are not initialising TNPViewController using the designated initialiser (unless in your subclass you are overriding init and calling through to initWithNibName:bundle: from there?). I wouldn't expect this to cause the transition to fail entirely, but may result in your view controller not being properly initialised.
In general, you might be better creating your view controllers in a storyboard and using segues to perform your navigation transitions, as #Joze suggests in his answer. You can still initiate these storyboard segues in code (e.g. in response to your notification) with performSegueWithIdentifier:, but again, be sure to do so on the main thread. See Using View Controllers in Your App for more details on this approach.
*I originally wrote an answer trying to explain the unexpected topViewController value as being a result of deferred animated transitions. While it is true that animated transitions are deferred, this does not prevent topViewController from being set to the new view controller immediately.
Body:
I am getting a Memory warning in my app, after which the UI stops responding. And, in the XCode logs I do see ViewController being Unloaded message.
I am afraid it is because I am not handling the transitions between the views properly and which is causing this memory issue.
Brief description of the ViewControllers(VC) I have and how I perform the transition:
I have 1 main/home VC which is the start of the main workflow of my app.
And from all other VCs, I have links to come back to the Home VC.
So, instead of having Segues from all the VCs to the 1st one, I use the following way:
UIStoryboard* sb = [UIStoryboard storyboardWithName:#"Main_iPad" bundle:nil];
HomeViewController *homeViewController = [sb instantiateViewControllerWithIdentifier:#"HomeView"];
[self presentViewController:homeViewController animated:YES completion:nil];
Intention was to just avoid having so many Segues from all the Views connecting to the Home View.
I feel this way of transition is causing the memory issue. Same View is getting added to the stack several times and causing the issue.
I am no expert of iOS, so any help/suggestion will be of great help to me.
It looks like the way you have it you're creating an entirely new ViewController everytime you intend to transition back to the HomeView. This is a very bad idea because everytime you make a transition you're putting a new view controller on the stack, rather than using the original ViewController (which you should be doing).
So as you keep making a transition, you're allocating new memory which eventually causes a memory warning then causes a stack overflow making your app crash.
HomeViewController should be presenting other view controllers using this method presentViewController:animated:completion: and dismissViewControllerAnimated:completion: or something similar in order to perform transitions if you don't want to use segues.
Please read this apple documentation:
https://developer.apple.com/library/ios/featuredarticles/viewcontrollerpgforiphoneos/ModalViewControllers/ModalViewControllers.html
I have a navigation controller, and one of the views in my navigation controller has a date picker. The date picker transition is a little slow so I wanted to preload that view. So to do that in my navigation controller’s viewDidload I instantiate the date picker view with:
datePickerViewController = [self.storyboard instantiateViewControllerWithIdentifier:#“datePickerView"];
[datePickerViewController view]
I have verified that datePickerViewController's viewDidLoad is being called. Then when I want to push the datePickerView:
[self.navigationController pushViewController:datePickerViewController animated:YES];
But this does not improve the transition speed. What's more is that if I push it, go back, then forward again--the transition is fast, which leads me to believe I'm not preloading the view correctly. Any help would be greatly appreciated.
Accessing the view property will only create the view and call loadView and viewDidLoad. It will however not call viewWillAppear, viewDidAppear and it will not "render" the view. But other than calling the view property there is no simple solution preload "more". If its still slow, you should profile your app in Instruments and figure out why its so slow.
I have an application created from the tabbed application template. (ARC, iOS 4)
There are several tabs and there is a button on the 2. tabs viewcontroller.view(ViewCont2).
This button loads another viewcontroller's(ModalViewCont) view by presentModalViewController method.
There is a close button on ModalViewCont which calls dismissModalViewControllerAnimated.
In viewDidDisappear of ViewCont2, i am setting self.view = nil and other outlets to nil to unload the view so it will be fresh loaded next time it appears on screen. I am doing this because it inherits from a base class(BaseViewCont) which initializes some general properties of the view controller and adds some buttons, labels etc. in viewDidLoad method. So, ViewControllers that inherit from this base class may configure those properties differently as they wish in their viewDidLoad method.
Problem
Now, when ModalViewCont on screen, pressing the Home button to put application in background and after getting the application back, closing the ModalViewCont does not bring back the ViewCont2's view but a black screen with the tabbar at the bottom. The same thing happens without putting the application background/foreground; if other tabs tapped before tapping the 2. tab.(EDIT : This happens only if self.view set to nil in viewWillDisappear instead of viewDidDisappear.)
I determined that ViewCont2 loads a new view (checked it's reference) but view's superview is nil so the new view is not displayed but a black screen.
Things that did not work
Using [self.view removeFromSuperview]; before setting self.view=nil,
In viewWillAppear adding view to the parent; [self.parentViewController.view addSubview:self.view]; This one did not work smoothly, view placed slightly up of the screen. This is because there are several other superviews in the hierarchy.
Solutions i considered;
1- If superview is nil in viewDidLoad, it becomes available in viewWillAppear (assumption). So, viewWillAppear method of ViewCont2 could be used to get the superview loaded correctly by the following;
_
if (self.view.superview == nil)
{
self.tabBarController.selectedViewController = nil;
self.tabBarController.selectedViewController = self;
}
2- viewWillAppear method of base class could be used instead for initialization so there is no need to unload the view. So, performance could be optimized, it will not be unloaded each time view disappears. Also, it would be better to perform initialization only once by checking a flag, instead of performing it every time it appears.
Questions
1- Why does not the superview restored? What should i do for it? (This is the main problem i want to understand and solve instead of trying alternatives...)
2- Am i doing something wrong by assigning nil to view for unloading it? If so, how should i unload the view properly in such case like this(tabbed application)?
3- Is anything wrong with the 1. solution? Does it seem like a kludge? Is that assumption about superview and viewWillAppear correct?
EDIT : It seems that when viewDidLoad is called earlier than it should(i.e when view nilled in viewWillDisappear instead of viewDidDisappear), superview is not set.
It seems weird, but your suggestion (1) is indeed a correct workaround for this problem:
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (!self.view.superview) { // check if view has been added to view hierarchy
self.tabBarController.selectedViewController = nil;
self.tabBarController.selectedViewController = self;
}
}
Your second suggestion is good for performance (because view loading is an expensive operation) - but it will not solve the problem. You can also end up with a black screen without setting the view to nil in the following situation (test this in the iOS simulator):
open the modal view
simulate a memory warning -> this will unload the views in the tabbarcontroller
press home button and open the app again
close modal view -> black screen
Generally you can assume that in viewDidLoad the view property is set and in viewWillAppear + viewDidAppear the view has been added to the view hierarchy; so the superview should be there at that time (Here the superview is a private view of the tabbarcontroller of class UIViewControllerWrapperView). However in our case, although the view is reloaded (at the time of app resume), it is not added to the view hierarchy resulting in a black screen. This seems to be a bug in UITabBarController.
The workaround forces the appearance selectors to be performed again. So viewWillAppear will be called again, this time with a superview in place. Also viewDidAppear will be called twice!
Setting self.view to nil is okay, but should not be necessary in most cases. Let the system decide when to unload the view (iOS can unload views when memory gets low). The view controller code should be designed in a way so that the UI can be reconfigured at any time without reloading the view.
You do not have full control over when views are loaded and unloaded, and you are not supposed to load/unload views manually yourself.
Instead, you should think of view loading/unloading as something that's entirely up to your UIViewControllers, with you being responsible only for:
Implementing the actual loading, by associating your UIViewController subclass with a nib file or by implementing loadView manually.
Optionally implementing the viewDidLoad, viewWillUnload and viewDidUnload callbacks, which are called by the view controller when it decides to load/unload its view.
The fact that you have no full control of when the above callbacks will be called, has implications about what should go into them.
In your case, if I understand correctly, whenever your ViewCont2's view disappears, you want to reset it so that when it reappears it will be in some "clean" state. I would implement this state reset in some method, and call it both from viewDidLoad and from viewDidDisappear. Alternatively, you can have the "clean" logic in viewWillAppear.
Or maybe you want to clean ViewCont2's view only when the present button is tapped? In that case, clean the view both in viewDidLoad, and when the button is tapped.
What I offer is that when the modal view controller is active, and you dismiss the view, that you add a new view to the navigation view controllers viewControllers, then that view is told to remove its predecessor.
You can play with my project to see if you think it works for you.
EDIT: my comment on the selected answer is that this technique obviously works now, but I myself am having a hard time followiing it. The code in my project uses the system in a simple and direct fashion - when the modal view is told to dismiss itself, it calls a method (could be in any class) that adds a new view to the navigation controller's array, then dismisses itself. For a bit of time there are two view controllers of the same time, the new one stacked over the old one. When the new view controller appears, based on seeing a flag it silently and behind the scenes removes the undesired viewController from the nab bar's stack, and poof, it goes away.
I have found the actual solution to the UITabBarController bug(memory warning,app enter back/foreground,dismiss modal). Using UITabBarController as the root view controller is the cause of the bug. So, we could use another view controller as the root view controller and present the tab bar from it. I have tested it on iOS 5.1 simulator.
Of course, the overhead of extra UIViewController is subject to debate. Also, it's against the Apple documentation;
Unlike other view controllers, a tab bar interface should never be installed as a child of another view controller.UITabBarController Class Reference
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// A root view controller other than the actual UITabBarController is required.
self.window.rootViewController = [[UIViewController alloc] init];
[self.window makeKeyAndVisible];
self.tabBarController = [[UITabBarController alloc] init];
self.tabBarController.viewControllers = [NSArray arrayWithObjects:viewController1, ..., nil];
[self.window.rootViewController
presentModalViewController:self.tabBarController animated:NO];
}
I have found other solutions;
First one causes the warning: "Application windows are expected to have a root view controller at the end of application launch" although there is root view controller.
Although it seems kludgy, the temporary view controller will be released with the first one.
Second one seems more reasonable.
.
- (void) tabBarBlankScreenFix1
{
self.window.rootViewController = [[UIViewController alloc] init];
[self.window makeKeyAndVisible];
[self.window addSubview:self.tabBarController.view];
self.window.rootViewController = self.tabBarController;
}
- (void) tabBarBlankScreenFix2
{
self.window.rootViewController = [[UIViewController alloc] init];
[self.window makeKeyAndVisible];
[self.window addSubview:self.tabBarController.view];
}
I think you shouldn't assign the view to nil.
If I understand you right you want to refresh/reload content every time the view appears.
So instead of setting the view to nil, you should try to refresh it. You can do it by adding:
- (void)viewWillAppear{
[self.view setNeedsDisplay];}
Please tell me if I understand your issue right
I have a view controller which contains a table view, and which is wrapped within a navigation controller, i.e. in the app delegate these two are created and set as:
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:self.myViewController];
self.window.rootViewController = navController;
If the user clicks on a row within a table then another view controller is created and pushed to the navigation controller's stack:
[self.navigationController pushViewController:webPageController animated:YES];
The webPageController loads and reads local files. If a file is missing I want to abort the loading of the webPageController and the displaying of its view and have the table view displayed.
How should I achieve this?
If the webPageController detects a problem I've tried experimenting with it calling various things such as:
[self.navigationController popViewControllerAnimated:YES];
or
[self.navigationController.navigationBar popNavigationItemAnimated:YES];
To pop itself off the navigation stack, however these aren't working, is it wrong for a navigation controller to attempt to pop itself like this? What is the canonical way of implementing this?
Thanks
That should be fine. Where are you calling popViewControllerAnimated:? If you're calling before viewDidAppear, you'll likely run into problems. ViewControllers need to finish the appearing and disappearing before they can make any kind of pop or push to their stack. If you do it before, you get really weird results. The most common symptom of this is it doesn't work. Often buttons inside it will get messed up as well.