I have a UIViewController that is creating another view controller, and adding its view as a subview:
In the parent UIViewController:
SlateMoreView* subView = [[SlateMoreView alloc] initWithNibName:#"SlateMoreView" bundle:nil];
[self.view addSubview:subView.view];
I then need to call a method from the subview, in the parent view.
I have seen how to do this when I am adding the sub UIViewController using [self.navigationController pushViewController: subView animated: YES], because I can find the parent using this kind of code:
In the sub view UIViewController:
NSArray* viewControllerArray = [self.navigationController viewControllers]
int parentViewControllerIndex = [viewControllerArray count] - 2;
SlateView* slateView = [viewControllerArray objectAtIndex:parentViewControllerIndex];
...and then I can send messages to it. But since I added the sub view manually by using addSubView, I can't do this.
Can anyone think of how I can talk to my parent UIViewController?
Thanks!
UIViews have a superview property which seems to be what you are looking for.
In addition you probably don't want to nest UIViewController's view like that unless you are very deliberately building a custom contain view controller. See http://blog.carbonfive.com/2011/03/09/abusing-uiviewcontrollers/
You might want to consider if your problem can be solved by using NSNotifications. You could post a notification from your subview when an event happens that interested listeners (your superview) need to know about . When the superview receives the notification, it can run whatever code you wish. All the while the subview never needs to know about the superview.
This is one way to make your classes less dependant on each other.
You could also use delegation as another option.
When you add your view as a subview to a view hierarchy, you put it in the responder chain. You can go up the responder chain to reach the view controller as a UIView controlled by a UIViewController has the UIViewController as its nextResponder.
id object = theSubview;
do {
object = [object nextResponder];
} while ( ![object isMemberOfClass:[YourViewController class]] );
// object has the view controller you need.
Related
I want to create a custom segue that acts in the same way as the standard push segue does when used on UINavigationController view controllers. I've implemented my custom segue:
CustomSegue.m
-(void)perform {
UIViewController *source = (UIViewController *)[self sourceViewController];
UIViewController *dest =(UIViewController *)[self destinationViewController];
if (1==2 //test) {
[source.navigationController pushViewController:dest animated:YES];
}
else {
UIViewController *altDest = [[UIStoryboard storyboardWithName:#"MainStoryboard" bundle:NULL]
instantiateViewControllerWithIdentifier:#"alternateView"];
[source.navigationController pushViewController:altDest animated:YES];
}
As you can see, the reason I want to use a custom push segue is so that I can decide which view controller to push based on the user configuration (currently only checking a trivial 1==2 expression). I can instantiate the alternate view controller with no issue, but what I want to be able to do is go back and forth without reloading the view controller each time (using the back and next buttons). Is there a way to retrieve an existing instance from the storyboard, or some standard way of doing this?
Instead of a custom segue with its perform, the way to do what you describe, i.e. choose in real time whether to push dest or altDest, is either (1) do not use segues at all and just call pushViewController directly as you are doing here, or (2) prepare two segues emanating from the view controller as a whole, and call performSegueWithIdentifier: to say which we should perform.
As for going directly from dest to altDest, you can push altDest on top of dest and then remove dest from the stack of the navigation controller's view controllers.
Like so much about about iOS, this is all so much easier and more obvious if you do not use a storyboard at all. This is why I don't like storyboards: they are so simple-minded and limiting, and they distract one's attention from the way iOS really works.
There is no way to retrieve an existing controller from a storyboard -- it would be nice if there were a controllerWithIdentifier: method to do that, but there isn't. Segues (other than unwinds) always instantiate new controllers, so I don't think you can do what you want with a segue. If you want to be going forward (push) to the same controller multiple times, then you need to do it in code by creating a property that points to your controller, and checking if that controller exists before pushing to it.
As the others have pointed out, you can't use a segue to push to an existing instance of a controller. The process of performing a segue always creates a new instance the destination controller for you.
Personally, when I'm jumping between existing instances of view controllers, I think "container view controller", such as a UIPageViewController, which makes it really easy to transition between two or more controllers, without necessarily reinstantiating them every time.
If you don't like the constraints the page view controller imposes (e.g. maybe you don't like the fact that iOS 5 version only supports page curl transitions, or that iOS 6 only adds the scroll transition, and you want something else), then you'd do a custom container view controller.
For example, if I wanted to jump between two view controllers and not reinstantiate them every time, I'd first create a custom container view controller, the "parent", and make sure I have a property to keep track of which child I'm currently at:
#property (nonatomic) NSInteger childViewIndex;
If supporting iOS 6.0 and above only, I'd then add a "container view" to my parent view controller's scene. If supporting iOS versions prior to 6.0, I'd add a standard UIView to the scene and then manually instantiate the first child controller:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
UIViewController *controller;
// add the first child
UIViewController *controller = [self addChildWithIdentifier:#"One"];
[self.containerView addSubview:controller.view];
[controller didMoveToParentViewController:self];
self.childViewIndex = 0;
}
- (UIViewController *)addChildWithIdentifier:(NSString *)storyboardIdentifier
{
UIViewController *controller = [self.storyboard instantiateViewControllerWithIdentifier:storyboardIdentifier];
[self addChildViewController:controller];
controller.view.frame = self.containerView.bounds;
controller.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
return controller;
}
Then, when I want to transition to the second child (or transition back to the first child), I'd call the following routine in the parent view controller:
- (void)transitionToViewControllerIndex:(NSInteger)index
{
// don't do anything if we're trying to transition to ourselves!
if (index == self.childViewIndex)
return;
// identify the two controllers in question
UIViewController *sourceController = self.childViewControllers[self.childViewIndex];
UIViewController *destinationController;
// if we're asking for page 2, but we only have one defined, then we'll have to instantiate it
BOOL instantiateDestination = (index == 1 && [self.childViewControllers count] < 2);
if (instantiateDestination)
destinationController = [self addChildWithIdentifier:#"Two"];
else
destinationController = self.childViewControllers[index];
// configure the destination controller's frame
destinationController.view.frame = sourceController.view.frame;
// if you're jumping back and forth, set the animation appropriate for the
// direction we're going
UIViewAnimationOptions options;
if (index > self.childViewIndex)
{
options = UIViewAnimationOptionTransitionFlipFromRight;
}
else
{
options = UIViewAnimationOptionTransitionFlipFromLeft;
}
// now transition to that destination controller
[self transitionFromViewController:sourceController
toViewController:destinationController
duration:0.5
options:options
animations:^{
// for simple flip, you don't need anything here,
// but docs say this can't be NULL; if you wanted
// to do some other, custom annotation, you'd do it here
}
completion:^(BOOL finished) {
if (instantiateDestination)
[destinationController didMoveToParentViewController:self];
}];
self.childViewIndex = index;
}
Thus, to transition to the second child view controller, you could simply call:
[self transitionToViewControllerIndex:1];
If you want to transition back, you could call:
[self transitionToViewControllerIndex:0];
I'm only scratching the surface here, but container view controllers (or if none of the standard ones do the job for you, a custom container view controller) is precisely what you need.
For more information, see:
Creating Custom Container View Controllers in the View Controller Programming Guide for iOS.
Implementing UIViewController Containment in the WWDC 2011 Session Videos (Apple developer ID required).
Implementing a Container View Controller in the UIViewController Class Reference.
Page View Controllers in the View Controller Catalog for iOS.
I have a main UIViewController where most of the users' interaction happens. In the main ViewController there are three subviews. The user can load separate ViewControllers into the UIView subviews.
Each of the subviews that are loaded deal with data entry. In turn, firstResponders are called. I would like to be able to dismiss the firstRespnders through the main ViewController, maybe with a 'Done' button.
I was thinking I could add a method in each of the separate subviews with one name ex;
-(void) methodToResignResponders {}
Then, in the main ViewController call this method to the view that is currently open to the user. In turn resigning the responders that are active in the subview.
Further Information:
This is how I set up each view as a subview of the main ViewController:
UIViewController *calcVC;
//set up the view to be added depending on the name of the view that was passed
if ([viewName isEqualToString:#"Tax"]) {
calcVC= [[TAXViewController alloc]initWithNibName:#"TAXViewController" bundle:nil];
}else if ([viewName isEqualToString:#"Rent"]){
calcVC= [[RENTViewController alloc]initWithNibName:#"RENTViewController" bundle:nil];
}else //continues with more views...
//Then add it to the subview
[firstView addSubview:calcVC.view];
Not sure if I've got the gist of this, mostly because it sounds like you've already solved it yourself. :)
But, from what I can see the ViewController you are talking about is always an UIViewController instance named calcVC. If it is always this viewController's view you are referring to you can simply call [calcVC.view resignFirstResponder];
You can make a basic protocol that all of your sub-view controllers implement that has does everything you need (resign first responder and anything else).
Not sure if this answers your question but you can loop through all the subviews and call it if it exists as follows:
for (UIView *subview in [self.view subviews]) {
if ([subview respondsToSelector:#selector(resignFirstResponder)]) {
[subview resignFirstResponder];
}
}
I'm trying to switch between several table views as the root of a navigation controller. Depending on the settings of my app, I want to use different sets of data with different methods, and prefer to have these encapsulated in separate classes.
My thought was to set a view manager class (UIViewController) as the root view controller of the navigation controller. In the view manager we check the settings to see which view we want to load:
if([application_mode intValue]==APPLICATION_MODE_A){
AViewController *aView = [[DeviceTableViewController alloc] init];
[self.view insertSubview:aView.view atIndex:0];
}
else if([application_mode intValue]==APPLICATION_B){
BViewController *bView = [[BViewController alloc] init];
[self.view insertSubview.bView.view atIndex:0];
}
That does in fact insert the appropriate view into the view manager, at the cost of a white bar at the top of the inserted view and no info on the navigation bar, ie the subview is not connected to the navigation controller.
What's the proper way to do this? I'd really prefer not to have one ginormous table view!
Where do you set your navigationController's rootViewController? Can't you just set it to an AviewController's object or an BViewController's object at this time ? You may not need an intermediate UIViewController
I would do at the beginning :
//navigationController comes from a Xib or previous code
if([application_mode intValue]==APPLICATION_MODE_A){
AViewController *aView = [[DeviceTableViewController alloc] init];
navigationController.rootViewController = aView;
[aView release];
}
else if([application_mode intValue]==APPLICATION_B){
BViewController *bView = [[BViewController alloc] init];
navigationController.rootViewController = bView;
[bView release];
}
Since there is no view controller containment, I like the approach outlined in Jonah William's blog:
http://blog.carbonfive.com/2011/03/09/abusing-uiviewcontrollers/
You can't effectively place a view controller inside another; instead, we create something with similar lifecycle methods (viewDidLoad, viewDidAppear, etc) and forward those methods from the parent to the child. This 'psudo-viewcontroller' has a view property that we add as a subview to the parent's view, using UIView addSubView
http://developer.apple.com/library/ios/#documentation/uikit/reference/UIView_Class/UIView/UIView.html#//apple_ref/occ/instm/UIView/addSubview:
With this approach, we can encapsulate view elements, switch them out dynamically in a view controller, place several within a single view controller, etc. This way they can be considered separately from your navigation stack. It's a bit of work, but the cleanest UI encapsulation approach in iOS 4 in my opinion.
I have a ViewController that responds to some touchEvents (touchesBegan, touchesMoved, etc...).
I've found that when I show this controller using presentModalViewController: it works just fine, but I'm trying to add it's View as a subview of another ParentViewController like so:
- (void)viewDidLoad
{
[super viewDidLoad];
//Add SubController
controller = [[SubViewController alloc] initWithNibName:#"SubViewController" bundle:nil];
controller.view.frame = CGRectMake(0, 30, 300, 130);
[view addSubview:controller.view];
[controller release];
}
When I do this, it gets added the parent view but it no longer responds to touch events. Is there a way to fix this?
Also, is there a better way to go about this? I know I probably could have used a View subclass for the child view, but it's supposed to use a Nib and I wasn't sure how to handle that without using a ViewController.
You're correct you should use a UIView subclass.
The easiest way to load it from a nib is to include the subview in your nib.
Just drop a UIView into the view connected to the original view controller.
Then with the view inside selected go to the identity inspector. It's the one that looks like a little ID card.
The very first field is called Custom Class.
Type the name of your UIView subclass here.
If you need a reference to this just create an IBOutlet in your original view controller and hook it up. That way you can set hidden = YES until you need it.
In your UIView subclass you might want to override
- (void)awakeFromNib
This will get called when the nib first unpacks.
for setting up any gesture recognizers, etc.
To load a nib directly into a view :
// Get the views created inside this xib
NSArray *views = [NSBundle mainBundle] loadNibNamed:#"myViewNib" owner:nil];
// There's probably only one in there, lets get it
UIView *myView = [views objectAtIndex:0];
// Do stuff . . .
[[self view] addSubview:myView];
You could try to call becomeFirstResponder in your subview and see whether it receives touchesBegan... It is probably so, but it will also possibly make the superview not receive touchesBegan if you require it...
I need to add this to my dismiss button :-
[self dismissModalViewControllerAnimated:YES];
[self release];
else
[self.view removeFromSuperview];
I thought
if( self.navigationController.modalViewController ) {
would work be it nevers true
A couple of things:
1) You shouldn't ever release yourself in an object. If you're presenting a modal view controller, you should perform the release there since the view controller will now be retained by the view controller's .modalViewController property:
(In the parent):
UIViewController *someViewController = [[UIViewController alloc] init];
[self presentModalViewController:someViewController animated:YES];
[someViewController release];
2) The parent will store its child modal view controller in .modalViewController. The child will have its .parentViewController property set in this case. If the view has been added as a subview, its .superview property will be set. These are not mutually exclusive, however, so be careful. Generally speaking, UIViewControllers are intended to host full-screen views, and if you're adding the view as a subview, you should ask yourself if the view should just be a UIView subclass, and move the logic into the parent view controller.
That said, I suppose you could check your case (assuming you don't present modal view controller and add as a subview simultaneously):
if (self.parentViewController) {
[self dismissModalViewControllerAnimated:YES];
} else if (self.view.superview) {
[self.view removeFromSuperview]
}
In the latter superview case, the view controller will still be hanging around, so you'd need to let the other view controller know via delegate method or something to release you. In the first case, if you have released the presented view controller already as I described above, it will be released automatically when the parent view controller sets its .modalViewController property to nil.
Normally for a "dismiss" button I would call a method in the controller that presented the modal controller (use a delegate), not try to dismiss the modal view controller from within itself. I don't quite get what youre trying to do though, but that [self release] looks bad. I don't think you ever want to release self like that.
Try this in you modal viewcontroller:
- (IBAction)close:(id)sender {
[self.parentViewController dismissModalViewControllerAnimated:YES];
}
Then just connect the button's action to that method.