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");
}];
}
}
}
Related
I am designing an iOS app with 10 main UIViewControllers in it. Each representing a different section of the app. Its basically for a company and shows info about the company.
One of the things I am doing on bottom section of the app (in all the different view controllers) is displaying a UIView which contains a map. This map shows a certain location.
Now it works, but the problem I have is that I have 10 copies of the same code and 10 copies of the same UIView.
Is there anyway I could make a small view controller with one class attached to it that would handle the map and then just create an instance of the view controller in all my 10 view controllers in my app?
I hope my question makes sense. Basically I want to know how I can go about reusing ONE UIView in all 10 of my ViewControllers. So I can just call it or something and it appears.
Update - this is basically what I am trying to achieve
Thanks, Dan.
View controllers can contain other view controllers. You can either use a container view in a storyboard or setup the relationship programmatically (see: Creating Custom Container View Controllers).
The storyboard container view is easiest, but the programmatic solution isn't too bad.
- (void)displayContentController:(UIViewController *)content
{
[self addChildViewController:content];
content.view.frame = [self frameForContentController];
// NOTE: You could also add it to any subview of self.view.
[self.view addSubview:content.view];
[content didMoveToParentViewController:self];
}
- (CGRect)frameForContentController
{
return CGRectMake(…);
}
- (void)viewDidLoad
{
…
MyMapViewController *mapViewController = …;
[self displayContentController:mapViewController];
…
}
- (void)dismissContentController:(UIViewController *)content
{
[content willMoveToParentViewController:nil];
[content.view removeFromSuperview];
[content removeFromParentViewController];
}
Final Note: Have each parent view create its own instance of the map view controller. Resist the temptation to reuse an instance of the map view controller between parents.
Update to address questions
So lets say I had 2 of the same view controllers open at once and they both were displaying the same imported viewcontroller then it wouldn't work right?
You can't do this. An instance of a view controller can only have 1 parent view controller. Create separate instances for each use.
So if I create different instances, I can reuse the same view lets say 5 times in one view?
Yes, if you create different instances, you can put as many as you need on a view.
Let me be clear, an instance is a distinct memory location created using a constructor.
MyMapViewController *mapViewController1 = [[MyMapViewController alloc] initWithNibName:#"MyMapViewController" bundle:nil];
MyMapViewController *mapViewController2 = [[MyMapViewController alloc] initWithNibName:#"MyMapViewController" bundle:nil];
or
MyMapViewController *mapViewController1 = [self.storyboard instantiateViewControllerWithIdentifier:#"MapViewController"];
MyMapViewController *mapViewController2 = [self.storyboard instantiateViewControllerWithIdentifier:#"MapViewController"];
Updated to demonstrate dismissing a container view controller.
Here is a method for a child view controller, so it can use to dismiss itself.
- (void)dismissFromParentViewController
{
[self willMoveToParentViewController:nil];
[self.view removeFromSuperview];
[self removeFromParentViewController];
}
Please try below method:
Create "map controller" super class with inherit to UIViewController and define your need common method and variables.
Inherit your 10 child class into "map controller" super class. And connect common IBOutlets and IBActions to super class.
You can access common methods and variable to super class from child class(10 view controller child class).
Please refer below code
#interface mapController : UIViewController
{
NSString *mapControllerVariables;
}
-(IBAction)mapControllerActions:(id)sender;
#end
#interface yourChileView : mapController
{
}
#end
I'm experiencing a memory leak (the UINavigationController and its root View Controller are both being leaked) when presenting and dismissing a UINavigationController in a subview. My method of presentation of the navigation controller seems a bit non-standard, so I was hoping someone in the SO community might be able to help.
1. Presentation
The Navigation Controller is presented as follows:
-(void) presentSubNavigationControllerWithRootViewControllerIdentifier:(NSString *)rootViewControllerIdentifier inStoryboardWithName:(NSString *)storyboardName {
// grab the root view controller from a storyboard
UIStoryboard * storyboard = [UIStoryboard storyboardWithName:storyboardName bundle:nil];
UIViewController * rootViewController = [storyboard instantiateViewControllerWithIdentifier:rootViewControllerIdentifier];
// instantiate the navigation controller
UINavigationController * nc = [[UINavigationController alloc] initWithRootViewController:rootViewController];
// perform some layout configuration that should be inconsequential to memory management (right?)
[nc setNavigationBarHidden:YES];
[nc setEdgesForExtendedLayout:UIRectEdgeLeft | UIRectEdgeRight | UIRectEdgeBottom];
nc.view.frame = _navControllerParentView.bounds;
// install the navigation controller (_navControllerParentView is a persisted IBOutlet)
[_navControllerParentView addSubview:nc.view];
// strong reference for easy access
[self setSubNavigationController:nc];
}
At this point, my expectation is that the only "owner" of the navigation controller is the parent view controller (in this case, self). However, when dismissing the navigation controller as shown below, it is not deallocated (and as a result its rootViewController is also leaked, and so on down the ownership tree).
2. Dismissal
Dismissal is pretty simple, but it seems not to be sufficient for proper memory management:
-(void) dismissSubNavigationController {
// prevent an orphan view from remaining in the view hierarchy
[_subNavigationController.view removeFromSuperview];
// release our reference to the navigation controller
[self setSubNavigationController:nil];
}
Surely something else is "retaining" the navigation controller as it is not deallocated. I don't think it could possibly be the root view controller retaining it, could it?
Some research has suggested that retainCount is meaningless, but FWIW I've determined that it remains at 2 after dismissal, where I would expect it to be zero.
Is there an entirely different / better method of presenting the subNavigationController? Maybe defining the navigation controller in the storyboard would have greater benefit than simply eliminating the need for a few lines of code?
It is best practice when adding a controller's view as a subview of another controller's view, that you make that added view's controller a child view controller; that is, the controller whose view your adding it to, should implement the custom container controller api. An easy way to set this up is to use a container view in the storyboard which gives you an embedded controller automatically (you can select that controller and, in the edit menu, choose embed in Navigation controller to get the UI you're trying to make). Normally, this embedded view controller would be added right after the parent controller's view is loaded, but you can suppress that by implementing shouldPerformSegueWithIdentifier:sender:. I created a simple test app with this storyboard,
The code in ViewController to suppress the initial presentation, and the button methods to subsequently present and dismiss it is below,
#implementation ViewController
-(BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender {
if ([identifier isEqualToString:#"Embed"]) { // The embed segue in IB was given this identifier. This method is not called when calling performSegueWithIdentifier:sender: in code (as in the button method below)
return NO;
}else{
return YES;
}
}
- (IBAction)showEmbed:(UIButton *)sender {
[self performSegueWithIdentifier:#"Embed" sender:self];
}
- (IBAction)dismissEmbed:(UIButton *)sender {
[[self.childViewControllers.firstObject view] removeFromSuperview];
[self.childViewControllers.firstObject willMoveToParentViewController:nil];
[self.childViewControllers.firstObject removeFromParentViewController];
}
#end
The navigation controller and any of its child view controllers are properly deallocated when the Dismiss button is touched.
The navigationController property on a UIViewController is retain/strong, which is presumably the other strong reference.
So try popping all view controllers from the navigation controller and see if that works.
Is it possible to detect if a UIViewController is inside a container view, compared to for instance being displayed modally, being inside a UINavigationViewController and so on?
Edit: to clarify the reason for this question: I have a VC that sometimes is displayed as a Form Sheet, other times as a child VC inside another VC (in a Container View). I want to be able to check how the VC is actually displayed (Form Sheet or in Container View).
parentViewController property is set only if you are inside a container view.
See -->
https://developer.apple.com/library/ios/documentation/uikit/reference/UIViewController_Class/Reference/Reference.html#//apple_ref/occ/instp/UIViewController/parentViewController
EDIT:
as to check the type do something like this.
UIViewController * parentController = self.parentViewController;
if (parentController != nil && [parentController isKindOfClass:[UINavigationController class]])
{
// code
}
In Swift3, using
if let parentVC = self.parent{ //no embeded
if parentVC is UINavigationController //no embedded{
...
} else {//embeded
...
}
} else {//presented
...
}
express showing current view controller from navigation bar; otherwise
embedded by parent view controller(for example, ViewController with one view embedding with one UITableViewController).
if self.parent == nil, it is presented. Hope for helps.
I wrote a little snippet that shows all the subviews of a view, so if you pass it a top-level view, you can see your entire subview tree. Pass #" " to Indent to make the subtrees indent a bit, then copy it from the debugger console and paste it into a text editor like Bbedit.
- (void) viewAllSubviews:(UIView *) topView Indent:(NSString *) indent {
for (UIView * theView in [topView subviews]){
NSLog(#"%#%#", indent, theView);
if ([theView subviews] != nil)
[self viewAllSubviews:theView Indent: [NSString stringWithFormat:#"%# ",indent]];
}
}
You can use something like this to check for your container view.
UIViewController has a property navigationController, and a property tabBarController. See UIVIewController reference
if(self.navigationController) {
//you are inside a navigation controller
}
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.
Before the Main View Controller is called, through a delegate I'll notify Main View Controller whether to call TVC1 or TVC2 in Container View.
Question: How can I programmatically tell the Container View in Main View Controller to call TVC1 or TVC2?
TVC1 *tvc1 = [self.storyboard instantiateViewControllerWithIdentifier:#"TVC1"];
TVC2 *tvc2 = [self.storyboard instantiateViewControllerWithIdentifier:#"TVC2"];
Where and how do I tell the Container View to load one of these controllers upon loading Main View Controller?
through delegate u can know which view will be added so u can manage it by using any bool value n in viewDidLoad you can load that view by putting one condition
in mainController:
bool isFirstView;
-(void)delegate:(bool)isFirst
{
isFirstView = isFirst;
}
// in viewDidLoad
)
(void)viewDidLoad
{
if(isFirstView)
{
TVC1 *tvc1 = [self.storyboard instantiateViewControllerWithIdentifier:#"TVC1"];
}
else
{
TVC2 *tvc2 = [self.storyboard instantiateViewControllerWithIdentifier:#"TVC2"];
}
}
You can do this by using custom Container View Controller which will manage child view controllers.
There is a good tutorial for that:
http://sandmoose.com/post/35714028270/storyboards-with-custom-container-view-controllers