8

I have two view controllers, parent and child.

So in the viewDidLoad method I do the following:

ChildViewController* childViewController = [[ChildViewController alloc] init];

[self addChildViewController:childViewController];

// ChildViewController sets his own constraints in viewDidLoad
[self.view addSubview:childViewController.view];

[childViewController didMoveToParentViewController:self];

//
// setup constraints to expand childViewController.view to 
// fill the size of parent view controller
//

So basically what happens is that updateViewConstraints is called on ChildViewController before parent controller constraints apply, so in fact self.view.frame == CGRectZero, exactly the same as I specified in custom loadView method in ChildViewController.

translatesAutoresizingMaskIntoConstraints all set to NO for all views.

What's the proper way to setup constraints in this case so ChildViewController updates his constraints after parent?

Current log from both controllers is pretty frustrating, I do not understand how updateViewConstraints can be called before viewWillLayoutSubviews:

App[47933:c07] ChildViewController::updateViewConstraints. RECT: {{0, 0}, {0, 0}}
App[47933:c07] ParentViewController::updateViewConstraints
App[47933:c07] ChildViewController:viewWillLayoutSubviews. RECT: {{0, 0}, {984, 454}}
App[47933:c07] ChildViewController:viewDidLayoutSubviews. RECT: {{0, 0}, {984, 454}}
pronebird
  • 12,068
  • 5
  • 54
  • 82
  • You need to show the code where you actually add your constraints, otherwise it's hard to say what's wrong. – Maarten Jul 29 '13 at 21:53
  • @Maarten I solved the problem and posted answer below, not sure it's the solution that I was looking for, it seems wrong, but it works. Sorry that I cannot post the code, it's too much code... – pronebird Jul 29 '13 at 22:11

5 Answers5

8

You can add constraints right after invoke of addSubview: , don't forget to set translatesAutoresizingMaskIntoConstraints to false

Here is a code snippet of adding and hiding child view controller with constraints (inspired by apple guide)

Display

private func display(contentController content : UIViewController)
    {
        self.addChildViewController(content)
        content.view.translatesAutoresizingMaskIntoConstraints = false
        self.containerView.addSubview(content.view)
        content.didMove(toParentViewController: self)

        containerView.addConstraints([
            NSLayoutConstraint(item: content.view, attribute: .top, relatedBy: .equal, toItem: containerView, attribute: .top, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: content.view, attribute: .bottom, relatedBy: .equal, toItem: containerView, attribute: .bottom, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: content.view, attribute: .leading, relatedBy: .equal, toItem: containerView, attribute: .leading, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: content.view, attribute: .trailing, relatedBy: .equal, toItem: containerView, attribute: .trailing, multiplier: 1, constant: 0)
            ])
    }

Hide

private func hide(contentController content : UIViewController)
    {
        content.willMove(toParentViewController: nil)
        content.view.removeFromSuperview()
        content.removeFromParentViewController()
    }
Benny Davidovitz
  • 1,152
  • 15
  • 18
5

I have a similar situation where I add a child controller to a visible controller (a popup).

I define the child view controller in interface builder. It's viewDidLoad method just calls setTranslatesAutoresizingMaskIntoConstraints:NO

Then I define this method on the child controller which takes a UIViewController parameter, which is the parent. This method adds itself to the given parent view controller and defines its own constraints:

- (void) addPopupToController:(UIViewController *)parent {
    UIView *view = [self view];
    [self willMoveToParentViewController:parent];
    [parent addChildViewController:self];
    [parent.view addSubview:view];
    [self didMoveToParentViewController:parent];

    NSArray *horizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[view]-0-|"
                                                                             options:0
                                                                             metrics:nil
                                                                               views:NSDictionaryOfVariableBindings(view)];
    [parent.view addConstraints:horizontalConstraints];

    NSArray *verticalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[view]-0-|"
                                                                           options:0
                                                                           metrics:nil
                                                                             views:NSDictionaryOfVariableBindings(view)];
    [parent.view addConstraints:verticalConstraints];
}

then inside the parent UIViewController (the one which is already displayed), when I want to display my child popup view controller it calls:

PopUpNotificationViewController *popup = [[self storyboard] instantiateViewControllerWithIdentifier:@"NotificationPopup"];
[popup addPopupToController:self];

You can define whatever constraints you want on the child controller's view when adding it to the parent controller's view.

pkamb
  • 33,281
  • 23
  • 160
  • 191
Jonathon Horsman
  • 2,303
  • 1
  • 20
  • 18
2
  1. I think the layoutIfNeeded method will only work if you have previously called setNeedsLayout. If you use setNeedsLayout instead, the system will know to update at an appropriate time. Try changing that in your code.

  2. When you add constraints to a view, that view should automatically layout its subviews again to account for the new constraints. There should be no need to call setNeedsLayout unless something has changed since you have added the constraints. Are you adding the constraints to a view and if so: are you adding them to the right view?

  3. One thing you can try is to subclass UIView so that you see a log message whenever the ChildViewController.view or ParentViewController.view performs a fresh layout:

    -(void) layoutSubviews {
        [super layoutSubviews];
        NSLog(@"layoutSubviews was called on the following view: %@", [view description]);
    }
    

Maybe that will reveal something about when your views are (or aren't) layout out their subviews.

Maarten
  • 1,873
  • 1
  • 13
  • 16
  • Right, layoutIfNeeded doesn't really change anything if setNeedsUpdateConstraints wasn't used before. That's my log at the moment: ChildViewController::updateViewConstraints. RECT: {{0, 0}, {0, 0}} ParentViewController::updateViewConstraints ChildViewController_view::layoutSubviews – pronebird Jul 29 '13 at 21:15
  • So Child controller updated first, then parent. All constraints created in viewDidLoad, so I do not exactly understand how it's possible. – pronebird Jul 29 '13 at 21:16
2

According to the following link, I moved constraints creation to viewWillLayoutSubviews, this is the place where view bounds are set properly. I feel this answer misses explanation on why Child view controller's updateViewConstraints called before parent view controller, or maybe it's just some bug in my code, but this workaround solves the problem...

Community
  • 1
  • 1
pronebird
  • 12,068
  • 5
  • 54
  • 82
1

It is convenient to put the code for adding and removing a child controller into an extension. Usage example:

override func viewDidLoad() {
   super.viewDidLoad()
   let childController = YourCustomController()
   add(childController)
}

The code itself:

extension UIViewController {
   
   func add(_ controller: UIViewController) {
      addChild(controller)
      view.addSubview(controller.view)
      
      controller.view.translatesAutoresizingMaskIntoConstraints = false
      NSLayoutConstraint.activate([
         controller.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
         controller.view.topAnchor.constraint(equalTo: view.topAnchor),
         controller.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
         controller.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
      ])
      
      controller.didMove(toParent: self)
   }
   
   func remove() {
      guard parent != nil else {
         return
      }

      willMove(toParent: nil)
      view.removeFromSuperview()
      removeFromParent()
   }
   
}
dronpopdev
  • 797
  • 10
  • 13