Difference in view hierarchy when adding a child VC in code or IB - ios

Iv'e noticed something quite weird, when I add a child VC to a view hierarchy in IB the hierarchy looks like this
parent vc view
- -container view
- - - -child vc view
when adding it by hand
parent vc view
- - child vc view
Following the instructions from apple, in their guide, they never talk about the container view as part of the hierarchy, however in the code in practice, for example when I am trying to call
- (void) hideContentController: (UIViewController*) content {
[content willMoveToParentViewController:nil];
[content.view removeFromSuperview];
[content removeFromParentViewController];
}
the container view is still "polluting" my view hierarchy. I don't understand the relationship between that container and my child VC.
A practical example in my code, is that I put these viewControllers in a UIStackView and when trying to remove the UIViewController that was inserted with an Embed Segue, I remain with a phantom view. The only way I could find to get access to that view, is an IBOutlet from storyboard.
Anyone have experience in handling, replacing or removing ChildViewControllers added with IB? Or can explain where the difference is coming from and how to get rid of it?

I would say that the "container view" you can use in IB is a bit, well... misleading?
If you select it in the Object Library pane, you get an info popup that lists it as UIContainerView. However, if you search Apple's documentation, that is nowhere to be found.
And, as you have probably noticed, if you connect it with an IBOutlet, it gets set as a UIView.... and if you try to do something like:
UIContainerView *vc;
or
let cv: UIContainerView?
you'll get a "Use of undeclared type" error.
What makes it "different" is only how it behaves in IB, and the automatic "load-and-display" of its embedded Child View Controller. This is much like Segues... you can create and "see" them in IB; you can assign properties to them; at run-time they server a purpose / have actions; but... you cannot create a UISegue in code.
So... if you want to use the visual design-and-embed automation of "container views" in IB, you need to be aware that it is added as a UIView to your view hierarchy, and the .view of its embedded VC is added as a subview to itself... and you'll need to use IBOutlet to have a reference to it at runtime if you want to remove it (or size it or change any other property of it).
When you see "code only" implementations of Child View Controllers, you generally also see code for adding the Child VC's view as a subview of the "main" view (or some other view). It just doesn't seem related to the UIContainerView you can use in IB, because it's never referenced that way.
Does that make sense?

Related

Is it bad practice to put UIViewControllers in other UIViewControllers?

I know there is the common practice in iOS development of having one UIViewController presented on the screen whose view is loaded from a XIB which will contain all the UIView subclasses in it.
While I was working on a pretty complex widget for an app, I decided to make the widget subclass a UIViewController instead of a UIView. This was because I thought a UIViewController was essentially a UIView with some helper methods around it. Then I could create a XIB for it (I know UIViews can have their own XIBs too), load the views it contains, place ITS view in the presented parent VC's view, and lay it out.
This works fine so far, but I'm wondering if this is bad practice and if I should just subclass a UIView instead and give it a normal NSObject controller. I am seeing some problems with this and I was wondering if anybody could address concerns I have with this approach?
EDIT NOTE: The widget VC does NOT relate to the VC view it is in and is reusable on ANY screen. So the answer is not subclassing the widget VC with the parent VC. The widget is INSIDE the parent VC, but it is NOT a parent VC.
EDIT NOTE 2: I am NOT using Storyboard. Only Autolayout, XIBs, and ARC.
Why can't we have VC's in VC's?
1) Can VC's be simply dropped into ANOTHER VC's XIB and be loaded easily as a subview?
2) I read here: When to use a UIView vs. a UIViewController on the iPhone?
The top answer explains how the VC controls rotation of the screen and laying out the subviews again, so if you add another VC, then the system will automatically think that THAT is the main VC and will attempt to rotate it instead, causing problems. Is this true? Or is he just talking about if you somehow got into a state where 2 VC's were "presented"? I wasn't sure if his answer applied to VC views that were SUBVIEWS of other VC views.
3) In general is this good practice? It certainly seemed more reasonable as it made loading the subview VC's view much easier.
Thanks!
It's absolutely fine. The answer to your problem is ContainerView.
Container View defines a region within a view controller's view subgraph that can include a child view controller. Create an embed segue from the container view to the child view controller in the storyboard.
You almost got it right. Yes, it's good to make a view controller for what you needed. But you shouldn't just add it's view to the parent view, you should also add the view controller as a child view controller of the first view.
You can add many views controllers as child view controllers of a view controller.
You can learn more about this here.

MVC with view built in code

