ChatGPT解决这个技术问题 Extra ChatGPT

Linking child view controllers to a parent view controller within storyboard

Can you associate child view controllers to a custom container view controller in Storyboard?

I can link child view controllers to a tab view controller, and I can link one view controller to a navigation controller.

What must I do to the container VC to accept child VCs?


B
Ben Mosher

As something of a combo of Caleb and Matt's answers, I did:

-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    if ([segue.identifier isEqualToString:@"cpdc_check_embed"]) {
        self.checkVC = segue.destinationViewController;
    }
}

...where checkVC is a property on the container controller:

@property (weak,nonatomic) PXPCheckViewController * checkVC;

You just have to set your embed segue's Storyboard ID to whatever you want (in this case, cpdc_check_embed):

https://i.stack.imgur.com/5Zy5h.png

...and then check the identifier in -prepareForSegue:sender:.

Still not an outlet, but cleaner than Matt's (IMHO) and more specific than Caleb's, and you still get a nice-looking storyboard:

https://i.stack.imgur.com/IMhUd.png


You're using an embed segue, which is an iOS 6 feature. My question dates from November 2011. :-p
Yep. Just realized I'm letting the cat out of the bag a little. Tried to install to a client's 5.1.1 phone; no dice.
Now that iOS 6 has been released, this is the correct answer.
@awolf I'm afraid you're right. I guess "correct answers" can evolve too… I changed the flag, causing Matt's rep to return to 666 (coincidence? :-p ).
How can you get 2 segues from the same container? I am trying to add a second embeded view controller but as soon as I click embed type, the previos segue goes away and I am left with only 1 segue.
M
Matt

The storyboard deals with built-in container view controllers very nicely, displaying segues to child/root view controllers so that relationships are clearly shown. It is also nice how the children and parent view controllers are separated into different scenes.

If you want to achieve this effect in your own project, then there is a trick that is not perfect but very straightforward. In my example, suppose I have a container view controller that acts like a tab bar controller with only two tabs, 'left' and 'right'. I want to have a scene represent the parent view controller, and two separate scenes represent both the 'left' child view controller and the 'right' child view controller.

Even though it is impossible, it would be nice if I could create IBOutlets from the container view controller to its children in different scenes, and then when my container view controller is displayed set up the parent/child relationships according to the rules described the UIViewController documentation. If we had references to our 'left' and 'right' child view controllers, then we could set up the relationships no problem.

The standard solution to this referencing problem is to create references to child view controllers by dragging in Object outlets into the container view controller's scene, and then specifying their class type as being instances of the child view controller classes.

In order to keep children separated in different scenes like Apple's built-in containers, however, we will use a different trick. First, suppose we have the following properties declared in our container class, ContainerViewController:

@property (nonatomic, strong, readwrite) UIViewController *leftViewController;
@property (nonatomic, strong, readwrite) UIViewController *rightViewController;

In our storyboard, select the scene representing the 'left' view controller. In the attributes inspector, set the view controller's identifier property to "cvc_leftViewController" ("cvc_" refers to ContainerViewController, but really the identifier can be anything you want). Do the same for the right view controller's scene, setting it's identifier to "cvc_rightViewController".

Now insert the following code into ContainerViewController's viewDidLoad method:

if (self.storyboard) {
    _leftViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"cvc_leftViewController"];
    _rightViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"cvc_rightViewController"];
}

When ContainerViewController is loaded from the storyboard, it will go grab the 'left' and 'right' view controllers from their respective scenes and set references to them via its properties. Now that you have control of the child view controller instances, you can set up the parent/child relationships however you like. To learn how to do that properly refer to the UIViewController documentation.

This trick is not perfect, and has many caveats, but if you are careful you can make it work nicely for your project.

Edit: Although this is completely unnecessary and doesn't mean anything, if you really really want to have the storyboard display connections from your container to your child view controllers just like Apple's built-in containers, just use my method above and then set up segues directly between the container scene to the child scenes, and simply never perform those segues. Now everything will work correctly and look pretty too.


