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.
Related
i have an app with 5 views, mostly consisting of drill downs.
lets say i drill down to the 4th view controller. is there a way to present the second view controller exactly as it is without recreating it and modally present that view?
the drill downs do a sort of round about and i dont want to force the user to reselect their selection on the first view to bring them into the second view
so its like this (tvc = tableviewcontroller)
tvc1 > tvc2 > tvc3 > tvc4 > tvc2 > tvc5
^ ^
these two views are the same view in memory
You cannot present tvc2 again while it is already in the "stack" of presented view controllers.
If you push your view controllers onto a navigation controller's stack, then you can change the order of the view controllers in that stack by assigning to its viewControllers property or by sending it setViewControllers:animated:. You can hide its navigation bar if you don't want users to see it. I don't think it's safe to put the same view controller into the stack in two places at the same time.
You can possibly iterate over the visible viewControllers, and use the is casting operator to check if it is that type of class. Then find the view controller and pop to it.
Objective-C
for (id viewController in self.navigationController.viewControllers) {
if ([viewController isKindOfClass:[ViewControllerClass class]]) {
[self.navigationController popToViewController:viewController animated:TRUE];
}
}
Swift 2.0
for viewController in self.navigationController!.viewControllers {
if viewController is ViewControllerClass {
self.navigationController?.popToViewController(viewController, animated: true)
}
}
UINavigationController has an array called viewControllers. This will give you the list of UIViewControllers that exist in that navigation stack.
You can try something like:
UIViewController *yourTableViewController = [self.navigationController.viewControllers objectAtIndex:(theIndexOfYourTableViewController)];
Hope this helps.
I have an embedded container on a view controller, and I would like to change it's content depending on a specific condition. How should I do this, knowing that there can only be one embed segue linked to an embedded container.
I tried to put a view controller between my embedded container and my 2 possible content views, but it won't work because of custom segues (error : "Could not create a segue with class 'null'). I don't understand this error by the way, if someone could tell me more about it :)
I read about some ways to go around this problem by creating a tab view, and switching between the tabs programmatically, or by adding 2 container view and hiding the unwanted one, but these seem to be kind of hacky.
What would be the best practice to do this ? (In swift please)
Thank you for your help
There's two ways to do it, the first is to add two container views on top of each other and set the alpha to 0 for one and 1 for the other and switch the alpha values when you want to change between view controllers.
The disadvantage of this is that there will always be two view controllers instantiated.
The second way is to change the the type of segue from embed to a custom segue (this will allow you to add more than one segue in the storyboard) that loads or swaps a view controller. Here is the implementation of a segue I implemented that does this, if you can understand it you can implement it in swift.
(void)perform
//
// Used to seque between the master view controller and its immediate child view controllers and also from the homw view controller
// and all its immediate child controllers.
// At app launch then it is necessary to directly load a particular view controller - this is determined by checking that the source
// view controller has no children. At other times the seque is used to switch from one controller to another.
//
{
//
// This seque is for use when there is a container view controller where it is necessary to switch the contained view controllers (storyboards only permit one embed seque).
//
//
// embed segue segueA
// MainVC --------------------------> ContainerVC -------------------> VCA
// (has the view containing
// the containded VC)
// sequeB
// --------------------> VCB
//
//
// When the app initially launches the OS will automatically execute the embed seque and thus the ContainerVC gets the opportunity in its viewDidLoad to decide which
// VC to load at that point and then execute either segueA or sequeB. Assuming it calls sequeA then when the seque executes the source will be the ContainerVC and the
// destination with will VCA. The seque checks to see if the containerVC already has any children, if not then it knows to just add VCA as a child.
//
DDLogInfo(#"SEGUE - entered seque");
UIViewController *container = self.sourceViewController;
UIViewController *destination = self.destinationViewController;
if([container.childViewControllers count] == 0)
{
DDLogInfo(#"SEGUE - adding intitial VC: %#", [destination description]);
// The containerVC doesn't yet any any children so just add the destination VC as a child
[container addChildViewController:destination];
destination.view.frame = container.view.frame;
[container.view addSubview:destination.view];
[destination didMoveToParentViewController:container];
}
else
{
// The containerVC already has an existing child and thus it is necessary to swap it out and replace it with the new child
UIViewController* currentChild = container.childViewControllers[0];
currentChild.view.userInteractionEnabled = NO;
PyngmeAssert([container.childViewControllers count] == 1, #"More than one child controller");
// First check to make sure the destination type is not the same as the current child
if([destination isMemberOfClass: [currentChild class]])
{
DDLogInfo(#"SEGUE: Trying to swap view controllers of the same type *****");
}
else
{
// Swap the new VC for the old VC
destination.view.frame = container.view.frame;
[currentChild willMoveToParentViewController:nil];
[container addChildViewController:destination];
[container transitionFromViewController:currentChild
toViewController:destination
duration:0.35
options:UIViewAnimationOptionTransitionCrossDissolve
animations:^{
}
completion:^(BOOL finished) {
[currentChild removeFromParentViewController];
[destination didMoveToParentViewController:container];
DDLogInfo(#"SEGUE finished swapping seque");
}];
}
}
}
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.
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.
I've been converting an application to use storyboards. I'm sure this is a simple problem, but somehow I can't seem to figure out the 'correct' way of doing it, coming as we are from the old XIB world.
One of the subsections of it contains a UITabBarController, each with some subviews within it.
The action that launches this set of tabs works perfectly; I detect the segue, and set some data properties within my (custom) UITabBarController.
Next, I would like to be able to pass that data to the child views when they get created. But - because these tabs are simply 'relationships' and not segue's, I can't do what I do everywhere else, which is override the 'prepareForSegue' function.
In the old XIB universe, I'd simply bind some IBOutlets together between the tab controller and the child views. But I can't do that in storyboards, because the parent and children are separate 'scenes'.
I've tried making my UITabBarController class implement its own delegate, override 'didSelectViewController' and doing 'self.delegate = self' which almost works, except for the fact that it is never called with the first tab when the view is initially shown.
What's the "correct" (or 'best') way to do this? Please don't tell me to get/set some value on the app delegate, as this is 'global variable' territory - nasty.
Try looping through the view controllers on the UITabBarController, e.g. in this example the setData method is called from the segue in to the UITabBarController, and it then loops through the child view controllers, making a similar call on the child controller to set the data on that too;
- (void)setData:(MyDataClass *)newData
{
if (_myData != newData) {
_myData = newData;
// Update the view.
[self configureView];
}
}
- (void) configureView {
for (UIViewController *v in self.viewControllers)
{
if ([v isKindOfClass:[MyDetailViewController class]])
{
MyDetailViewController *myViewController = v;
[myViewController setData:myData];
}
}
}