I have view A with view controller A and view B with view controller B.
View A has a Container View which I've held down control + dragged to view B to make an association. At this point running the app shows view B inside view A's Container View
From view controller B I can programmatically change properties, but I'd like to change properties once it's loaded (or perhaps control the initialization of view B showing up in the Container View).
What I have is a wizard step (view B) and I want to highlight different icons based on the view that is consuming it, but I'm not sure how to call into the view controller B from view controller A (or if that's even the approach I want to take).
You can use prepareForSegue to get a reference to the controller in the container view from the main controller (controller A in your case). That controller will be the segue's destinationViewController, and prepareForSegue will be called as soon as the two controllers are instantiated (which happens one right after the other).
In your storyboard set an Identifier for the segue between A & B and then in Controller A add this code
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"YOUR_IDENTIFIER"]) {
ViewControllerB *viewController = segue.destinationViewController;
viewController.property = value; // You can pass any value from A to B here
}
}
You could work with a single view controller that has the desired number of UIViews you want to have. This way you can control all the views from a single controller without needing to switch controllers. You can hide and show the views (and highlight the icons) according to your scenarios.
Related
Unwind segues seem not to behave as expected in iOS 8.1 when combined with a modal view and container view.
Here's the view controller hierarchy for the test project which can be found on github:
Tapping on the "tap me" button pushes the modal view which is embedded in a navigation controller and which has a tableView as a child view controller. Tapping on a row in the tableView pushes another tableView. Finally, tapping on a row in this final tableView should call the unwind segue named bUnwindSegue found on the previous view controller.
Problems:
bUnwindSegue is never called.
According to technical note TN2298 a container view controller is responsible for selecting the child view controller to handle a segue. In this case viewControllerForUnwindSegueAction:fromViewController:withSender: should be called on the container view controller. It isn't.
In the example project, you can see that BTableViewController contains the unwind segue:
- (IBAction)bUnwindSegue:(UIStoryboardSegue *)segue;
{
NSLog(#"Unwinding...this unwind segue will never get called.");
}
In the storyboard, the cell selection action for CTableViewController is indeed the bUnwindSegue. Also note that if you change the cell select action of CTableViewController to the unwind segue in the container view controller -- containerVCUnwindSegue -- that the segue is called correctly.
Are unwind segues not behaving as expected?
(1) You're misunderstanding the technical note TN2298 you cited and (2) you're not overriding viewControllerForUnwindSegueAction: appropriately.
As the TN2298 doc section you linked to about Container View Controller states underneath its "Selecting a Child View Controller to Handle An Unwind Action" subheading:
Your container view controller should override the method shown in
[viewControllerForUnwindSegueAction:] to search its child
view controllers for a view controller that wants to handle the unwind
action. If none of a container's child view controllers want to handle
the unwind action, it should invoke the super's implementation and return
the result.
First off, to override the method, you have to subclass the UINavigationController in your storyboard and add the viewControllerForUnwindSegueAction: method there. After doing that, you'll see the method is now being called as expected.
But your second error is that your current attempt to override the viewControllerForUnwindSegueAction: method simply contains return self;. You should instead be returning the view controller that you'd like to handle the unwind action.
So say, for example, you have a public variable in VCWithContainedVCsViewController to hold the current instance of BTableViewController and you access you're able to access that current container view controller, ex:
- (UIViewController *)viewControllerForUnwindSegueAction:(SEL)action fromViewController:(UIViewController *)fromViewController withSender:(id)sender
{
NSLog(#"Technical note TN2298 indicates child VCs defer to their parent to determine where an unwind segue will be handled.");
if ([NSStringFromSelector(action) isEqualToString:#"bUnwindSegue:"]) {
NSLog(#"%#", self.viewControllers);
VCWithContainedVCsViewController *containerVC = (VCWithContainedVCsViewController*)self.viewControllers[0];
return containerVC.container;
}
return [super viewControllerForUnwindSegueAction:action fromViewController:fromViewController withSender:sender];
}
What you'll see in that case is that bUnwindSegue: is in fact being called (your message should print), but the segue still won't happen.
Why is this?
Because as I mentioned in the comments, BTableViewController is not on the current navigation stack. Some child view controllers of BTableViewController, like CTableViewController, will be on the navigation stack because, for example, CTableViewController is not a container view. But BTableViewController itself is not capable of performing the segue on its own because it is not on the current navigation stack. So although you can in fact select some child view controllers to handle unwind actions as the documentation states, BTableViewController isn't going to be one of them.
Is it possible to have one view controller save data or control two views?
Say like a 2 page questionnaire. Instead of have a view controller for each page can one view controller save data from both?
You can, yes, but only one view can be considered the view controller's actual view (the property)
This is important because sublassed view controller methods like
-(void)viewDidLoad
-(void)viewWillAppear
etc
will only call for the actual view of the view controller.
Simple answer YES you can have one view controller and control multiple views on that view controller.
I realized that I have to still treat the second page differently even though it uses the same view controller.
I solved my problem by
-(void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
Test *vc = [segue destinationViewController];
vc.testInt = 2;
}
I'm attempting define a series of container view controllers using Interface Builder (probably my first mistake). In the storyboard, I created the top view controller, added 3 container views, which automatically added each child view controller to the storyboard. I added an outlet for each container view, and am able to successfully maneuver through the child views by hiding/showing the container views. There's really not much code to speak of, it's just:
-(IBAction) button1Pushed:(id)sender
{
containerView2.hidden = true;
containerView1.hidden = false;
}
While that works, I need to update the content on each showing. viewWillAppear (and related functions) fire for the Child View Controllers only on the initial creation, not when hiding/showing the containers. I supposed I could add something like:
[childVC1 updateContent];
containerView1.hidden = false;
but I was hoping I could rely on viewWillAppear (and related functions). I've tried a few things with limited success, and have several questions:
I understand the example in Apple's Programming Guide for manually creating containers:
[self addChildViewController:content]; // 1
[self.view addSubview:self.currentClientView];
[content didMoveToParentViewController:self]; // 3
but how does that apply when using IB? There's no mention of IB in that Programming Guide. IB must be calling addChildViewController, because I can find all the the container VCs using [self childViewControllers]. But since viewWillAppear only happens on creation, does that mean IB didn't go on to add the views as well?
So question 1 is: when adding container views via Interface Builder, how much of the example code does IB handle and how much do I have to implement?
As an experiment, I added this code in the parent's viewDidLoad:
for ( UIViewController *vc in [self childViewControllers])
{
[self.view addSubview:vc.view];
[vc didMoveToParentViewController:self];
}
That caused each of the child views to all appear at once on top of each other at the top left of the screen. It no longer respects the IB layout, and unsurprisingly, no longer respects showing/hiding the containerView, either. I can control them with
vc.view.hidden = true;
but they're moved to some default position. I could manually reset their coordinates, but one of the reasons for using IB in the first place was to avoid doing screen positioning via code.
question #2 is: when adding container views in IB, how am I supposed to manage displaying the children while still honoring the IB layout? By manipulating the container view's outlet? by manipulating the child view controller (found via [self childViewControllers])? Or something else?
At the end of the day, my desired state is:
Define the screen layouts and positions in IB for the parent and children
Hide/show children based on buttons selected by the user
viewWillAppear fires each time a child is displayed, allowing me to update the content
Any advice on reaching this state is appreciated, thanks!
Answer 1: When you add child view controllers via the storyboard (called container views in this case) then the children are added via segues. No it's not a segue in the normal sense where you push or present a view controller, but the method that's called in your 'super' view controller is prepareForSegue. In interface builder you can name the connection between the two controllers just like a normal segue and grab a reference to the view controller in this instance. This takes the places of the whole dance of addChildViewController, didMoveToParent and so forth.
Answer two: I think it makes sense to address your desired state. I looks like you have already solved #1 and #2. As for getting viewWillAppear to fire again you would have to get your parents viewWillAppear to fire again by doing something like pushing and popping a new VC or presenting and dismissing one. Just because you set it to hidden and then unhide it won't make it fire again (hidden basically tells the drawing system to not render it).
The recommended approach (from Apple, forgive me I don't know the link) is to do all of your updates in your view subclass via updateViewContraints or layoutSubviews. In the view controller world the similar method is ViewDidLayoutSubviews. You can signal to your view that it needs to be laid out again by calling [yourViewController.view setsNeedsLayout]. For reference check out the doc on UIView in regards to needsLayout.
Likely what you'll want to do when your button is pressed to unhide your viewController is something like:
1: set any properties or call methods on your viewController which update its content
2: call setsNeedsLayout on your viewControllers view
3: unhide it.
viewWillAppear will not fire for the children each time they are shown. Adjust your thinking.
When you create a container view and a child view in IB, it sets up an embed segue to link to the child view controllers.
What I suggest you do is to add unique segue identifiers for each of your child view controllers, and then in prepareForSegue, use an if/else if/else if statement to match each segue ID. When you find a segue Id, save that child view controller in a property of the parent:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString: #"firstChildID")
self.firstChildVC = (FirstChildVC *) segue.destinationViewController;
else if ([segue.identifier isEqualToString: #"secondChildID")
self.secondChildVC = (SecondChildVC *) segue.destinationViewController;
else if ([segue.identifier isEqualToString: #"thirdChildID")
self.thirdChildVC = (ThirdChildVC *) segue.destinationViewController;
}
Then define a protocol that you use to communicate with your child view controllers to tell them that they are being shown:
#protocol ChildVCProtocol
- (void) getReadyToShowView;
//whatever other methods
#end
Then make each of your child VCs conform to that protocol. Put your code to get ready to be shown in the getReadyToShowView method. Call that method every time you make a child VC visible.
I'm working on an app that I need to change the text of text view of child view controller from a method in the parent view controller.
I have a button in child VC, when it's pressed, the view doesn't change, but a method in parent VC is called, and another method in parent VC will change the text of text view in child VC.
[self.delegate buttonPressed];//in child VC, call method in parent VC
It was working before I use storyboard.
In the parent vc, I just had code below,
childViewControler.textViewName.text=#"something";
Now I changed to storyboard, by using NSLog, I know the method to change the text is called. But no text is displayed.
I'm thinking I might add something since I am using storyboard now?
Can someone give me advice?
Thank you.
The childViewControler variable in your parent view controller needs to point to the instance of your child view controller class created by the segue. If you don't assign anything to it, it's nil, and any messages to it do nothing.
It's unclear from the code you've posted whether it's a property, instance variable, or local variable; here's how you could do it with an instance variable:
#implementation ParentViewController
{
ChildViewController *childViewController;
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
// if you have multiple segues, check segue.identifier
// so you only do this for the correct segue
childViewController = segue.destinationViewController;
}
- (void)buttonPressed
{
childViewController.textViewName.text=#"something";
}
#end
The key bit is in prepareForSegue:sender: -- you need to make sure that the childViewController variable points to the view controller created by the segue.
It's also possible that your problem results from still having some old pre-storyboard code that allocs and inits a child view controller and puts it into your childViewController variable. In that case, you'd have two instances of that view controller class around: the one created by the segue and the one you made. Only the one created by the segue is hooked into the UI, so any changes you make to the other won't be visible. You can solve this problem by getting rid of any code that creates and presents the child view controller -- the storyboard takes care of all that now, and prepareForSegue:sender: is the only place you need to get a reference to the child view controller.
I'm trying to put together an iPad app using UISplitViewController and storyboards. The master view starts with a navigation controller linked to a table view of 6 menu options. Each cell in the table pushes a different table view controller onto the navigation stack. This is working fine for the master view. Each master view has a table list which when clicked needs to display a different view controller in the detail pane. I've currently done this with a segue set to 'Replace' and 'Detail Split' which works the first time a row is clicked, but as soon as you click another row in the master view, or rotate the device then the app crashes with EXC_BAD_ACCESS.
I'm fairly sure my problems are to do with how the delegate is setup for the UISplitViewController. I'm confused as to how this should be used when I have multiple master VCs and multiple detail VCs. Where should the delegate code be placed - master or detail? Do I have to implement the UISplitViewControllerDelegate protocol events in every view controller?
Any help appreciated.
If the split view controller delegate was the detail view controller that had been replaced, this is the cause of the crash. The replaced detail view controller is being dealloc'd and so the split view controller delegate is no longer a reference to a valid object.
You can update the delegate in prepareForSegue:sender:. For example:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:#"MySegue"]) {
UIViewController *destinationViewController = [segue destinationViewController];
if ([destinationViewController conformsToProtocol:#protocol(UISplitViewControllerDelegate)]) {
self.splitViewController.delegate = destinationViewController;
}
else {
self.splitViewController.delegate = nil;
}
}
}
Which view controllers you use for delegates is dependent on your view controller hierarchy. In the simplest case, any view controllers that are assigned to splitVC detail will probably need to be delegates. You may want to base them all on a common super class that handles the shared split view controller delegate logic.