How to save/restore view state in iOS? - ios

I'm using the ECSlidingViewController to create a slide out menu (like Facebook).
I have this storyboard:
As you can see, I have a navigation controller. I have a major problem though, that even the official demo created by the user who made that controller didn't implement: it doesn't save the state of the view controller when changing views and then coming back.
So, for example, the orange view will always be the first view when I open the app, it gets viewDidLoad. Then I switch to my green view (the second one), and click the button. It changes the background color of that view to red. Then if I go back to my first view, and then back to the second one, the background color of the latter is green again. I want it to stay red.
This is my code to switch views (in MenuViewController):
(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// Get identifier from selected
NSString *identifier = [NSString stringWithFormat:#"%#", [self.menu objectAtIndex:indexPath.row]];
// Add the selected view to the top view
UIViewController *newTopVC = [self.storyboard instantiateViewControllerWithIdentifier:identifier];
// Present it
[self.slidingViewController anchorTopViewOffScreenTo:ECRight animations:nil onComplete:^{
CGRect frame = self.slidingViewController.topViewController.view.frame;
self.slidingViewController.topViewController = newTopVC;
self.slidingViewController.topViewController.view.frame = frame;
[self.slidingViewController resetTopView];
}];
}
As you can see, it's always instantiating a new VC every time. I want it to save the VC, create the new one if it's not created, then show that one. If the user goes back to a view that has already been created, it should just restore the saved view, not create a new one.
I have put the Init View Controller in a Navigation Controller, now how can I implement this save/restore mechanism for my views? I'd like it to work with 2,3,4, etc....as many views as possible.
Thanks.

When you go "back" you essentially pop the viewcontroller from the navigationstack. At that point there are no more references to that viewcontroller and it get's deallocated and as a result you loose all your changes.
You can handle this a couple of ways:
1) Keep a reference to red/green viewcontroller alive in the parent viewcontroller (the one that is presenting) and use that instead of instantiating a new one. This is not very memory friendly but can be used if used sparingly.
in the interface put:
#property (nonatomic, strong) UIViewController* myGreenController;
then change instantiation to
if (!self.myGreenController)
{
self.myGreenController = [self.storyboard instantiateViewControllerWithIdentifier:identifier];
}
...
self.slidingViewController.topViewController = self.myGreenController;
2) Ideally implement a delegate pattern to pass state back to the parent viewcontroller (something like How do I set up a simple delegate to communicate between two view controllers?). Then next time when you need the viewController you set the state before presenting it.

Related

Amending UINavigationController viewControllers stack before animation

