Freelance iOS Developer, Rapidly Aging Punk

What to Do When Your VFL Constraints Aren't Respecting Margins in iOS8

Dec 23, 2014 at 03:42PM

Let's say you've got a view with a single button, and you want that button to sit at the top center of the view. You set up the constraints using constraintsWithVisualFormat: like so:

- (void)layoutSubviews {
    [self addConstraints:[NSLayoutConstraint
        constraintsWithVisualFormat:@"V:|-[_button]"
                            options:0
                            metrics:nil
                              views:@{@"_button" : _button}]];

    [self addConstraints:[NSLayoutConstraint
        constraintsWithVisualFormat:@"H:|-[_button]-|"
                            options:0
                            metrics:nil
                              views:@{@"_button" : _button}]];

    [super layoutSubviews];
}

In iOS7 you get exactly what you want:

Result in iOS7

In iOS8 you end up with a button smooshed underneath the status bar:

Result in iOS8

What's going on? Apple changed the default behavior of how AutoLayout respects margins in iOS8. So now when you use V:|-[_button] AutoLayout says "put a default amount of space between the superview and _button, with no respect to the margins" where previously it would have first taken the margins of the superview into account.

There are a few things you can do about this. First there's a new property of UIView called preservesSuperviewLayoutMargins which is set to NO by default. On paper this seems like just the thing you need, but in testing I couldn't figure out what changing this to YES actually did. So scrap that one.

Next, you can manually set the edge insets of your superview like so:

- (UIEdgeInsets)layoutMargins {
    return UIEdgeInsetsMake(22, 0, 0, 0);
}

This works! But I really hate setting that top margin to a literal value. It seems safe enough now, but the old way seemed pretty safe too until it wasn't.

What we end up doing is setting the constraint to use the topLayoutGuide property of the view controller instead of referencing the superview directly:

NSDictionary *views = @{ @"_button" : _button,
                         @"_topLayoutGuide" : _viewController.topLayoutGuide };

    [self addConstraints:[NSLayoutConstraint
        constraintsWithVisualFormat:@"V:[_topLayoutGuide][_button]"
                                options:0
                                metrics:nil
                                views:views]];

This works in both iOS7 & iOS8. The only kludgy part is that you need to set a property on your view that references back to the view controller so you can retrieve the topLayoutGuide, but you can do that easily enough in an initializer.

You can see a complete example project here: https://github.com/richrad/AutoLayoutVFLMargins