So I have a stack of three UITableViewControllers, each of which displays its view correctly underneath the navigation bar when I tap through the UI manually.
However, now I'm working on restoring state across app restart, and so I'm pushing the same two controllers on top of the root view controller, one at a time, in the same method in the main thread. What ends up happening then is that the middle controller's view is laid out too far down, and the top controller's view is too far up (underneath the nav bar).
Relevant code:
for (int i = 0; i < [controllerState count]-1; i++) {
MyViewController* top = (MyViewController*)navigationController.topViewController;
int key = [[controllerState objectAtIndex:i] integerValue];
[top restoreNextViewController:key]; // this calls pushViewController:
// so top will be different in the next iteration
}
I suspect that the problem is that I'm not allowing some important UI refresh process to take place in between the two pushes, because they happen in the same thread. It almost looks as though the automatic view adjustment that's supposed to affect the top controller affects the middle one instead.
Is that right? And if so, what should I do, since I do want all the state-restoration to take place immediately upon app start?
EDIT: looks like I was unclear. restoreNextViewController: is a MyViewController subclass method that restores the controller's state based on a stored key, and then pushes the appropriate child controller with [self.navigationController pushViewController:foo animated:NO]. I'm doing this because my actual app, unlike this simplified case, has up to 6 controllers in the stack, and they're not always the same ones. So I figured this would be a cleaner design than going down the stack checking controllers' classes. Each controller already knows how to push a child controller in response to user input; why not reuse that on app restart?
I'm not having any trouble getting the controllers to show up; they're just being laid out strangely.
Have you considered having each view controller push its child during viewWillAppear:? I would just set an isRestoringState property in your appDelegate and check that during viewWillAppear:, if it's set, run your restore for that view, including pushing any visible child view. Something like:
- (void)viewWillAppear:(BOOL)animated {
if ([[UIApplication sharedApplication] isRestoringState]) {
// restore local state, set myChildController if a child is visible
if (myChildController) {
[self.navigationController pushViewController:myChildController animated:NO];
}
}
}
You can call pushViewController:animated: multiple times. Please post the code for -restoreNextViewController. It's confusing why this code is as complex as it is. Have you read the section of the View Controller Guide on Creating a Nav Controller? They cover pushing view controllers on the stack at startup.
I agree with Rob, I would look at implementing UINavigationController when your application is loaded. Then you would push each of your controllers onto UINavigtionController in the sequence you want them layered.
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
So if you know what state you want to bring your application back to you could load the appropriate ViewController in the series to be latered and push them onto the NavigationController.
UIViewController *myViewController = [[UIViewController alloc] initWithNibName:#"myView" bundle:nil];
[navigationController pushViewController:myViewController animated:NO];
Then if you want to "pop" back to the UIViewController either the user can press the "Back" button on navigationbar for free. Or you can control the navigation with
- (UIViewController *)popViewControllerAnimated:(BOOL)animated
eg [[self navigationController] popViewControllerAnimated:NO];
You can also control whether you want the NavigationBar to show or not if you want to control the push/pop yourself. I hope I've explained this well enough I've just traveled this road recently myself.
I ended up manually resetting each table view's content inset during its controller's viewWillAppear:.
Related
I'm sure this has been asked countless times, and I've seen similar questions though the answer still eludes me.
I have an application with multiple view controllers and as a good view controller does its own task. However I find myself stuck in that I can't switch from one view controller to another. I've seen many people say "use a navigation controller" but this isn't what I want to use due to the unwanted view elements that are part and parcel to view controller.
I've done the following and have had limited success. The view controller is switched but the view does not load and I get an empty view instead:
- (IBAction)showLogin:(id)sender
{
PPLoginViewController *login = [[PPLoginViewController alloc] initWithNibName:#"PPLoginViewController" bundle:nil];
PPAppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
appDelegate.window.rootViewController = login;
[self.view insertSubview:login.view atIndex:0];
}
Using UINavigationController as a rootViewController is a good tone of creating iOS application.
As i understand unwanted view elements is a navigationBar? You can just hide it manually, setting:
[self.navigationController setNavigationBarHidden:YES];
And about your case, if you want to change you current viewController(targeting iOS 6), you can just present new one:
[self presentViewController:login animated:YES completion:nil];
or add child (Here is nice example to add and remove a child):
[self addChildViewController:login];
Why to set UINavigationController as a root?
1) First of all it makes your application visible viewcontrollers to be well structured. (Especially it is needed on iPhone). You can always get the stack and pop (or move) to any viewController you want.
2) Why I make always make navigation as a root one, because it makes the application more supportable, so to it will cost not so many code changes to add some features to the app.
If you create one (root) viewcontroller with a lot of children, or which presents other viewcontrolls, it will make your code really difficult to support, and make something like gode-object.
Listen to George, UINavigationController is the way to go. Your reasons for not wanting to use it are not valid.
However, the reason your code doesn't work might have to do with the unnecessary line after setting the rootViewController to the login vc.
Per Apple's documentation, setting rootViewController automatically sets the window's view to the view controller's view.
So, I am trying to implement a swipe right to go back to the previous view controller feature.
But when I have the following code in my current view controller, I get a black screen
- (void)initPreviousViewController:(GGMainViewController *)previous
{
self.previousViewController = previous;
self.previousViewController.view.frame = self.currentView.bounds; //self.currentView is declared elsewhere (in the init of the current view controller)
[self addChildViewController:self.previousViewController];
[self.currentView addSubview:self.previousViewController.view];
[self.previousViewController didMoveToParentViewController:self];
{
^I call this method within the previous view controller after initializing the current view controller, and before pushing the current view controller
If I change self.previousViewController = previous; to self.previousViewController = [GGMainViewController alloc] init]; it works. However, I don't want to reinitialize the previous controller.
So how do I set the previous view controller as a property of the current view controller.
Oh, I am also an iOS first timer, so if this is not something that is recommended, please let me know.
Rationale behind my thinking:
Normally the user would just click a button, and the UINavigationController would do the trick, but since I am using a swipe functionality, I think the user needs to see the previous view controller behind the current view controller as they swipe the screen.
As requested
How I call the currentViewController from the previousViewController
- (void)displayCurrentViewController:(id)sender
{
GGNextViewController *next = [[GGNextViewController alloc] init];
[next initializeExploreViewController:self]; // If I comment this out, everything is ok (screen is not black,etc. apart from being able to swipe backwards
[self.navigationController pushViewController:next animated:YES];
}
Your using the self.navigationController for pushing the current view controller, So you can call a method when ever the right swipe gesture fired.
Try this code in your Current view controller(GGNextViewController.m)...
1. Add the rightSwipeGestureAction() selector to the gesture recognizer which you have.
2.
-(void)rightSwipeGestureAction
{
[self.navigationController popViewControllerAnimated:YES];
}
It turns out that the following lines were causing the problem. I just decided to comment them out and then it worked, without a black screen.
[self addChildViewController:self.previousViewController];
[self.previousViewController didMoveToParentViewController:self];
I can't just pop it with the navigation controller, because I want to create somewhat of a dragging effect, the user swipes (as they see the previous view controller below) until halfway through the screen, from where it goes to the previous view controller.
this question is very frequent, but I am not able to solve it with any answers available.
I am working on iOS 5.1. My navigation controller is one tab amongst tab bar view controllers. There's a tableview, in which selecting of a row pushes new view controllers.
This problem occurs Only on selecting of the second row and only sometimes. It's not regular.
The Pushed view comes blank - viewWillAppear/viewDidAppear are not being called. On clicking the back button of the navigation bar - the root view's viewWillAppear/viewDidAppear are also not being called, making it blank.
I am pushing the view on select of first row/second row in exactly the same way. But the problem occurs only on the second row.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
switch (indexPath.row) {
case 0:
AViewController *aObj = [[AViewController alloc] init];
aObj.homeObj = self;
[self.navigationController pushViewController:aObj animated:YES];
[aObj release];
break;
case 1:
BViewController *bVCObj = [[BViewController alloc] init];
bVCObj.homeObj = self;
[self.navigationController pushViewController:bVCObj animated:YES];
[bVCObj release];
break;
default:
break;
}
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
I have tried this and this but in vain.
viewDidLoad is being called on pushing the BViewController, However, viewWillAppear and viewDidAppear is not being called. Following is my viewDidLoad:
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.backgroundColor = [UIColor blackColor];
NSLog(#"nav stack: %#", [self.navigationController viewControllers]);
NSLog(#"nav stack: %#", [[self.navigationController visibleViewController] description]);
//some initialization and call of methods
}
It's not regular. Sometimes I get this scenario, and this continues until I close the app from the background and restart it. But sometimes it works just fine. I am just pushing my view controller to the nab stack.
As I mentioned in the comment, It's a regular navigation controller in tab bar controller.
How are you defining your views for AViewController and BViewController? Generally you'd use initWithNibName, e.g.
AViewController *aObj = [[AViewController alloc] initWithNibName:#"mynibname" bundle:nil]`
As Carl pointed out, you can apparently use just init (though I don't see this documented in the UIViewController Class Reference), but then the system will be very particular about the name of your NIB file. The documentation does say, though, that you can use initWithNibName and pass a nil for the NIB name, in which case it will try to find it for you. Personally, if you're having inconsistent results, though, I'd try using initWithNibName and explicitly pass the name of your NIB, and see if that rectifies the situation.
Or are you building your view programmatically with loadView in your two controllers? Then you need to show us those loadView routines (not to be confused with viewDidLoad).
But according to the documentation, you need to either specify your NIB or use loadView. See the View Management discussion in the UIViewController Class Reference.
Update:
Given your feedback, I have a couple of thoughts:
Needless to say, the problem is apparently not related to the above code. You need to broaden you search and show us more code. Perhaps show us your viewDidLoad of B?
Generally when you don't get these sorts of events, it's because the view controller hierarchy has gotten out of sync with the view hierarchy. The most common way that people do this is if they've done something like "[addSubview someNewController.view]" at some point. If you're using a view controller in any context either than (a) your app delegate's initial configuration; (b) presentViewController (or dismiss); or (c) pushViewController (or pop), then you might want to share what you've done.
As andreamazz pointed out, your comment, "My navigation controller is inside a view controller of the tab bar controller," is a little disturbing if one reads it literally. You can put navigation bar in a view controller's view, but you can't put a navigation controller in a view controller (unless you're doing view controller containment, which is a whole different beast). Equally concerning is where, in another one of your questions, you said, "Embedding a UINavigationController or UITabBarController (my case) in a UIViewController somehow interrupts with the calling of these methods." Thing is, you don't embed nav controllers in other view controllers (unless it is, itself, a container controller such as a tab view controller), but rather its the other way around. But if you literally mean that you have a controller that contains a nav controller, you have to show us how you're doing that (proper view controller containment?) because that's highly unusual.
It's unusual, but I've had projects get corrupted, ending up in weird states. At a minimum, I might suggest "Product" - "Clean" and rebuild. If problem persists, and you've isolated the problem to to B's NIB, then temporarily rename the it and build a quick and dirty one from scratch.
On a project I'm working on, the design decision was to use a UISegmentControl at the top, with a UITabBarController on the bottom. The UISegmentControl has 3 choices for 3 different views. Currently, my coworker has added all 3 views to an NSArray when that particular tab is selected, and then based on the UISegmentControl, the view selected gets unhidden, and the other two are hidden. It seems to not follow Apple's guidelines of lazy loading and seems expensive since 3 viewDidLoads (where queries are made to a database) are getting all loaded at once. There is some lag because of it when the tab is selected for the first time, loading all 3 viewControllers at once.
Is there a better way to do this? I saw a simple example with just two viewControllers, and a button that would switch between the two views. That makes sense to me since you always know what your previous view was, and you can remove that view from the superview, present your new one, release your old one. But with 3 choices, I do not know how to keep track of my view hierarchy (since I could be on segment 0, showing view 0, and then go to segment 2, showing view 2). I am not sure how to check for the last view that was shown, and even if that's the best method. I'm thinking that if there is a better option to keep track of this, but still using the segment control, might as well do it now before the project gets more complex. Thanks!
I would suggest creating a root view controller whose job it is to manage the segment control and load the proper VC depending on which button in the segmented control is selected. The root VC's view would have a subView where the segmented control's VC views are inserted. Something like:
- (void)segmentAction:(id)sender
{
NSParameterAssert([sender isKindOfClass: [UISegmentedControl class]]);
switch ([sender selectedSegmentIndex]) {
case 0:
MYViewController1 *vc = [[MyViewController1 alloc] init];
self.segmentVC = vc;
self.segmentSubvew = vc.view;
[vc release];
break;
}
}
One thing people tend to get hung up on is that there needs to be only 1 VC per screenfull of content -- while that was originally what was recommended by Apple, they have since changed this recommendation. So, loading your segment specific VCs inside the SegmentManagerVC is perfectly acceptable.
You could further tweak this design for performance. For example, you could initially load the VC for the default selected segment and then lazy load the other two so they are already available when a different segment is selected. If you take this approach, though, be sure to hook up -didReceiveMemoryWarning to release the two VCs that aren't currently being viewed.
You could push/pop views onto the UINavigationControler stack. This would also support a "back" button if you wanted it.
[self.navigationController pushViewController:self.myVC animated:YES];
Link a method up to the SegmentedControl that pushes the appropriate ViewController when the corresponding segment is selected. The VC with your segmented control inside of it would need a reference to each segment's corresponding VC. viewDidLoad() will only be called once, and only when the view is pushed onto the navigation stack for the first time.
When you change views or want to go "back", you can pop the VC off the stack:
[self.navigationController popViewControllerAnimated:YES];
Is this the type of functionality which you were looking for?
Edit for Clarity
UIViewController References:
Each view will need a reference to the other two view's ViewControllers. This can be done like this: (assume that we are in "View1", and we also have "View2" and "View3":
View2Controller v2Controller = [[View2Controller alloc] initWithNibName:#"View2" bundle:nil];
View3Controller v2Controller = [[View3Controller alloc] initWithNibName:#"View3" bundle:nil];
The reference to self.navigationController should be declared in your app's delegate as:
UINavigationController* navigationController;
It can be initialized as:
[navigationController initWithRootViewController: rootViewController];
The RootViewController
rootViewController is the UIViewController that corresponds to your application's root view (whatever loads on startup). It is declared in the delegate as:
RootViewController* rootViewController;
And initialized as:
rootViewController = [[RootViewController alloc] initWithNibName:#"RootViewController" bundle:nil];
I have a viewController (Planner) that loads two view controllers (InfoEditor and MonthlyPlan) when the application starts. MonthlyPlan is hidden behind InfoEditor (on load).
So my question is when I exchange InfoEditor for MonthlyPlan (MonthlyPlan gets brought to the top) how can I have data on the MonthlyPlan view be updated. An NSLog in viewDidLoad is being called when the application starts (which makes sense.) NSLogs in viewDidAppear and viewWillAppear aren't doing anything.
Any ideas?
Thanks!
-- Adding more details --
I'm creating the view hierarchy myself. A simple viewController that is just loading two other viewControllers. The two child viewControllers are loaded at the same time (on launch of application.) To exchange the two views I'm using this code:
[self.view exchangeSubviewAtIndex:1 withSubviewAtIndex:0];
The exchanging of the views is fine. The part that is missing is just some way of telling the subview, you're in front, update some properties.
There's a lack of details here. How are you "exchanging" the two views?
If you were using a UINavigationController as the container then viewWillAppear/viewDidAppear would be called whenever you push/pop a new viewController. These calls are made by the UINavigationController itself. If you ARE using a UINavigationController then make sure you have the prototypes correct for these functions.
- (void)viewWillAppear:(BOOL)animated
If you are trying to implement a view hierarchy yourself then you may need to make these calls yourself as part of activating/deactivating the views. From the SDK page of viewWillAppear;
If the view belonging to a view
controller is added to a view
hierarchy directly, the view
controller will not receive this
message. If you insert or add a view
to the view hierarchy, and it has a
view controller, you should send the
associated view controller this
message directly.
Update:
With the new details the problem is clear: This is a situation where you must send the disappear/appear messages yourself as suggested by the SDK. These functions are not called automagically when views are directly inserted/removed/changed, they are used by higher-level code (such as UINavigationController) that provides hierarchy support.
If you think about your example of using exchangeSubView then nothing is disappearing, one view just happens to cover the other wholly or partially depending on their regions and opacity.
I would suggest that if you wish to swap views then you really do remove/add as needed, and manually send the viewWillAppear / viewWillDisappear notifications to their controllers.
E.g.
// your top level view controller
-(void) switchActiveView:(UIViewController*)controller animated:(BOOL)animated
{
UIController* removedController = nil;
// tell the current controller it'll disappear and remove it
if (currentController)
{
[currentController viewWillDisapear:animated];
[currentController.view removeFromSuperView];
removedController = currentController;
}
// tell the new controller it'll appear and add its view
if (controller)
{
[controller viewWillAppear:animated];
[self.view addSubView:controller.view];
currentController = [controller retain];
}
// now tell them they did disappear/appear
[removedController viewDidDisappear: animated];
[currentController viewDidAppear: animated];
[removedController release];
}
I would just add an updataData method to each subview and call it at the same time you bring it to the front. You would need to add a variable to your root view controller to track the active subView:
[self.view exchangeSubviewAtIndex:1 withSubviewAtIndex:0];
if (subView1IsActive) [subView1Controller updateData];
else [subView2Controller updateData];