+1 Sounds like a good technique, and you're right that it mimics the way Apple's container view controllers work in storyboards. The main drawback compared to creating the child view controllers in the same scene as the container is that you can't make connections between scenes. So, if you want to set a child controller's delegate to the parent controller, you have to do that in code -- you can't just drag a connection.
True. Also, I always get a bit nervous basing connections off of identifier strings rather than the feeling of stability that comes with outlets.
Cool hack! I'll use it in my existing iOS 5 apps like now. My project's going to be much simpler. Apple solved my problem for iOS 6 (with view controller container views right in Storyboard/IB), but that's for new apps or when I decide to abandon iOS 5 in 2-3 years for existing apps.
C
Caleb

Can you associate child view controllers to a custom container view controller in Storyboard?

I think what you're asking here is how to connect a view controller in one scene to an outlet of a view controller in a different scene. I don't believe that's possible, perhaps because the storyboard machinery may not have all the scenes in a storyboard loaded at the same time.

You're probably asking this because you want to pass some information from one view controller to another as you segue from one scene to the next. The way to do this when you're working with storyboards is to override -prepareForSegue:sender: in one or both view controllers affected by the segue. The UIStoryboardSegue object provided in the segue parameter has sourceViewController and destinationViewController properties, and also an identifier property. You can use these properties to identify the segue that's about to transfer data between the view controllers.

Ray Wenderlich's blog has a nice two-part tutorial on using storyboards that may help you:

Part 1 covers setting up a storyboard project, adding scenes, and creating segues.

Part 2 deals with using segues to transition between scenes, including the prepareForSeque method mentioned above.

iOS 5 allows multiple view controllers to be active in the same scene (although one should still be in charge), so a single scene in your storyboard might have several controllers. You can use outlets to connect these controllers to each other, and you can configure those connections the same way you did in IB: control-drag from one controller to another in the same scene. The usual outlet list will pop open to let you choose which outlet to connect.


No, I mean using two view controllers at the same time. A container view controller containing itself multiple child view controllers. Something like the split view controller, which contains a master view controller and a detail view controller, but children of the split view controller (the parent VC).
In that case, just drag a NSObject from the library into the dock for the scene that you want to contain the child view controller. Set the object's type to your view controller class. After that, you can control-drag to make connections to or from other objects, including other view controllers in the scene. What you can't do, as explained above, is to make connections between objects (view controllers or otherwise) in different scenes.
So, unlike Apple's container view controllers, all view controllers (child and parents) must be inside the same scene if I want to display them simultaneously. Well, it does the job and that is all I wanted. Thank you!
@Randy: See my answer for an alternate solution that might be closer to what you are looking for.
C
Community

The key to using multiple controllers in one scene (what I believe you are after here) is using the mysterious Object from the Objects list in IB to represent the other view controller and hooking up its outlets.

This answer How to create custom view controller container using storyboard in iOS 5 should help I hope. The answer also provides a working example app which is very helpful.


C
Chris Conover

The problem with @Ben's (otherwise reasonable) answer is that it only works at one level of nesting. Beyond that, it would required that every subsequent VC is customized to save the nesting view controller in prepareForSegue.

To solve this, I spent too much time exploring an NSObject based index that that you could add to the Storyboard, bind to a scene, and which would then register it's parent VC in a global index, based on type and restorationId. That works / can work, but is too much effort in the end, and still requires the two step process of visually binding, and programmatically looking up.

For me, the simplest and most general solution is to lazily descend the view controller hierarchy

In my simple test project, I added the following lines to viewDidLoad:

    self.left.data = [
        "Zombie ipsum reversus ab viral inferno, nam rick grimes malum cerebro.",
        "De carne lumbering animata corpora quaeritis." ]

where left is defined as:

lazy var left:CollectionViewController = { [unowned self] in
    return self.childViewControllerWithId("Left") as! CollectionViewController }()

and childViewControllerWithId is defined as:

extension UIViewController {
    func childViewControllerWithId(rid:String) -> UIViewController? {

        // check immediate child controllers
        for vc in self.childViewControllers as! [UIViewController] {
            if vc.restorationIdentifier == rid { return vc }
        }

        // check nested controllers
        for vc in self.childViewControllers as! [UIViewController] {
            if let vc = vc.childViewControllerWithId(rid) {
                return vc
            }
        }

        assert(false, "check your assumptions")
        return nil
    }
}

Note that you could do other find variants based on type, if need be. Also note that the above requires that you define the restoration id in the Storyboard file. If you did not have repeated instances of the same view controller, then using type would be easier.

And to state what is hopefully obvious, you don't need to implement prepareForSegue, nor do you have to use the lazy loading, you just have to call find(...).