Reputation: 5495
I have tried several of the open-source sliding menu navigations that emulates the navigation menus in the Facebook, Gmail apps (ViewDeck, MFSideMenu and SWRevealViewController), and run into the same issue that whenever I segue into another ViewController based off a menu selection, then back to the original VC, thee VC is always reloaded, and so any subviews that were previously programmatically added all disappear. This is the case even when I just select the same VC from the menu - it will still get loaded. I have tried to keep a strong pointer to the view controller in the App Delegate (by instantiating a property of that view), but this has not solved the issue? Is there another workaround to this? Thanks!
I am currently using SWRevealViewController, and have my Storyboard set up as such.
Update and what worked for me:
Ultimately, everything worked with MFSideMenu and danh's suggestions to keep the controller that I am interested in retaining in the Navigation stack.
Here is my code for the selection for a non-primary view controller
- (IBAction)buttonForNonPrimaryVC:(UIButton *)sender
{
someVC *someVC = [self.storyboard instantiateViewControllerWithIdentifier:@"someIdentifier"];
UINavigationController *navigationController = self.menuContainerViewController.centerViewController;
[navigationController pushViewController:someVC animated:NO];
[self.menuContainerViewController setMenuState:MFSideMenuStateClosed];
}
And for the selection of the primary view controller:
- (IBAction)primaryVCTapped:(UIButton *)sender {
if ([[[self.menuContainerViewController.centerViewController viewControllers] lastObject] isKindOfClass: [primaryVC class]] || [[self.menuContainerViewController.centerViewController viewControllers] count] < 2){
//Do nothing
} else {
[self.menuContainerViewController.centerViewController popViewControllerAnimated:NO];
}
[self.menuContainerViewController setMenuState:MFSideMenuStateClosed];
}
Upvotes: 3
Views: 3479
Reputation: 2431
Specifically for SWRevealViewController, reusing instances of your view controllers is actually very straightforward, and it doesn't require modifying SWRevealViewController.
In your designated MenuViewController (the view controller responsible for invoking segues whenever you wish to show a menu item's view controller), create a view controller cache. This will be used to store view controller instances whenever they are created via segue:
@property (nonatomic, strong) NSMutableDictionary *viewControllerCache;
We'll initialise this later when needed.
Whenever you respond to selecting menu items, instead of invoking a segue directly, call a method instead that checks the cache:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// your logic will vary here, this is just an example
switch(indexPath.row)
{
case 0:
[self showViewControllerForSegueWithIdentifier:@"showSomething" sender:nil];
break;
default:
break;
}
}
This cache checking method could look something like this:
- (void)showViewControllerForSegueWithIdentifier:(NSString *)identifier sender:(id)sender
{
NSString *cacheKey = [identifier stringByAppendingFormat:@":%@", sender];
UIViewController *dvc = [self.viewControllerCache objectForKey:cacheKey];
if(dvc)
{
NSLog(@"reusing view controller from cache");
UINavigationController* navController = (UINavigationController*)self.revealViewController.frontViewController;
[navController setViewControllers: @[dvc] animated: NO ];
[self.revealViewController setFrontViewPosition: FrontViewPositionLeft animated: YES];
}
else
{
NSLog(@"creating view controller from segue");
[self performSegueWithIdentifier:identifier sender:sender];
}
}
In this scenario, I create a key in the cache as a combination of the segue name and the sender parameter. I'm assuming the sender is a string here for the sake of an example. It's quite possible that the sender parameter is not a string, so you should probably do some checking to ensure it doesn't crash.
I then check the view controller cache if any view controller exists. If so, perform the view controller swapping you normally do in prepareForSegue:sender:
(you would have pasted this snippet in there when you first set up your SWRevealViewController). If it doesn't exist, perform the segue as normal (which will create a new instance).
All that's left now is to modify your prepareForSegue:sender:
method to store a reference to the instantiated view controller in the cache:
- (void) prepareForSegue:(UIStoryboardSegue *) segue sender:(id) sender
{
if([segue.identifier isEqualToString:@"showSomething"])
{
// do whatever you wish to the destination view controller here
// ...
}
// this part should be very familiar
if ( [segue isKindOfClass: [SWRevealViewControllerSegue class]] )
{
SWRevealViewControllerSegue *swSegue = (SWRevealViewControllerSegue*) segue;
swSegue.performBlock = ^(SWRevealViewControllerSegue* rvc_segue, UIViewController* svc, UIViewController* dvc) {
// cache the view controller
if(self.viewControllerCache == nil) self.viewControllerCache = [NSMutableDictionary dictionary];
NSString *cacheKey = [segue.identifier stringByAppendingFormat:@":%@", sender];
[self.viewControllerCache setObject:dvc forKey:cacheKey];
// the rest remains as before
UINavigationController* navController = (UINavigationController*)self.revealViewController.frontViewController;
[navController setViewControllers: @[dvc] animated: NO ];
[self.revealViewController setFrontViewPosition: FrontViewPositionLeft animated: YES];
};
}
}
Notice how I'm just appending 2-3 lines to prepareForSegue:sender:
, which shouldn't interfere with your existing setup. It's possible that the segue identifier isn't set, which will result in a crash. You should use identifiers on your segues so you can identify them for caching.
One limitation with this approach is that this only caches view controllers that were invoked with segues from the menu view controller. You'll notice that selecting the menu item for the first visible view controller will cause it to be reloaded from scratch (because it wasn't cached yet). Any time after that, it should work with the cache. I imagine shifting the caching to the SWRevealController that sits before your menu setup in the storyboard will alleviate this. You could do this by subclassing it.
Upvotes: 2
Reputation: 354
alternate solution is adding a delegate in the your side menu controller and just implementing
delegate in the view controller to which the user navigates on click.
Upvotes: 0
Reputation: 62686
I had a client that wanted to use MFSideMenu, and the solution I used was to place a UINavigationVC as the center vc. Instead of segues, the app uses pops and pushes to navigate. This has the desired behavior of maintaining the app's main view controller in the navigation stack while user interacts with the push-ed view controller.
The side menus are table views, so when a table row gets selected, a message is sent to the center vc's root vc and it does basically this:
- (void)sideMenu:(MFSideMenuContainerViewController *)menuVC didSelectItemAtIndex:(NSInteger)index {
// we keep an array of vs classes to instantiate for each menu item
Class *klass = self.menuVCClasses[index];
// client also wanted each vc in it's own nib. I would have preferred storyboard, but...
NSString *nibName = NSStringFromClass(klass);
UIViewController *vc = [[klass alloc] initWithNibName:nibName bundle:[NSBundle mainBundle]];
// or from storyboard
UIViewController *vc = [self.storyboard instantiateViewControllerWithIdentifier:NSStringFromClass(klass)];
// here's the punch line. pop without animation, push with animation
// this will keep the main vc around at the bottom of the stack
[self.navigationController popToRootViewControllerAnimated:NO];
[self.navigationController pushViewController:vc animated:YES];
}
Upvotes: 1
Reputation: 1840
A new instance of a view controller is always created when you segue. Using .xib files instead of storyboards would allow you to reuse your view controllers and to keep the other views alive.
Upvotes: 0