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?
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):
...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:
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.
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.
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.
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(...).
Related
Lets say I have a container view in a parent UIView.
What is the difference between referencing it as an Outlet in my parent UIView, or accessing it this way :
categoryContainerViewController = self.childViewControllers[0] as! CategoriesControllerView
View and view controllers are two different things
A VIEW is an object that is drawn to the screen. It may also contain other views (subviews) that are inside it and move with it. Views can get touch events and change their visual state in response. Views are dumb, and do not know about the structure of your application, and are simply told to display themselves in some state.
A VIEW CONTROLLER is not drawable to the screen directly, it manages a group of view objects. View controllers usually have a single view with many subviews. The view controller manages the state of these views. A view controller is smart, and has knowledge of your application's inner workings. It tells the dumb view objects what to do and how to show themselves.
now you can get idea about View and a view controller.
A view and a view controller are two totally different things.
categoryContainerViewController = self.childViewControllers[0] as! CategoriesControllerView
In spite of the name, that is a view controller.
The outlet is to the view.
In layman terms : -
IBOutlet connects any view element from your interface builder to attached swift class file. So you can get reference to any subview of UIView(eg, UILabel, UIButton) from interface builder to your UIViewController or UIView Swift class
In your ex.
by using
categoryContainerViewController = self.childViewControllers[0] as! CategoriesControllerView
You are getting reference to your ChildViewController and not any view
This question may have been asked similarly relating to iOS, but not OS X. I've been struggling for three days to simply try and embed an NSView or NSViewController within an NSView using storyboards.
This works fine using a .xib or .nib: (when the next button is pushed it displays either customView1 or customView2 within the Container View.
Trying to use a storyboard it does not work. In fact I have no concept or clue on how to connect, embed, call, summons, or beg customView1 or customView2 to get itself inside Container View.
It seems I cannot access anything outside of it's own View Controller!?!
Example of properly working functionality (from .nib):
Here's one way to do it.
You can add two "Container View" objects to your main view controller, as top level objects, and connect them to NSView outlets in your controller. This will automatically create two new view controller scenes, with Embed segues from the container view to the child view controller.
Your view controller now has references to two inner NSViews and you can manipulate them as you wish.
If you need a reference to the child view controllers, assign a storyboard identifier to each Embed segue and implement -prepareForSegue:sender:
- (void)prepareForSegue:(NSStoryboardSegue*)segue sender:(id)sender
{
if ([segue.identifier isEqual:#"Embed1"])
{
_innerController1 = segue.destinationController;
}
else if ([segue.identifier isEqual:#"Embed2"])
{
_innerController2 = segue.destinationController;
}
}
Alternatively to segues, you can assign a storyboard identifier to each of your inner view controllers, and instantiate them in code from the storyboard:
_innerController1 = [self.storyboard instantiateControllerWithIdentifier:#"InnerController1"];
You're also free to mix Storyboards and NIBs, so you can design your inner views in a separate NIB and instantiate them in code.
I've got four scenes in my storyboard. One scene is acting as the parent to all the others via container views. Everything is arranged like so:
If you squint, you'll notice that all four are subclassed from the same view controller. I did this so that I could connect each scene's elements to a single, common view controller and avoid subclassing UIViewController four times. The ProductDetailViewController implementation looks like this:
#implementation ProductDetailViewController {
// Scene 1
__weak IBOutlet UINavigationBar *_navigationBar;
// Scene 2
__weak IBOutlet UILabel *_productName;
// Scene 3
__weak IBOutlet UILabel *_typeNameLabel;
__weak IBOutlet UILabel *_categoryNameLabel;
__weak IBOutlet UIImageView *_richImage;
// Scene 4
__weak IBOutlet UIImageView *_productImageView;
}
The problem is that viewDidLoad fires four times (obviously) and things are showing up blank. When I step through the debugger, the product object I'm passing around is nil for three cycles and then initializes on the fourth. Maybe the view controllers are loading out of order?
In any case, is this setup even ok to do? I'm thinking there's got to be a better way to avoid a subclassed view controller for every storyboard scene.
Setting multiple .xibs to be the same class is common. Not wanting to subclass UIViewController four times is also common.
One problem is that by doing this with Child View Controllers embedded in a Parent View Container and all the same View Controller class, you are creating four instances of the class that are all different and when writing you can't assume anything about which one you are. No wonder the product object is getting lost.
This structure can be salvaged, and it has a lot to recommend it, but it will take some arrangement. A good way is for the parent-delegate to make all the decisions.
Give the class a delegate property, of its own kind, of course weak (weak because the child can't keep the parent alive). Throughout, this you can check to see if it's nil, because if it has a delegate it's a child, but if it has no delegate it's a parent. You usually don't need to know, though.
Use Segue Identifiers. In IB, give each of those embed segues an identifier. Then, in your View Controller class, implement prepareForSegue:sender: with the body
if ([segue.identifier isEqualToString:#"ProductNameIdentiferInStoryboard"]) {
/*...*/ }
else if ([segue.identifier isEqualToString:#"typeBarIdentiferInStoryboard"]) {
/*...*/ }
else if ([segue.identifier isEqualToString:#"productImageIdentifierInStoryboard"]) { /*...*/ }
Implement three properties, again of the same kind, each representing a childViewController,
#property ProductDetailViewController *productNameViewController;
#property ProductDetailViewController *productTypeBarViewController;
#property ProductDetailViewController *productImageBarViewController;`
In prepareForSegue from step 2, fill in with the pattern:
self.productNameViewController = segue.destinationViewController;
self.productNameViewController.delegate = self;
Note prepareForSeque:sender: fires before any viewDidLoad, so you have the references: do all the setup here from the parent's point of view. self.productNameViewController.titleLabel.text = #"Product Name"; The children will fire first, regardless sending a message to nil does nothing, so it's fine they play with their imaginary children. They have a delegate if they need to send information up or ask something important.
This may seem cumbersome, and the identifier stuff definitely is. However, the benefits of keeping centralized control in the code while working with spread out layouts in IB are obvious as a way to handle view controllers.
You definitely can have a single view controller class shared by these four scenes, but there will be four instances of that view controller class, not one.
Having said that, I would advise against this approach and I would suggest either:
Keep the storyboard layout you have, but use a unique view controller class for each child scene, each with its own unique IBOutlet references.
If you'd like the parent view controller to have access to data entered in the child view controllers, you can obviously have the child view controllers update the parent view controller (e.g., using delegate-protocol pattern). But I wouldn't personally expose the child's UIKit properties to the parent view controller (a view controller has no business accessing the UIKit objects of another controller's view), but rather pass back the model data.
If you don't want separate controllers, just don't put these child views in separate scenes. Your example does not appear to be a very compelling use of view controller containment, anyway. In my mind, those child scenes would have to have some reasonable degree of complexity to justify the use of a separate scene (and thus justify separate view controllers). If they're not that complicated, you'd just add subviews to the parent view controller, which is much easier, rather than using container views and the view controller containment that entails.
I'm using a storyboard. Let's say I have a view controller that's named MYviewController.
In - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender; I would like to substitute the view controller that I'm segueing to, by one of its child, for example: MYviewControllerChild1 OR MYviewControllerChild2. The child that's segued to depends on the sender parameter.
These view controllers have the same scene (in the storyboard). Only their behaviour is slightly different.
I have a tableView that shows the user the settings of the application. When he clicks a cell, it segues to a viewController where he can modify the value of some setting. Some of theses are alphanumeric, others are numeric. Depending on which cell is clicked, I'd like the input viewController to format the value accordingly (if it's a decimal value I'll use a NSNumberFormatter for example).
Is that possible?
As mentioned in comments to your OP, I believe you should handle this kind of scenario in one viewcontroller.
However, if you insist on using separate controllers, maybe because you think the functionality will be expanded later down the line and therefore add more diversity, you need to handle this by creating multiple storyboard scenes - one for each child controller.
The destination view controller in prepareForSegue is imposed by the viewcontroller at the end of the segue in the storyboard. I don't think there is any way to override that.
As described, your problem isn't really a good candidate for a storyboard. If you use a storyboard you will have to create and sync multiple scenes. Several possible solutions::
Create multiple storyboard scenes and invoke them manually via performSegueWithIdentifier.
Use a nib file instead of a storyboard for this scene. You can use a single nib file since the view controller is created outside the storyboard with [[VCClass alloc] initWithNibFile: bundle: You can create the appropriate view controller class and pass the same nib file to all instances.
Use a single storyboard scene and view controller and pass in typing information in your prepareForSegue.
Update: I have decided to go a different route with my problem.
Instead of trying to reuse the same UIViewController directly, I use two vanilla UIViewControllers that are set as rootViewControllers. In their loadView methods, they make a call to [storyboard instantiateViewControllerWithIdentifier:] to get the former UIViewController and set their views to the controller's view. This is probably the preferred approach anyway, since I need to set several variables and delegates.
I have a UIStoryBoard with a UITabBarController as the entry point connected with two UINavigationControllers. Each of those share a common UIViewController as their root view controller. When the app starts, the first UITabBarItem is selected and the view loads as-expected. However, when I select the second UITabBarItem, the same view is not visible. I see the UINavigationBar with a black background. Am I doing something incorrect with the Storyboard interface, or do I need to manually instantiate the UIViewController via each UINavigationController's method--loadView for instance?
Strangely this is a question that no one else is asking. As far as I know it is not possible to share the rootViewController which I think is without a doubt a bug since when you inspect the connection on the storyboard you can see that the view controller is connected to both navigation controllers. I consider this a flaw in storyboarding because duplicating viewControllers and reapplying all of their connections is quite error prone and makes the storyboards overly complex.
I see your solution to the problem. Workarounds like this make me question if the current storyboard functionality in iOS is ready for creating apps. I think that there is a conceptual problem with the storyboards, Apple needs to decide if a viewController on a storyboard represents an instance or if it represents just the class, right now it is not consistent as you can see that multiple segues can actually point to the same viewController but in reality each segue has its own instance, why this is not also followed for rootViewController connections?, I don't know.
Just as a note, with your solution take into account the following from Apple's documentation:
"Important A view controller is the sole owner of its view and any subviews it creates. It is responsible for creating those views and for relinquishing ownership of them at the appropriate times, including during low-memory conditions and when the view controller itself is released. If you use a storyboard or a nib file to store your view objects, each view controller object automatically gets its own copy of these views when the view controller asks for them. However, if you create your views manually, you should never use the same view objects with multiple view controllers."
http://developer.apple.com/library/ios/#documentation/UIKit/Reference/UIViewController_Class/Reference/Reference.html
Definitely a bug in Storyboards. Another way to do this is to create basic UIViewControllers for each UINavigationController, then have a UIContainerView that points to the same UIViewController in each of the basic view controllers.
I think the easiest solution is to set no root view controller for your nav controller in the storyboard and then do something like this with your nav controller:
- (void)viewDidLoad
{
[super viewDidLoad];
UIViewController *topVC = [[UIStoryboard storyboardWithName:#"MainStoryboard" bundle:nil] instantiateViewControllerWithIdentifier:#"myTopVC"];
[self pushViewController:topVC animated:NO];
}
The first-pushed VC becomes the root.