I have an app where you can customize products to varying degrees. In some cases the options are split to two views, while in some other cases the first step isn't necessary.
What I would like is to treat all products the same and push the first customization step view controller to the navigation controller stack, let that view controller decide whether or not this step is necessary. If it is not necessary I want it to apply some default options to the product and immediately skip (before the transition animation) to step 2 while not allowing the user to back up to the first step.
The normal UINavigationController.viewControllers stack may look like this when at step 2:
[ListView (root)] -> [CustomizeStep1] -> [CustomizeStep2]
But I want it to apply the default values to the product and amend the view controller stack so that:
[ListView (root)] -> [CustomizeStep1]
----- becomes -----
[ListView (root)] -> [CustomizeStep2]
What I've tried is to use code like this in the CustomizeStep1 view controller:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (shouldSkipToStep2) {
UINavigationController *navController = self.navigationController;
// Move directly to step 2
UIStoryboard *storyboardLoader = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
UIViewController *customizeStep2VC = [storyboardLoader instantiateViewControllerWithIdentifier:#"customizeStep2"];
// Replace current view contoller
NSMutableArray *viewHierarchy = [NSMutableArray arrayWithArray:navController.viewControllers];
[viewHierarchy removeObject:self];
[viewHierarchy addObject:customizeVC];
// Apply new viewController stack
[navController setViewControllers:viewHierarchy animated:NO];
}
}
If I take a look at the navigation controller's viewControllers array after this has been set, everything looks as expected.
What happens in iOS 7
When doing this, the entire functionality of the UINavigationController breaks. The CustomizeStep1 view controller still animates in but is nonfunctional. Tapping the back button still shows CustomizeStep1. Trying to interact with the view controller crashes the app. (It works as expected if the view controller is displayed without the sliding transition, though.)
What happens in iOS 8
The CustomizeStep1 view controller still animates in, but immediately after the transition ends it snaps over to show CustomizeStep2. Other than that it works as intended.
So, my question is if there is a better place to add the code to amend the view controller stack on the navigation controller?
I obviously need to wait until the view controller has been added to the navigation controller, otherwise I can't replace the view controller in the stack. However, I need to be able to cancel the transition animation so that I can animate in CustomizeStep2 instead.
I appreciate if this is impossible, just wanted to check if anyone knows a good way around this.
Edit:
How I would like it to ideally appear to the user
Instead of viewWillAppear:, use viewDidAppear: which is called after the animation finishes.
You could have a boolean on your view controller denoting whether it is filled in or not:
#interface ViewControllerOne : UIViewController
#property (nonatomic, assign, getter = isInitiallyFilledIn) BOOL initiallyFilledIn;
#end
Then, when it is initially filled in, just denote this boolean value.
ViewControllerOne *viewController = [[ViewControllerOne alloc] init];
[viewController setInitiallyFilledIn:YES];
Now, in viewDidAppear:, check this boolean value and check whether that method has been launched before. If it hasn't been launched before (to allow editing) and it is initially filled in, push the next controller!
#interface ViewControllerOne
#property (nonatomic, assign) BOOL hasCheckedFillInStatusBefore;
#end
#implementation ViewControllerOne
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if ([self isInitiallyFilledIn] && ![self hasCheckedFillInStatusBefore]) {
// push the next view controller
}
[self setHasCheckedFillInStatusBefore:YES];
}
#end
Alternatively, if you want to display the two view controllers at the same time, you could alter the navigation stack:
// create instances of ViewControllerOne and ViewControllerTwo
NSMutableArray *viewControllers = [[[self navigationController] viewControllers] mutableCopy];
[viewControllers addObjectsFromArray:#[viewControllerOne, viewControllerTwo]];
[[self navigationController] setViewControllers:viewControllers animated:YES];
Note, the ViewControllerOne will not have viewDidLoad called so if you do any setup in that method (such as a back button title or the view controller title), you will either have to manually invoke that method before setting the view controllers or move that setup to the initializer.

Using one instance of a viewController class in Storyboard

I am writing an iOS7 app in Xcode 5 with storyboards. In a part of the app, I need three screens that shared the same viewController class. These screens are UIViewControllers. I use a UISegmentControl to go from screen to screen based on conditions. I disabled the control if the user had not completed certain steps.
I used a BOOL value check if certain steps had been completed and set its value to YES / NO.
The problem is when I want to go back to the last screen - I am getting a new instance of my viewController class. This has two problems:
Memory grows each time the user goes between the two views
BOOL value and all other properties are nil when new instance loads.
In my segment control this is how I get to the views:
-(void)segmentcontrol:(UISegmentedControl *)segment
{
if (segment.selectedSegmentIndex == 0)
{
self.viewController = [self.storyboard instantiateViewControllerWithIdentifier:#"stepOne"];
[self presentViewController:self.viewController animated:NO completion:nil];
}
else if (segment.selectedSegmentIndex == 1 ){
self.viewController = [self.storyboard instantiateViewControllerWithIdentifier:#"stepTwo"];
[self presentViewController:self.viewController animated:NO completion:nil];
}else {
}
}
This viewController is a subclass of my BaseViewController - which I used for UI elements that is constant across all screens.
What I want to do is return the same instance of the viewController class when I change the segment control to a another view, using the same class.
Is this at all possible?
Not clear why you're using presentViewController:animated:completion: but it looks like you're doing things in the wrong way.
What you want to be doing is creating a container controller. So, the view controller which hosts the segmented control creates a number of view controller instances and adds them as child view controllers. Now, when the segments are selected you get the child at the selected index, remove the old view controllers view from its superview and add the new view controllers view as a subview.
You don't need to do it like that, but it will probably be cleanest. Your memory grows currently because you use instantiateViewControllerWithIdentifier:. All you really need to do is to keep an array of view controllers and reuse instead of recreating. That said, continually presenting view controllers is not wise.

iOS make popup with table view [duplicate]

I need to pop up a quick dialog for the user to select one option in a UITableView from a list of roughly 2-5 items. Dialog will be modal and only take up about 1/2 of screen. I go back and forth between how to handle this. Should I subclass UIView and make it a UITableViewDelegate & DataSource?
I'd also prefer to lay out this view in IB. So to display I'd do something like this from my view controller (assume I have a property in my view controller for DialogView *myDialog;)
NSArray* nibViews = [[NSBundle mainBundle] loadNibNamed:#"DialogView" owner:myDialog options:nil];
myDialog = [nibViews objectAtIndex:0];
[self.view addSubview:myDialog];
problem is i'm trying to pass owner:myDialog which is nil as it hasn't been instantiated...i could pass owner:self but that would make my view controller the File's Owner and that's not how that dialog view is wired in IB.
So that leads me to think this dialog wants to be another full blown UIViewController... But, from all I've read you should only have ONE UIViewController per screen so this confuses me because I could benefit from viewDidLoad, etc. that come along with view controllers...
Can someone please straighten this out for me?
There is no such thing as a view controller being on the screen; its view is on the screen. With that said, you can present as many views as you want on the screen at once.
I would create a new view and view controller. You would not make a UIView be a UITableViewDelegate, you make a UIViewController be a UITableViewDelegate. But instead of doing that manually, instead make your new view controller a subclass of UITableViewController, if you're using iPhone OS 3.x+. You can then present this view controller modally.
You probably want to give the user a chance to cancel out of the selection. A good way to do that is to wrap your new dialog view controller in a UINavigationController and then put a "Cancel" button in the nav bar. Then use the delegate pattern to inform the parent view controller that the user has made their choice so you can pop the stack.
Here's what the code will look like inside your parent view controller, when you want to present this option dialog:
- (void)showOptionView
{
OptionViewController* optionViewController = [[OptionViewController alloc] initWithNibName:#"OptionView" bundle:nil];
optionViewController.delegate = self;
UINavigationController* navController = [[UINavigationController alloc] initWithRootViewController:optionViewController];
[self.navigationController presentModalViewController:navController animated:YES];
[navController release];
[optionViewController release];
}
Your OptionViewController .h will look like this:
#protocol OptionViewControllerDelegate;
#interface OptionViewController : UITableViewController
{
id<OptionViewControllerDelegate> delegate;
}
#property (nonatomic, assign) id<OptionViewControllerDelegate> delegate;
#end
#protocol OptionViewControllerDelegate <NSObject>
- (void)OptionViewController:(OptionViewController*)OptionViewController didFinishWithSelection:(NSString*)selection;
// or maybe
- (void)OptionViewController:(OptionViewController*)OptionViewController didFinishWithSelection:(NSUInteger)selection;
// etc.
#end
Your OptionViewController.m will have something like this:
- (void)madeSelection:(NSUInteger)selection
{
[delegate OptionViewController:self didFinishWithSelection:selection];
}
Which has a matching method back in your original view controller like:
- (void)OptionViewController:(OptionViewController*)OptionViewController didFinishWithSelection:(NSUInteger)selection
{
// Do something with selection here
[self.navigationController dismissModalViewControllerAnimated:YES];
}
There are plenty of examples throughout Apple's sample source code that follow this general pattern.

instantiateViewControllerWithIdentifier and pass data

I am using Storyboard in my app and I want to pass data from one view to another view.
Instead of using segues I am using instantiateViewControllerWithIdentifier. In this case I am instantiate from my first TableViewController to a NavigationController which has a second TableViewController attached because I need the navigation in the second TableViewController. Now I want to pass data from my first TableviewController, depending which row was clicked, to my second TableviewController. In this case newTopViewController would be my NavigationController but my problem is now how to pass data from firstTableViewController to the secondTableviewController.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *identifier = [NSString stringWithFormat:#"%#Top", [menuArray objectAtIndex:indexPath.row]];
UIViewController *newTopViewController = [self.storyboard instantiateViewControllerWithIdentifier:identifier];
}
If you instantiate a navigationController, you can use the viewControllers property to get the inner viewController of the navigation controller.
Something like this:
UINavigationController *navigationController = [self.storyboard instantiateViewControllerWithIdentifier:identifier];
MBFancyViewController *viewController = navigationController.viewControllers[0];
// setup "inner" view controller
viewController.foo = bar;
[self presentViewController:navigationController animated:YES completion:nil];
newTopViewController.anyVariableToShow= anyVariableToSend;
I do this pretty often on a few of my apps...
//Create new VC
CookViewController *detailViewController = [self.storyboard instantiateViewControllerWithIdentifier:#"CookVC"];
//Set recipe
[detailViewController setRecipe:recipe];
//Pop over VC (can be pushed with a nav controller)
[self presentPopupViewController:detailViewController animationType:MJPopupViewAnimationFade];
If you aren't using a navigation controller or segues, then I think you need to reconsider your app design.
Actually it's not just a data pass problem as this is a program control and data transfer question together.
Even you would have to rethink about your app's concept, as you'd like to use storyboard without the meaning of storyboard, it's up to you and I hope you have good reason to do what you do.
So when you decided not to use segue you lost the new and comfortable way of instantiating a new controller and transferring data with it and you have to do the transfer of control and the data in two distinct steps. When you instantiate another scene in storyboard (like you do with instantiateViewControllerWithIdentifier:) you just instantiated a new controller and transferred the control but not the data. Just think about it as you instantiated a new controller from a xib in an old way (so you have to use initWithCoder: or awakeFromNib in the second view controller as the storyboard will not call initWithName:bundle:), but did not do anything more.
So you will have a new controller (it named in the identity part of the second storyboard) which is hanging in the universe without any relationship or connection with anything else (as the storyboard picture illustrates it nicely) and you could do with it what you'd like.
So you'd like to do something and you need data from the previous storyboard (ViewController). What you need is making available those data to the second storyboard(ViewController), and as you know there are lot of solution for this which were available long time before even storyboard is existed.
So regarding your code, the "data transfer" is depending on your design, whether the two controllers are subclasses of each other or whatsoever...
If you don't like to deal with subclassing and like to decoupling them as much as possible, the best way just make a property of your data in the first controller and refer to them from the second (after importing the first's .h file) and just refer to it in it's viewDidLoad or in initWithCoder: or anywhere where you need them, as
secondViewControllerdata = firstViewControllerdata.thatDataProperty
Of course you can do the same in reverse and make a property of the second controller and refer to it in your first view controller.
You can define some parameter in UIViewController to receive data:
#property (assign) int param1;
#property (retain) NSMutableArray *param2;
and use below to pass the data:
[newTopViewController setParam1:XX];
[newTopViewController setParam2:XX];

My viewController doesn't show up right

in my iPad-app I am trying to present one of my views with a modal formsheet-style.
Here's some code:
-(void)present
{
SecondViewController *modal = [[SecondViewController alloc]init];
modal.modalPresentationStyle = UIModalPresentationStyleFormSheet;
[self presentModalViewController:modal animated:YES];
}
I am using Storyboard, and I have put stuff like a textView and toolbars in the view I'd like to show. I have set the right class in Identity Inspector, and in the class-files I have checked that it's the right view appearing with putting NSLog(#"Right view");
When calling the void present, a view is appearing, but only as a dark-white square. Nothing og my content from Storyboard is in it, I even tried changing the background color of the view and the textView to see if something was just outside the square, but the whole thing stayed white. It feels like it's not using the view I created in storyboard, but I have set it to the correct class, and the NSLog gets printed out when calling it. I have not connected the two views in any way in Storyboard, the SecondViewController is just floating around, so that might be the problem? The button that calls for -(void)present is created programmatically, so I can't ctrl+drag it to the button either.
Why is it showing an empty version of my class?
In the "Identity Inspector" set a "Storyboard ID" for your ViewController, and then present it like this:
-(void)present
{
SecondViewController *modal = [self.storyboard instantiateViewControllerWithIdentifier:#"myStoryboardID"];
modal.modalPresentationStyle = UIModalPresentationStyleFormSheet;
[self presentModalViewController:modal animated:YES];
}
And if you're using iOS6, presentModalViewController:animated: is deprecated, so use this:
-(void)present
{
SecondViewController *modal = [self.storyboard instantiateViewControllerWithIdentifier:#"myStoryboardID"];
modal.modalPresentationStyle = UIModalPresentationStyleFormSheet;
[self presentViewController:modal animated:YES completion:nil];
}
Your problem is that you're assuming the program will intrinsically know where to find the, already laid out, view for this controller when that's simply not how storyboards work. The code you list about will create a view controller, but without an associated view it will simply show as a black square.
There's a few ways to solve your dilemma:
Add the modal transition as a segue in the view controller, this would be the simplest way and is what iOS storyboards expect you to do.
Move the view from the storyboard to an external .xib and call the initWithNibName:bundle: method to load this as your view controller's view. This is the best solution if you just want to programmatically load the view.
Load the view from your storyboard programmatically with the instantiateViewControllerWithIdentifier: method, this is probably a bad idea as it goes against the design of storyboards.
I can elaborate on those if you want.

Resources