Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

iOS 8 Auto Layout issue

I have an annoying problem with auto layout that occurs in iOS 8, while the same setup works fine in iOS 7.

I've simplified and isolated the problematic situation, and here is how it should work - and how it does work in iOS 7:

Constraints working in iOS 7

The feature consists of one container view (blue), and three sub views:

  • A label
  • An image view (with a sliced, scaled image)
  • A button

I've added a slider that controls the width of the container view by amending the width constraint of the container.

The auto layout constraints anchors the label to the left-hand-side, the button to the right-hand-side, and the image view is anchored to both sides without having a width constraint.

In iOS 8, the exact same setup looks like this:

Constraints broken in iOS 8

Notice how the image view (the black line) appears to be anchored with an offset outside to the right of the container view.

I've tried everything and I can't figure out why this behaves differently in iOS 8. I've removed and replaced all the constraints multiple times but I still get the same result.

These are the constraints set on each of the sub views, respectivly:

Sub views constraints

And these are the constraints for the container view:

Container view constraints

The width constraint of the container view is manipulated by the slider like this:

Width constraint connection

- (IBAction)handleSliderChange:(id)sender
{
    self.buttonWidth.constant = self.slider.value * 1024.0;

    [self.containerView setNeedsLayout];
    [self.containerView setNeedsUpdateConstraints];
    [self.containerView layoutIfNeeded];

    [self.view setNeedsLayout];
    [self.view setNeedsUpdateConstraints];
    [self.view layoutIfNeeded];
}

Others that have had problems with auto layout in iOS 8 seems to have been able to solve similar issues by calling setNeedsLayout and / or setNeedsUpdateConstraints directly on the containing view, which is why I'm being very explicit in calling it on both the containing view and the root view. I've tried all combinations of the above, but nothing seems to make a difference.

I'm starting to lose hope of finding a solution, but perhaps someone here has dealt with a similar situation and could bring some clarity into this issue?


Edit: I should add that I've also tried to set constraints from the image view to the label and button as opposed to the edges of the containing view with the same result.


Edit 2: Here's the test project that I've been using when trying to isolate the issue: Download

like image 475
david.emilsson Avatar asked Oct 09 '14 21:10

david.emilsson


2 Answers

It's not a problem of Auto Layout, but must be a bug in iOS 8 about handling sliced image assets.

With your sample project:

    UIImage *img = [UIImage imageNamed:@"pointer-dark-left"];
    NSLog(@"alignmentRect: %@", NSStringFromUIEdgeInsets(img.alignmentRectInsets));
    NSLog(@"capInsets: %@", NSStringFromUIEdgeInsets(img.capInsets));

This code prints

// iOS 7
alignmentRect: {0, 0, 0, 0}
capInsets: {0, 5.5, 0, 134}

// iOS 8
alignmentRect: {0, 0, 0, 65.5}
capInsets: {0, 5.5, 0, 134}

alignmentRectInsets.right is 65.5!

As documented, Autolayout works with alignment rectangles, and UIImageView uses its images alignmentRectInsets. So, your self.line has 65.5pt extra right width.

Of course, you didn't set the Alignment Insets in xcassets editor, I believe it's a bug.

screenshot

Here is a ugly, but working, workaround :(

- (void)viewDidLoad {
    [super viewDidLoad];
    self.line.image = [self.line.image imageWithAlignmentRectInsets:UIEdgeInsetsZero];
}
like image 200
rintaro Avatar answered Oct 21 '22 00:10

rintaro


I think you connected your constraints wrong.

I build up an app with same components and it is working all fine. Maybe you can clear all constraints and setup again.

I add code to do it programmatically.

#import "ViewController.h"

@interface ViewController () {    
    NSLayoutConstraint* csViewWidth;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    NSDictionary *views = @{
                        @"label": self.uiLabel,
                        @"image": self.uiImage,
                        @"button": self.uiButton
                        };

    NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[label]-[image]-[button]-|"
                                                                options:0
                                                                metrics:nil
                                                                  views:views];
    [self.view addConstraints:constraints];

    NSArray *panelConstraint = [NSLayoutConstraint constraintsWithVisualFormat:@"H:[panel(320)]" options:0 metrics:nil views:@{ @"panel": self.uiPanel }];
    csViewWidth = [panelConstraint firstObject];
    [self.view addConstraints:@[csViewWidth]];
}

- (IBAction)onSliderChanged:(UISlider*)sender {
    float containerWidth = self.view.frame.size.width;
    csViewWidth.constant = sender.value * containerWidth;
}

@end

This solution is tested and working fine. There is no need to call setNeedsLayout and layoutIfNeeded.

like image 22
Tuan Avatar answered Oct 21 '22 00:10

Tuan