I've been trying to interpret the lessons from CS193P, and have a few questions.
I'm building views in code, the way I do it is I have a UIView subclass where I put all the views in place in the init method. This class is initialized by the ViewController.
The question is then, what is the right approach from here - say i want to animate a button I placed at 0,0 to 100,100. I'd like to animate it from the ViewController, but i don't like the fact that i set the 0,0 position in the UIView class (in the initializer) and now i am setting a new position in the ViewController. I'd prefer there would be just one place knowing about the actual (x,y) positions of my views.
How am i supposed to go about this?
Move the positions in the initializer to the ViewController
Put a method in my UIView "-(void)AnimateToSecondPosition" where the actual "second position" is then up to the view?
Just let it go. It seems like this would be the right approach if i had placed the button in interface builder - i consider interface builder to be the view then...
Or maybe even a fourth option?
Please help me understand it better and not just give me the right answer ;)
I'd like to be able to compare my approach in some way to how you would do it using interface builder, so each of my views are public and accessable from the controller - this way i believe i could easily start using interface builder instead if i wanted, without changing the controller code, just hooking up the outlets.
I'm guessing the case would be the same for disabling, hiding and doing other things with the views.
Thanks in advance.
If you want to create a new View programmatically you should generally instantiate it in your View Controller using its designated initialiser:
UIView *testView = [[UIView alloc]initWithFrame:myFrame];
If you create a custom view it's totally fine and correct to put some configuration code in the init method, but it's your ViewController that should be in charge of deciding what to do with this view - it is his job! Using the MVC the View and the Model should never communicate directly (as you definitely learned in the first lesson of CS193P).
Therefore the same apply to the animations. You should animate the Views within your ViewController and not implement the animation in the View itself.
Therefore in my opinion you "second position" should be setup by the VC - if this has to be done when something happens to the view (e.g. someone pressing a UIButton) you should set a target/action to your VC and handle this within your VC.
ADDED:
Regarding building UIViews in the Interface Builder I don't know what you mean by "and let them go". Interface builder will create the views and add them to the specific superviews at runtime - as you can see in the example below you control the view hierarchy graphically on the left. For instance in this case there is a UIView (which I coloured green for clarity) and two buttons. One is a subview of the main view while the other is a subview of the green UIView.
Once your ViewController is loaded the view hierarchy is automatically loaded to self.view - in fact if you run the following code in your VC when it is loaded you will see the list of self.views subviews in your console.
for (UIView *view in self.view.subviews){
NSLog("%#", [view description]);
}
If you know already that you need to change some attributes of a specific UIView you setup via Interface Builder (e.g. we know we want to change programmatically the color of the green UIView in the example above) you should create an outlet which allows you to have a reference to that view in your code. You do it by crtl-drag from the storyboard to your ViewController code - see the example below.
When you have done that you can refer in code to this as any other property, with the difference that it has been created by Interface Builder.
Hope this helps.
You can add an -setButtonFrameToSecondPosition to the view subclass, which simply updates the frame of the button, and then call that from the view controller via one of the +[UIView animate:...] methods.

In iOS, using storyboard, how to setup a view controller inside a container view?

I have created and drawn out a custom UIViewController called AutocompleteVC in my Main storyboard. AutocompleteVC will be used in several different places/storyboards and with different dimensions.
For example, in my Transit storyboard, shown below,, there is a custom UIViewController called TransitVC, shown on the left. In TransitVC, I have a container view with an IBOutlet called autocompleteContainerView. autocompleteContainerView has a segue called autocompleteEmbedSegue to a generic UIViewController, shown on the right in red.
The goal is to have TransitVC hold AutocompleteVC inside autocompleteContainerView. But I'm having trouble getting this to work. The reason I want to do this inside a container view is so I can use autolayout to set constraints on it. Otherwise, I know how to do this purely in code.
I believe my approach might be flawed. What is the correct approach to do this if I want to maximize storyboard usage.
I'm not sure what you are asking. Setting up a parent/child relationship with a container view is very easy, exactly as you have outlined. Just create a container view inside the parent view controller, create the child view controller scene, and then control-drag from the container view to the child view controller to create the embed segue.

Subview management within master/detail view in iOS (with ARC)

