Use autolayout to set dynamic UIView to match container view

Feta picture Feta · Apr 23, 2013 · Viewed 67.8k times · Source

I have a UIView in IB that has another UIView within it, which I am using as a container view. In code, I create three different views and then animate the appropriate one into the container's view depending on the state of the app. Only one of the three different views will be valid at any given time. The problem is, when I run on different iPhone's in the simulator, my new subviews are not scaled to match the container view. I am using autolayout. For testing purposes, I've set up my subviews to just be a big button that has all it's edges constrained to the superview. And the container view also has it's edges constrained to it's superview. What I want is for the subview to match the container's view. i.e the button stretches the entire size of the container view. When run on different iPhones the size of the container view and therefore the subview should scale proportionally relative to the different iPhone's screen sizes.

Below is the code I use to init my subview and set up it's constraints relative to the container view.

UIView *containerView = self.subView;
UIView *newSubview = [[mySubview alloc] init];
[newSubview setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.containerView addSubview:newSubview];

[self.containerView addConstraint:[NSLayoutConstraint constraintWithItem:newSubview attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.containerView attribute:NSLayoutAttributeTop multiplier:1.0 constant:0]];

[self.containerView addConstraint:[NSLayoutConstraint constraintWithItem:newSubview attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.containerView attribute:NSLayoutAttributeLeading multiplier:1.0 constant:0]];

[self.containerView addConstraint:[NSLayoutConstraint constraintWithItem:newSubview attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self.containerView attribute:NSLayoutAttributeWidth multiplier:1.0 constant:0]];

[self.containerView addConstraint:[NSLayoutConstraint constraintWithItem:newSubview attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self.containerView attribute:NSLayoutAttributeHeight multiplier:1.0 constant:0]];  

I just can't seem to get this to work. I'm fairly new to autolayout and am not sure what I am doing wrong and would like to stop banging my head against this wall. Any help would be terrific. :)


************* ADDITIONAL INFO **************


Sorry, I've not stated my problem as clearly as I could have. So here is some more info with screen shots. First, I'll describe what I've done code-wise.

In didFinishLaunchingWithOptions in AppDelegate.m, I create MyViewController like this,

self.myViewController = [[MyViewController alloc] initWithNibName:@"MyViewController" bundle:nil];

In viewDidLoad in MyViewController.m, I create mySubview and add it to my containerView and create constraints for it like this,

UIView *containerView = self.containerView;
UIView *mySubview = [[MySubview alloc] init];
[mySubview setTranslatesAutoresizingMaskIntoConstraints:NO];
[containerView addSubview:mySubview];

NSDictionary *views = NSDictionaryOfVariableBindings(mySubview);
[containerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[mySubview]|"
                                                                 options:0
                                                                 metrics:nil
                                                                   views:views]];
[containerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[mySubview]|"
                                                                  options:0
                                                                  metrics:nil
                                                                    views:views]];

And finally in init in MySubview.h I add the nib as a subview like this,

- (id)init
{
    if(self = [super init])
    {
        NSArray *nibArray = [[NSBundle mainBundle]loadNibNamed:@"MySubview" owner:self options:nil];
        [self addSubview:[nibArray objectAtIndex:0]];
    }
    return self;
}

A couple of things to note that may help,

In MyViewController.xib, I have a UIView that I am using as the containerView. It has an IBOutlet to UIView* containerView and is the one referenced above. The only constraints I have for the containerView in IB are, leading, trailing and bottom space to Superview and top space to Superview = 44.

For MySubview.xib, the height and width are 300, (no constraints are used for height or width). I feel like these sizes should not matter since mySubview is supposed to get constrained to containerView.

In MySubview.xib I have 3 objects, topButton: height = 29, middleView: height = 242 and bottomButton: height = 29. (see attached image) TopButton has leading, trailing and top to Superview constraints and a bottom to middleView constraint. MiddleView has leading and trailing to Superview constraints and a top to topButton constraint and a bottom to bottomButton constraints. And finally bottomButton has leading, trailing and bottom to Superview constraints and a top to middleView constraint.

What I want to happen is for mySubview to scale to fit the containerView since constraints have been create and added but instead, mySubview gets very large and clips containerView.

Here are some screen shots:

MyViewController.xib, the blue rectangle below the title is my container view and has the IBOutlet containerView.

http://i33.tinypic.com/14o3lmd.png

MySubview.xib

http://i37.tinypic.com/344ukk3.png

And finally, the result, which are incorrect.

http://i34.tinypic.com/nbyqua.png

Instead I would want this, which I have faked just to get the screen shot.

On iPhone 4,

http://tinypic.com/r/w9fp5e/4

On iPhone 5,

http://tinypic.com/r/2z8nldv/4

As you can see in the fake screen shots, mySubview scales to fit the containerView, even as containerView scales a bit to adjust for the different phone screen sizes.

Hope I did not go overboard with the info. Any help would be terrific. I feel like I am close but must be missing one key step. Grrrrr.

Answer

Rob picture Rob · Apr 23, 2013

