iOS State Restoration with custom container view controller (MMDrawerController) - iphone

Setup
I have a sliding menu custom container view controller (MMDrawerController) controlling a center view controller and a slide out left menu/drawer. Just like the Facebook app and hundreds of others.
Opening the drawer on the left and tapping a menu item replaces the center view controller.
What's Working
If I open the app, press a button on the first center view controller (which changes the background color), I can kill the app and successfully restore the background color. Perfect.
What's Not Working
If I choose another center vc to load (by opening up the menu/drawer and selecting a menu option) and then kill the app, the app won't restore to that view controller.
What I'm Doing
In my parent view controller I'm encoding the left and center view controllers so I can recreate them on restore.
- (void)encodeRestorableStateWithCoder:(NSCoder *)coder
{
[coder encodeObject:self.centerViewController forKey:#"centerVC"];
[coder encodeObject:self.leftDrawerViewController forKey:#"leftDrawerVC"];
[super encodeRestorableStateWithCoder:coder];
}
- (void)decodeRestorableStateWithCoder:(NSCoder *)coder
{
// if I don't change the center view controller, these values
// get logged out as expected
NSLog(#"leftDrawer: %#", [coder decodeObjectForKey:#"leftDrawerVC"]);
NSLog(#"center: %#", [coder decodeObjectForKey:#"centerVC"]);
[super decodeRestorableStateWithCoder:coder];
}
In the flow where I change the color on the first center view controller, during decoding I can get back the center and left View Controllers successfully. However in the flow where I select a new center vc to load, during decoding those objects are nil.
How do I set up my custom container view controller to properly encode references to it's children in a way that I'm guaranteed to get them back while decoding?
UPDATE 1
Using the restorationArchiveTool I inspected the archive after running through the scenario that doesn't restore properly, and the archive does in fact contain the hierarchy of encoded objects that I'd expect. I still can't figure out though why those previously encoded view controllers end up being null during decoding.
UPDATE 2
If you look at the comments section of this gist, you can see that all the proper encode/decode calls seem to be happening on saving and restoring. I'm wondering if in my app delegate, when i initially set up my root view controller (the instance of mmdrawercontroller) if I'm somehow clobbering the state restoration? Here's what I'm doing:
- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
UINavController *centerNav = [[UINavController alloc] initWithRootViewController:[FeaturedViewController new]];
centerNav.restorationIdentifier = #"centerNav";
UINavigationController *leftDrawerNavController = [[UINavigationController alloc] initWithRootViewController:[LeftDrawerViewController new]];
leftDrawerNavController.restorationIdentifier = #"leftDrawerNav";
MMDrawerController *drawerViewController = [[MMDrawerController alloc] initWithCenterViewController:centerNav leftDrawerViewController:leftDrawerNavController];
// no restoration class, since this will always be created before state restoration resumes, and therefore will be found implicitly
[drawerViewController setRestorationIdentifier:#"mmDrawer"];
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.window.rootViewController = drawerViewController;
self.window.restorationIdentifier = NSStringFromClass([UIWindow class]);
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return true;
}

I have done similar with MMDrawerController and believe the problem is that you are not providing UIKit with a way to create all of the possible center view controllers when it is restoring state. Note that when you encode a controller, all that gets saved is the restoration ID for that controller. To be restored when you next start the app UIKit needs to be able to obtain an instance of the controller -- it won't create one itself. It will try various methods to obtain said instance, as listed here:
http://developer.apple.com/library/ios/#documentation/iphone/conceptual/iphoneosprogrammingguide/StatePreservation/StatePreservation.html#//apple_ref/doc/uid/TP40007072-CH11-SW10
Your app may create the default center controller every time (either via a storyboard or manually) so that one works. For others you probably need to either implement a restoration class or implement application:viewControllerWithRestorationIdentifierPath:coder: in your delegate and have it return a new instance of the right type.

Related

Change XIB View at app start

What is the right way to change XIB View which loaded at app start depending on some app settings. Of course I know how to get all settings I need.
In your application's delegate, in the method
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
if(yourSettings)
myViewController = [[MyViewController alloc] initWithNibName:#"FirstNibName" bundle:nil];
else
myViewController = [[MyViewController alloc] initWithNibName:#"SecondNibName" bundle:nil];
self.window.rootViewController = self.myViewController;
[self.window makeKeyAndVisible];
return YES;
}
And that's it. Probably you may want to save your settings in your user defaults so you can load the view properly.
However, I would use 2 different view controllers, as you probably want them to do different things, not just modify some graphics.
EDIT-
Let me see if I understand. You will always load the first view controller and, if some conditions are met, you modally want to present the second view controller that will get dismissed at some point, returning the user to the first view controller. If this is the case, I suggest you move the code in your first view controller, in viewDidLoad or better yet in viewDidAppear, as this view controller will always get loaded. Also this way the user can see that he will eventually go to that view controller. I use something like this in applications the user needs to login to so that it will be obvious for him that he cannot continue until he does login.
I can't say that this is the right way to do it, because it's up to the programmer how he arranges his code, but it would seem to me that the place that controls what view and how it appears belongs in a view controller and not in the delegate, especially considering that your first view controller always gets loaded. It should be up to that view controller to see if it presents the second one or not.

Difficulty displaying Tab Views in iOS iPhone

I'm new to iOS dev and I have a program that begins by presenting the user a view. This view has two buttons, and depending on which the user clicks, a different tab view will be displayed. The tab view is displayed like this:
betaAppDelegate* delegate = (betaAppDelegate*)[[UIApplication sharedApplication]delegate];
acquireData *ac_view = (acquireData*)[[acquireData alloc] init];
[delegate.window addSubview:ac_view.view];
[self.view removeFromSuperview];
[self dealloc];
The tab view is ac_view.view. When I run the application in the simulator, instead of displaying my tab view with three tabs, it displays a white screen with a thin black bar (empty tab dock) on the bottom. It's encouraging to at least see something be displayed! But I've been trying without success for a while to get it to display my tabs. The .xib file looks correct. It has the three tabs at the bottom, and each of the three tabs say in the interface builder that they're loaded from xxxxxxx, so the linking appears correct...
Thank you!
I'm going to presume you're using the UITabBarController.
You can either set one up by adding the tabs in interface builder and then setting which xibs the individual tabs load up. It sounds like you have done this. After that there is no code you need to write to get the tab bar working to switch between your three view controllers.
You can also set up the TabBarController programatically.
This would be the programmatic way and would go into you application delegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
UITabBarController * aTabBarController = [[UITabBarController alloc] init];
NSArray * array = [[NSArray alloc] initWithObjects:controller1, controller2, controller3, nil];
[aTabBarController setViewControllers:array animated:NO];
[array release];
self.window.rootViewController = aTabBarController;
[self.window makeKeyAndVisible];
[aTabBarController release];
return YES;
}
You would then see a tab bar with three tabs that correspond to controller1, 2 and 3 (your custom view controllers)
To set the icon and text and things its as easy as reading the documentation and seeing
Tab bar items are configured through
their corresponding view controller.
To associate a tab bar item with a
view controller, create a new instance
of the UITabBarItem class, configure
it appropriately for the view
controller, and assign it to the view
controller’s tabBarItem property.
Just a final word of warning in Objective C you should never call dealloc yourself. Dealloc is called by the system when an objects retain count reaches 0. Read into how to retain and release objects to get the hang of how this all works.
Good luck

How can I open a new view from a background thread without knowing what the current/main-view is?

From what I understand any new view that is opened needs to have a parent/super view.
I have a background thread that communicates with a server and according to the server's response it needs to popup an alert and in response to that alert it needs to bring up some UI. This can happen 'anywhere' within my App and so I find it hard to provide this new view with a parent/super view.
Is it possible to open this new view with no knowledge of what's currently being displayed? Is there a non-intrusive way to detect the current view and use that as the parent/super?
One of the targets is not to require anything from the Delegate and other views.
The way to get the currently visible view depends on the setup of the controllers, eg. whether you use a tab bar, navigation controller etc. I think the easiest way would be if your background thread sent a notification (using NSNotificationCenter) on the main thread whenever you need the UI changes to happen. The View Controllers can then subscribe to this notification and handle the UI changes.
Notifications are just one way of communication though, basically what you want is to have your model background thread somehow message the currently active view controller on your main thread. How you exactly go about this will largely depend on your application (see first sentence again).
Actually you don't need a view, you can add it as a subview of your app delegate's window.
In your application delegate:
- (void) showView {
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0,0,320,480)];
view.backgroundColor = [UIColor redColor ];
[self.window addSubview:view];
[view release];
}
Then, when you want to show the view:
MyAppDelegate *d = [[UIApplication sharedApplication] delegate];
[d showView];
You'll need a way to dismiss the view once you are done with it, you can use
[self removeFromSuperview]

Layout screws up when I push several controllers without animation

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:.

How do I have a view controller run updating code when it is brought to the top of the stack of views?

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];