I have a master-detail controller for my app. The master controller is a UITabBarController and each tab is a UITableViewController that contains different types of data.
I plan on having a main header / image on the main detail view but then need to add different subviews to the main detail view to detail specific information depending on which tab I am using.
I am currently adding the relevant subview in my
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
Function like so:
UIViewController *subview = [[UIViewController alloc] initWithNibName:#"ItemNotFoundViewController" bundle:nil];
subview.view.frame = CGRectMake(20, 160, subview.view.frame.size.width, subview.view.frame.size.height);
[self.detailViewController.view addSubview:subview.view];
However, I believe that this is a poor way of doing things - every time someone clicks on a table cell another subview will be thrown on top of the stack of previously added subviews, creating memory issues.
What I am wondering is, does ARC take care of this for me? Is my approach passable? and even if it is passable, is there a better way of doing this?
First of all, no. ARC does not take care of this for you. It's not it's purpose to do that and even if it was, how could it know, that you don't want the previously added subviews anymore?
You have to remove those subviews yourself and then ARC will take care of deallocating them (if there are no other references to them).
Anyway that's not the way you're supposed to use a UISplitViewController (the master-detail view controller). As you noticed the split view controller handles two other view controllers. The master- and the detailViewController. In most cases the master view controller isn't changing while the app runs (it's content changes, but usually that's handled by a container view controller like UINavigationController which is assigned as the masterViewController), but the detail view controller does.
Instead of adding subviews to your existing detailViewController you should replace it by a new one. So you should create separate XIBs (what you've apparently done already) for all the view controllers that you want to present in the detail-section. And modify your code to
self.detailViewController = newDetailViewController; //newDetailViewController would be the vc you called subview in your code
instead of
[self.detailViewController.view addSubview:subview.view];
Edit: Notice that this assumes that your detailViewController property does 'the right things' when you set it's value. By default the UISplitViewController only has a property called viewControllers which is an NSArray in which the first object is the masterVC and the second is the detailVC.
Take a look at MultipleDetailViews for an example of how to manage that.
Since you want to have a header view in all your detail view controllers you have various choice of achieving that (which may or may not be applicable in your case, depending on your design):
add the header view to every details vc's XIB
instead of creating many XIBs for all detailVCs, create a new custom UIViewController subclass that modifies it's content based on some parameters you give it, i.e. which tableViewCell was tapped by the user
create a custom container view controller that manages two child view controllers, one for the headline and one for the content above it.
For more information about UISplitViewController and custom container view controller, please refer to:
View Controller Basics
Creating Custom Container View Controllers
No, ARC will not take of this for you, because detailViewController.view will keep a reference to its subviews. It's hard to say what approach is best without knowing more about what you're doing with these views. It would probably be better to just present the new view controller -- it will be deallocated after it's dismissed if you don't have a property pointing to it.

Custom views with Storyboard

In complex screens (View Controllers) I used to separate the whole thing in smaller pieces (I call them widgets). These widgets consist basically of a MyWidget.h and a MyWidget.m file as well as a MyWidget.xib file, where the root element is a UIView and the MyWidget class is the File Owner of the UIView. In the init of this widget I do a loadNibNamed.
In my View Controller I then do a [[MyWidget alloc] init], which I add to View's Controller main view as a sub view. This, so far, works perfectly.
I'm now wondering, how to do the same with storyboard, because I cannot really start to drag in a UIView somewhere, I always have to start with an UIViewController, which I don't want to.
If there is no possible way doing this with a Storyboard, can I simply do it the old way, by using the Storyboard for my main screens and segues, and use a separate .xib file to define custom views?
Putting the widget/view in a separate .xib file works, and is appropriate especially if you might want to reference that same view from multiple View Controllers.
However, sometimes you do want to see the additional view/widget within the same storyboard, and it is possible. Here's how you do it:
Select your view controller in IB (click on the black bar below the view), then drag a UIView from the Object Library into the black bar:
When a view is in the black bar, it's instantiated like any other view in IB but just isn't added to your view hierarchy until you do so in code. Change the view's class to match your own subclass if necessary:
You can hook it up to your view controller like you would hook up any other view:
The added view shows up in your Document Outline and you can hook up actions and references there too:
Now, the problem that remains is that you can't actually see the view no matter how many times you try to click or double click, which would defeat the whole purpose of putting it in the same storyboard. Fortunately there are two workarounds that I know of.
The first workaround is to drag the view from the black bar back into your view controller's view, edit it, then drag it back into the black bar once you're done. This is troublesome but reliable.
The other workaround is more finicky, but I prefer it because it lets me see all my views at the same time:
Drag a UITableView from the Object Library into your newly added view.
Then drag a UITableViewCell into that UITableView.
Once you do that, your view pops out magically by the side, but you have a UITableView that you don't want. You can either resize that to 0x0, or you can delete it and your UIView will (usually) still stay visible.
Occasionally the secondary view will become hidden again in IB. You can repeat the above steps if you deleted the UITableView, or if the UITableView is still in the hierarchy you just need to click on the UITableViewCell and the view will appear again.
The second method works for UIViews but not so well for UIToolbars and is impossible for UIButtons, so the cleanest solution I've found when you need to include lots of different subviews is to attach a single secondary UIView to your view controller as a container that never gets shown, put all your secondary views in there, and use the UITableViewCell trick to make everything visible. I resize my dummy UITableView to 0x0 to make that invisible. Here's a screenshot of how it all looks like together:
If you're just looking to make your view controllers else-where(and not in your story-board), then there's a pretty simple way to accomplish this:
1) Create your CustomViewControllers(abcdController in the code I tried) with their individual xibs as usual.
2) Add a UIViewController(or whatever was the superclass of your CustomViewController) to the story-board.
3) Set the CustomClass to CustomViewController instead of UIViewController as shown here:
4) Finally, in your viewDidLoad, load the custom xib and you're done.
- (void)viewDidLoad
{
[super viewDidLoad];
[[NSBundle mainBundle] loadNibNamed:#"abcdController" owner:self options:nil];
// Do any additional setup after loading the view from its nib.
}
I think you can do something like this to get instance of specific viewcontroller from Storyboard and use view on top of it.
ex:
MyViewController* myViewController = [[UIStoryboard storyboardWithName:#"Main" bundle:nil] instantiateViewControllerWithIdentifier:#"myViewController"];
UIView* view = myViewController.view; //Get the view from your StoryBoard.
Hope this helps
Thanks
Vijay

Resources