A couple of thoughts:

  1. This basically looks fine, other than some variable name weirdness: Your local variable, containerView is equal to self.subView, but all of your other lines refer to a different variable, a class property, self.containerView. Did you omit a line where you set that containerView class property? But when I fixed that, your code worked fine for me.

  2. Make sure you're not trying to look at the frame immediately after setting the constraints, as the change will not yet be reflected in the frame settings. You can do a [containerView layoutIfNeeded]; if you want to force it to relayout everything based upon the constraints. Also, if you want to confirm frame settings, it's better to defer looking at those values until after viewDidAppear (i.e. viewDidLoad is too early in the view construction process).

  3. A minor tweak on your code (and unrelated to your problem), but when I'm setting the constraints within a container view, I often will set not only NSLayoutAttributeTop and NSLayoutAttributeLeading, like you did, but also NSLayoutAttributeBottom and NSLayoutAttributeTrailing (rather than NSLayoutAttributeWidth and NSLayoutAttributeHeight). It accomplishes the same thing as your code, but when using non-zero values, it is a fraction more intuitive.

    Anyway, I just did the following code, permutation of yours, and it works fine:

    - (IBAction)didTouchUpInsideAddView:(id)sender
    {
        UIView *containerView = self.containerView;
        UIView *newSubview = [[UIView alloc] initWithFrame:CGRectZero]; // initializing with CGRectZero so we can see it change
        newSubview.translatesAutoresizingMaskIntoConstraints = NO;
        newSubview.backgroundColor = [UIColor lightGrayColor];
        [containerView addSubview:newSubview];
    
        [containerView addConstraint:[NSLayoutConstraint constraintWithItem:newSubview
                                                                  attribute:NSLayoutAttributeTop
                                                                  relatedBy:NSLayoutRelationEqual
                                                                     toItem:containerView
                                                                  attribute:NSLayoutAttributeTop
                                                                 multiplier:1.0
                                                                   constant:0.0]];
    
        [containerView addConstraint:[NSLayoutConstraint constraintWithItem:newSubview
                                                                  attribute:NSLayoutAttributeLeading
                                                                  relatedBy:NSLayoutRelationEqual
                                                                     toItem:containerView
                                                                  attribute:NSLayoutAttributeLeading
                                                                 multiplier:1.0
                                                                   constant:0.0]];
    
        [containerView addConstraint:[NSLayoutConstraint constraintWithItem:newSubview
                                                                  attribute:NSLayoutAttributeBottom
                                                                  relatedBy:NSLayoutRelationEqual
                                                                     toItem:containerView
                                                                  attribute:NSLayoutAttributeBottom
                                                                 multiplier:1.0
                                                                   constant:0.0]];
    
        [containerView addConstraint:[NSLayoutConstraint constraintWithItem:newSubview
                                                                  attribute:NSLayoutAttributeTrailing
                                                                  relatedBy:NSLayoutRelationEqual
                                                                     toItem:containerView
                                                                  attribute:NSLayoutAttributeTrailing
                                                                 multiplier:1.0
                                                                   constant:0.0]];
    
        // the frame is still `CGRectZero` at this point
    
        NSLog(@"newSubview.frame before = %@", NSStringFromCGRect(newSubview.frame));
    
        // if not doing this in `viewDidLoad` (e.g. you're doing this in
        // `viewDidAppear` or later), you can force `layoutIfNeeded` if you want
        // to look at `frame` values. Generally you don't need to do this unless
        // manually inspecting `frame` values or when changing constraints in a
        // `animations` block of `animateWithDuration`.
    
        [containerView layoutIfNeeded];
    
        // everything is ok here
    
        NSLog(@"containerView.bounds after = %@", NSStringFromCGRect(containerView.bounds));
        NSLog(@"newSubview.frame after = %@", NSStringFromCGRect(newSubview.frame));
    }
    
  4. You can simplify that code a little using visual format language, e.g.:

    NSDictionary *views = NSDictionaryOfVariableBindings(newSubview);
    
    [containerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[newSubview]|"
                                                                          options:0
                                                                          metrics:nil
                                                                            views:views]];
    [containerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[newSubview]|"
                                                                          options:0
                                                                          metrics:nil
                                                                            views:views]];
    

    I find it easier to get the constraints right using visual format language. A little less error-prone (for me, at least). There are, though, some constraints that cannot be represented in visual format language, in which case I fall back to the syntax you outline.

  5. In your revised question, you show us an init method for your subview, which does another addSubview. You have to set constraints there, too. Bottom line, wherever you do addSubview, you have to set your constraints, e.g.

    - (id)init
    {
        if(self = [super init])
        {
            NSArray *nibArray = [[NSBundle mainBundle]loadNibNamed:@"MySubview" owner:self options:nil];
            UIView *subview = [nibArray objectAtIndex:0];
            subview.translatesAutoresizingMaskIntoConstraints = NO;
    
            [self addSubview:subview];
    
            NSDictionary *views = NSDictionaryOfVariableBindings(subview);
            [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[subview]|"
                                                                         options:0
                                                                         metrics:nil
                                                                           views:views]];
            [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[subview]|"
                                                                         options:0
                                                                         metrics:nil
                                                                           views:views]];
        }
        return self;
    }