Question

I have a UIViewController which creates this layout:

UIViewController

The part that gives me trouble is the darker area on the bottom, containing the text.

The darker part is a UIScrollView subclass, which creates this layout using Auto Layout:

UIScrollView

I have a couple of UILabels ("Omschrijving", "Informatie", and the small labels at the bottom of the view) and a UITextView (the view containing the text starting with "Robert Langdon"). I set the UITextViews height explicitly to 60 points, and when the "Meer" button is tapped, I calculate its full height using boundingRectWithSize:options:attributes:context. I then change its height constraint constant from its prior hardcoded value to the value I've calculated.

That's where it goes wrong. The layout is absolutely fine until the UITextView's height changes. All the content in the UIScrollView subclass seems to move inexplicably. I looked at the view hierarchy with Reveal: new view hierarchy.

I've been messing with the constraints for hours now and I can't find a solution.

All the views have translatesAutoresizingMaskIntoConstraints set to NO. This is iOS 8 on the simulator, with Xcode 6 b4.

My init methods looks like this:

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        self.translatesAutoresizingMaskIntoConstraints = NO;
        [self layoutIfNeeded];

        [self addExpandButton];
        [self setupUI];
        [self setupConstraints];
        [self layoutIfNeeded];

        self.backgroundColor = [UIColor clearColor];
        self.textView.backgroundColor = [UIColor clearColor];
        self.textView.userInteractionEnabled = NO;
    }
    return self;
}

setupUI creates all the views and adds them to the scrollView. I've removed some repetitive lines of code for the sake of brevity

- (void)setupUI
{
    // Initialization
    UILabel *descriptionLabel = [[UILabel alloc] initWithFrame:CGRectZero];

    UITextView *textView = [[UITextView alloc] initWithFrame:CGRectMake(0.0, 0.0, 100.0, 60.0) textContainer:nil];
    textView.translatesAutoresizingMaskIntoConstraints = NO;
    textView.editable = NO;
    textView.textContainerInset = UIEdgeInsetsMake(0.0, -5.0, 0.0, -5.0);
    textView.textColor = [UIColor whiteColor];

   // init all the UILabels with CGRectZero frames
   // ...
   // ...

    UIView *separator = [[UIView alloc] initWithFrame:CGRectZero];
    separator.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.5];

    // Set the text
    descriptionLabel.text = @"Omschrijving";

    textView.text = @"Robert Langdon, hoogleraar kunstgeschiedenis en symboliek, wordt op een nacht wakker in een ziekenhuis in Florence zonder te weten hoe hij daar is beland. Geholpen door een stoïcijnse jonge vrouw, Sienna Brooks, vlucht Langdon en raakt hij verzeild in een duizelingwekkend avontuur. Langdon ontdekt dat hij in het bezit is van een reeks verontrustende codes, gecreëerd door een briljante wetenschapper; een genie dat geobsedeerd is door het einde van de wereld en het duistere meesterwerk Inferno van Dante Alighieri.";

    // Set the text of the labels
    // ...
    // ...

    descriptionLabel.font = [UIFont fontWithName:@"ProximaNova-Semibold" size:14.0];
    textView.font = [UIFont fontWithName:@"ProximaNova-Regular" size:12.0];
    informationLabel.font = [UIFont fontWithName:@"ProximaNova-Semibold" size:14.0];

    UIFont *font = [UIFont fontWithName:@"ProximaNova-Regular" size:10.0];
    // Set the font of the labels
    // ...
    // ...

    // Add the views to the view hierarchy
    [self addSubview:descriptionLabel];
    [self addSubview:textView];
    [self addSubview:separator];
    [self addSubview:informationLabel];
    // Add the other labels as subviews
    // ...
    // ...

    [self.subviews enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        if ([obj isKindOfClass:[UILabel class]]) {
            UILabel *label = (UILabel *)obj;
            [label sizeToFit];
            label.textColor = [UIColor whiteColor];
        }
    }];

    // Assign the local properties to properties
    // ...
    // ...


    for (UIView *view in self.subviews) {
        view.translatesAutoresizingMaskIntoConstraints = NO;
    }

}

Now on to the constraints. I have a big method that adds all the constraints called -addConstraints.

