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
}
Related
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");
}];
}
}
}
Is there a way to summon a callout box on a button next to a textfield or inside the textfield?
Or is UIAlertView my only choice?
Edit: With my successful attempt at creating a UIPopoverController, is there a method to position the arrow so that it does not appear in the center?
I have successfully achieved my goal of creating a callout box as it appears in the image of my question. I'd like to thank Paul Cezanne for mentioning a solution for me to pursue. I guess the name (pop over/callout) is different if it is not displayed in a map view. Anyways, for future apple developers, here are the steps I took that answered my question:
Embed your first view controller with a navigation controller
Keep auto layout and size classes checked in the File Inspector tab on the right hand side in your main storyboard, because the pop over will just act as a push view if we shrink the view controller.
Add a second view controller (you do not need to create a header nor an implementation file)
Drag a button onto the first view controller (you do not need to create an IBAction)
Hold the control key and click and drag from your button on the first view controller to your second controller and the segue choices should appear
Select 'Present As Popover'
Don't forget to create an identifier for the segue
Now heading over to coding, here is the header file of the view controller:
#import <UIKit/UIKit.h>
#interface ViewController : UIViewController <UIPopoverPresentationControllerDelegate>
#end
Here is the implementation file of the view controller:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// Declare a string to identify the segue
NSString *identifier = segue.identifier;
// If the string matches the string inside #""
if ([identifier isEqualToString:#"popOverSegue"]) {
// Assign the view controller to a pointer
UIViewController *dvc = segue.destinationViewController;
// Identify that the view controller is to be a pop over presentation controller
UIPopoverPresentationController *ppc = dvc.popoverPresentationController;
// Set the size of the view controller
// Width = 200
// Height = 200
dvc.preferredContentSize = CGSizeMake(200, 200);
// Have the pop over presentation controller point upwards
ppc.permittedArrowDirections = UIPopoverArrowDirectionUp;
if (ppc) {
ppc.delegate = self;
}
}
}
- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller {
return UIModalPresentationNone;
}
Build and Run and you should have a successful pop over controller app. Fin.
I'm in a view controller that was pushed onto the stack by by a navigation controller.
I have this code that runs when the back button is pressed:
- (void)didMoveToParentViewController:(UIViewController *)parent {
if (![parent isEqual:self.parentViewController]) {
//NSLog(#"Back pressed");
MyCustomViewController *vc = [self.storyboard instantiateViewControllerWithIdentifier:#"TableVC"];
vc.index = [_itemChooser selectedRowInComponent:0];
}
}
I'm trying to set index to the same index of what I chose in the picker view. If I NSLog the value here, it is correct, but if I NSLog the value in viewWillAppear in the parent, it is 0.
(It prints as Null if I'm using an object rather than an NSUInteger)
Pushed it on like this:
[self.navigationController pushViewController:pickervc animated:YES];
I solved my problem by defining a view controller property in the child class, and assigning self to it before pushing. I was then able to set/access properties from the parent in the child no problem.
Okay, so in the process of developing my newest app, I found that my storyboard got huge, so in an effort to clean it up some, i have divided it into multiple storyboards before it gets out of hand. just for settings alone i have roughly 20 tableviewcontrollers that branch out from a root NavigationController. That navigationcontroller was a TabItem on a TabBarController, which is the application's root view controller.
I've moved the TabBar into it's own StoryBoard as the Root_Storyboard and the Navigation controller is now the initial view of the Settings_Storyboard.
Just for testing purposes, I placed a few UIViewControllers as tab items in the TabBarController (Root_Storyboard) and subclassed one and added the following code to it's viewWillAppear method. It works great, but I know that the presentViewController displays the NavigationController modally and hides the tabBar. Obviously I don't want that, how do I get it to push properly so that the TabBar remains visible?
- (void) viewWillAppear:(BOOL)animated {
UIStoryboard *settingsStoryboard = [UIStoryboard storyboardWithName:#"Settings_iPhone" bundle:nil];
UIViewController *rootSettingsView = [settingsStoryboard instantiateInitialViewController];
[self.tabBarController presentViewController:rootSettingsView animated:NO completion:NULL];
}
Edit - To clarify. The above code is the subclassed method for a UIViewController (child of UITabBarController:index(1)) in the Root_iPhone.storyboard. The UINavigationController/UITableViewController that I am trying to load is found in Settings_iPhone.storyboard. Not sure how to implement the linkView suggested below in this situation.
This is quite possible and a smart move - decluttering your Storyboards presents cleaner interface files to dig through, reduced loading times in XCode, and better group editing.
I've been combing across Stack Overflow for a while and noticed everyone is resorting to Custom Segues or instantiating tab based setups programmatically. Yikes. I've hacked together a simple UIViewController subclass that you can use as a placeholder for your storyboards.
Code:
Header file:
#import <UIKit/UIKit.h>
#interface TVStoryboardViewController : UIViewController
#end
Implementation file:
#import "TVStoryboardViewController.h"
#interface TVStoryboardViewController()
#property (nonatomic, strong) UIViewController *storyboardViewController;
#end
#implementation TVStoryboardViewController
- (Class)class { return [self.storyboardViewController class]; }
- (UIViewController *)storyboardViewController
{
if(_storyboardViewController == nil)
{
UIStoryboard *storyboard = nil;
NSString *identifier = self.restorationIdentifier;
if(identifier)
{
#try {
storyboard = [UIStoryboard storyboardWithName:identifier bundle:nil];
}
#catch (NSException *exception) {
NSLog(#"Exception (%#): Unable to load the Storyboard titled '%#'.", exception, identifier);
}
}
_storyboardViewController = [storyboard instantiateInitialViewController];
}
return _storyboardViewController;
}
- (UINavigationItem *)navigationItem
{
return self.storyboardViewController.navigationItem ?: [super navigationItem];
}
- (void)loadView
{
[super loadView];
if(self.storyboardViewController && self.navigationController)
{
NSInteger index = [self.navigationController.viewControllers indexOfObject:self];
if(index != NSNotFound)
{
NSMutableArray *viewControllers = [NSMutableArray arrayWithArray:self.navigationController.viewControllers];
[viewControllers replaceObjectAtIndex:index withObject:self.storyboardViewController];
[self.navigationController setViewControllers:viewControllers animated:NO];
}
}
}
- (UIView *)view { return self.storyboardViewController.view; }
#end
Description:
The view controller uses its Restoration Identifier to instantiate a storyboard in your project.
Once loaded, it will attempt to replace itself in its
UINavigationController's viewController array with the Storyboard's
initial view controller.
When requested, this subclass will return the UINavigationItem of the Storyboard's initial view controller. This is to ensure that navigation items loaded into UINavigationBars will correspond to the view controllers after the swap.
Usage:
To use it, assign it as the subclass of a UIViewController in your Storyboard that belongs to a UINavigationController.
Assign it a Restoration ID, and you're good to go.
Setup:
And here's how you set it up in the Storyboard:
This setup shows a tab bar controller with navigation controllers as its first tab controllers. Each navigation controller has a simple UIViewController as its root view controller (I've added UIImageViews to the placeholders to make it easy to remember what it links to). Each of them is a subclass of TVStoryboardViewController. Each has a Restoration ID set to the storyboard they should link to.
Some wins here:
It seems to work best for modal presentations where the subclass is the root view controller of a navigation controller.
The subclass doesn't push any controllers on the stack - it swaps. This means you don't have to manually hide a back button or override tab behaviour elsewhere.
If you double tap on a tab, it will take you to the Storyboard's initial view, as expected (you won't see that placeholder again).
Super simple to set up - no custom segues or setting multiple subclasses.
You can add UIImageViews and whatever you like to the placeholder view controllers to make your Storyboards clearer - they will never be shown.
Some limitations:
This subclass needs to belong to a UINavigationController somewhere in the chain.
This subclass will only instantiate the initial view controller in the Storyboard. If you want to instantiate a view controller further down the chain, you can always split your Storyboards further and reapply this subclass trick.
This approach doesn't work well when pushing view controllers.
This approach doesn't work well when used as an embedded view controller.
Message passing via segues likely won't work. This approach suits setups where sections of interface are unique, unrelated sections (presented modally or via tab bar).
This approach was hacked up to solve this UITabBarController problem, so use it as a partial solution to a bigger issue. I hope Apple improves on 'multiple storyboard' support. For the UITabBarController setup however, it should work a treat.
This is a bit late for Hawke_Pilot but it might help others.
From iOS 9.0 onwards you can create a Relationship Segue to another storyboard. This means that Tab Bar View Controllers can link to View Controllers on another storyboard without some of the mind-bending tricks seen in other answers here. :-)
However, this alone doesn't help because the recipient in the other storyboard doesn't know it's being linked to a Tab Bar View Controller and won't display the Tab Bar for editing. All you need to do once you point the Storyboard Reference to the required View Controller is select the Storyboard Reference and choose Editor->Embed In->Navigation Controller. This means that the Nav Controller knows it's linked to a Tab Bar View Controller because it's on the same storyboard and will display the Tab Bar at the bottom and allow editing of the button image and title. No code required.
Admittedly, this may not suit everyone but may work for the OP.
Not sure if your question is answered, and for others looking for a solution to this problem, try this method.
Create the Tab Bar Controller with Navigation Controllers in one storyboard file. And add an empty view controller (I named it RedirectViewController) as shown in the picture.
The child view controller (let's call it SettingsViewController for your case) is located in Settings_iPhone.storyboard.
In RedirectViewController.m, code this:
- (void)viewWillAppear:(BOOL)animated
{
UIStoryboard *settingsStoryboard = [UIStoryboard storyboardWithName:#"Settings_iPhone" bundle:nil];
UIViewController *rootSettingsView = [settingsStoryboard instantiateInitialViewController];
[self.navigationController pushViewController:rootSettingsView animated:NO completion:nil];
}
SettingsViewController will be pushed into view instantly when Settings tab is touched.
The solution is not complete yet! You will see "< Back" as the left navigationItem on SettingsViewController. Use the following line in its viewDidLoad method:
self.navigationItem.hidesBackButton = YES;
Also, to prevent the same tab bar item from being tap and causes a jump back to the blank rootViewController, the destination view controllers will need to implement UITabBarControllerDelegate
- (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController
{
return viewController != tabBarController.selectedViewController;
}
It works for me.
Add Following code to your LinkViewController
-(void) awakeFromNib{
[super awakeFromNib];
///…your custom code here ..
UIStoryboard * storyboard = [UIStoryboard storyboardWithName:self.storyBoardName bundle:nil];
UIViewController * scene = nil;
// Creates the linked scene.
if ([self.sceneIdentifier length] == 0)
scene = [storyboard instantiateInitialViewController];
else
scene = [storyboard instantiateViewControllerWithIdentifier:self.sceneIdentifier];
if (self.tabBarController)
scene.tabBarItem = self.tabBarItem;
}
Here is the screenShot for LinkViewController .
LinkViewController is just a placeholder where new viewController would be placed. Here is the sample code which I used for my app.
RBStoryboardLink . Its working great for me. Let me know if it is helpful for you.
Let me explain. I have multiple UIViewControllers. On my MainPageController, I have 3 UIViews. Let's enumerate it this way: the first UIView is called LoginView, the second is called HomeView and the other one is called RegView. Now in HomeView, there are multiple buttons that will lead to other UIViewControllers. For example, one button will lead to StoreController. Now if I am inside StoreController and I want to go back to MainPageController, I simply call:
[self dismissModalViewControllerAnimated:YES completion:nil]
This will send me back to the HomeView.
That is good. However, inside the StoreController, there are buttons which will supposedly direct me to LoginView or RegView, whichever button was tapped. The problem is when the method [self dismissModalViewControllerAnimated:YES completion:nil], it only take me back to HomeView, no matter which button I pressed.
So how will I display the right UIView once the dismissModalViewControllerAnimated is called?
EDIT:
This is how I show the UIViews:
-(void)viewDidLoad
{
//Initialize the views here...
}
-(void)showViewByTag:(NSInteger)tag
{
if (tag == 1)
{
[self.view addSubview:loginView];
}
else if (tag == 2)
{
[self.view addSubview:homeView];
}
else
{
[self.view addSubview:regView];
}
}
Now I call the method showViewByTag: somewhere in my code to display the views.
What you could try and do is following: before calling [self dismissModalViewControllerAnimated:YES completion:nil] (and thus go back to your home view), change the view currently displayed in your MainPageController:
[(MainPageController*)self.presentingViewController showViewByTag:desiredViewTag];
[self dismissModalViewControllerAnimated:YES...];
If you are worried at the cast and you foresee that self.presentingViewController might be not of MainPageController type on some occasions, then you can check explicitly for its type:
if ([self.presentingViewController isKindOf:[MainPageController class]])
[(MainPageController*)self.presentingViewController showViewByTag:desiredViewTag];
[self dismissModalViewControllerAnimated:YES...];
For this to compile, MainPageController.h must be imported in your modal controller class.
dismissModalViewController will always bring back the viewController which presented it ,and that can be only one,so the ideal way would be to tell the navigationController to initWith your desired viewController..
eg on regButton click in the presented modalview
RegViewController *regViewController = [[RegViewController alloc]initWithNibNam:#"RegViewController" bundle:nil];
[self.navigationController initWithRootViewController:regViewController];