- (void)setupConstraints
{
    // Omschrijving label top
    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-3-[descriptionLabel]-3-[textView]"
                                                                 options:NSLayoutFormatAlignAllLeading
                                                                 metrics:nil
                                                                   views:@{@"descriptionLabel": self.descriptionLabel,
                                                                           @"textView": self.textView}]];

    [self addConstraint:[NSLayoutConstraint constraintWithItem:self.descriptionLabel
                                                     attribute:NSLayoutAttributeTop
                                                     relatedBy:NSLayoutRelationEqual
                                                        toItem:self
                                                     attribute:NSLayoutAttributeTop
                                                    multiplier:1.0
                                                      constant:3.0]];

    // Omschrijving label leading
    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-10-[descriptionLabel]"
                                                                 options:0
                                                                 metrics:nil
                                                                   views:@{@"descriptionLabel": self.descriptionLabel}]];

    // Text view width
    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"[textView(==width)]"
                                                                 options:0
                                                                 metrics:@{@"width": @220.0}
                                                                   views:@{@"textView": self.textView}]];

    // Text view height
    NSArray *textViewHeightConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:[textView(==60.0)]"
                                                                                 options:0
                                                                                 metrics:nil
                                                                                   views:@{@"textView": self.textView}];

    [self addConstraints:textViewHeightConstraints];
    self.textViewHeightConstraints = textViewHeightConstraints;
    NSLog(@"%@", self.textViewHeightConstraints);


    // Text view expand button
    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[textView][expandButton(==12.0)]"
                                                                 options:NSLayoutFormatAlignAllTrailing
                                                                 metrics:nil
                                                                   views:@{@"textView": self.textView, @"expandButton": self.expandButton}]];

    // Separator
    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[expandButton]-6-[separator]"
                                                                 options:NSLayoutFormatAlignAllTrailing
                                                                 metrics:nil
                                                                   views:@{@"expandButton": self.expandButton, @"separator": self.separator}]];

    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"[separator(==textView)]"
                                                                 options:0
                                                                 metrics:nil
                                                                   views:@{@"separator": self.separator, @"textView": self.textView}]];

    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[separator(==0.5)]"
                                                                 options:0
                                                                 metrics:nil
                                                                   views:@{@"separator": self.separator}]];

    NSString *leftVisualFormatString = @"V:[separator]-6-[informationLabel]-spacing-[languageDescriptionLabel]-spacing-[categoryDescriptionLabel]-spacing-[publisherDescriptionLabel]-spacing-[publishedDateDescriptionLabel]-spacing-[pageCountDescriptionLabel]-|";

    NSDictionary *descriptionViews = @{@"separator": self.separator,
                                       @"informationLabel": self.informationLabel,
                                       @"languageDescriptionLabel": self.languageDescriptionLabel,
                                       @"categoryDescriptionLabel": self.categoryDescriptionLabel,
                                       @"publisherDescriptionLabel": self.publisherDescriptionLabel,
                                       @"publishedDateDescriptionLabel": self.publishedDateDescriptionLabel,
                                       @"pageCountDescriptionLabel": self.pageCountDescriptionLabel};

    NSDictionary *metrics = @{@"spacing": @1.0};

    // All at once: vertical spacing and leading alignment for the labels on the left
    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:leftVisualFormatString
                                                                 options:NSLayoutFormatAlignAllLeading
                                                                 metrics:metrics
                                                                   views:descriptionViews]];

    // Same, for the righthand labels

    NSDictionary *views = @{@"languageLabel": self.languageLabel,
                            @"categoryLabel": self.categoryLabel,
                            @"publisherLabel": self.publisherLabel,
                            @"publishedDateLabel": self.publishedDateLabel,
                            @"pageCountLabel": self.pageCountLabel};

    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"[pageCountDescriptionLabel]-20-[pageCountLabel]"
                                                                 options:0
                                                                 metrics:nil
                                                                   views:@{@"pageCountDescriptionLabel": self.pageCountDescriptionLabel,
                                                                           @"pageCountLabel": self.pageCountLabel}]];


    NSString *rightVisualFormatString = @"V:[languageLabel]-spacing-[categoryLabel]-spacing-[publisherLabel]-spacing-[publishedDateLabel]-spacing-[pageCountLabel]-|";


    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:rightVisualFormatString
                                                                 options:NSLayoutFormatAlignAllLeading
                                                                 metrics:metrics
                                                                   views:views]];
}

As I said, this works fine until I call this method:

- (void)tappedExpandButton:(id)sender
{
    if (self.textViewHeightConstraints.count == 1) {
        NSLayoutConstraint *constraint = self.textViewHeightConstraints.firstObject;

        CGFloat newHeight = [self.textView.text boundingRectWithSize:CGSizeMake(self.textView.textContainer.size.width, CGFLOAT_MAX)
                                                             options:NSStringDrawingUsesLineFragmentOrigin
                                                          attributes:@{NSFontAttributeName: self.textView.font}
                                                             context:nil].size.height;

        constraint.constant = ceilf(newHeight);
        [self layoutIfNeeded];
    }
}

Thanks already!

Was it helpful?

Solution

I think I've figured it out. For some reason I don't yet understand, the containing UIScrollView's frame changed when adding the constraint. That still seems really weird to me, since I think only its contentSize should change, but adding constraints from the dark area (which is a UIView) to the UIScrollView fixed it. This is the code:

[self.darkEffectView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|[descriptionView]|"
                                                                                options:0
                                                                                metrics:nil
                                                                                  views:@{@"descriptionView": self.descriptionView}]];

[self.darkEffectView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[descriptionView]|"
                                                                                options:0
                                                                                metrics:nil
                                                                                  views:@{@"descriptionView": self.descriptionView}]];
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top